[
  {
    "path": ".checkstyle/checkstyle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<!DOCTYPE module PUBLIC \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\"\n    \"https://checkstyle.org/dtds/configuration_1_3.dtd\">\n<module name=\"Checker\">\n    <property name=\"charset\" value=\"UTF-8\"/>\n    <property name=\"fileExtensions\" value=\"java, properties, xml\"/>\n    <property name=\"severity\" value=\"error\"/>\n\n    <!-- https://checkstyle.org/config_filefilters.html#BeforeExecutionExclusionFileFilter -->\n    <module name=\"BeforeExecutionExclusionFileFilter\">\n        <property name=\"fileNamePattern\" value=\"module\\-info\\.java$\"/>\n    </module>\n\n    <!-- https://checkstyle.org/config_whitespace.html#FileTabCharacter -->\n    <module name=\"FileTabCharacter\">\n        <property name=\"eachLine\" value=\"true\"/>\n    </module>\n\n    <!-- https://checkstyle.org/config_misc.html#NewlineAtEndOfFile -->\n    <module name=\"NewlineAtEndOfFile\"/>\n\n    <!-- https://checkstyle.org/config_filters.html#SuppressionFilter -->\n    <module name=\"SuppressionFilter\">\n        <property name=\"file\" value=\"${configDirectory}/suppressions.xml\"/>\n    </module>\n\n    <!-- https://checkstyle.org/config_filters.html#SuppressWarningsFilter -->\n    <module name=\"SuppressWarningsFilter\"/>\n\n    <!-- carbon -->\n    <!-- Temporary until https://github.com/checkstyle/checkstyle/issues/5313 -->\n    <!-- empty line at beginning after class/interface/enum definition -->\n    <module name=\"RegexpMultiline\">\n        <property name=\"format\" value=\"^([^\\r\\n ]+ )*(class|interface|enum) [^{]*\\{\\r?\\n[^\\r\\n}]\"/>\n        <property name=\"message\" value=\"Leave an empty line after class/interface/enum definition!\"/>\n        <property name=\"fileExtensions\" value=\"groovy,java\"/>\n    </module>\n\n    <!-- empty line before the last } of class/interface/enum definition -->\n    <module name=\"RegexpMultiline\">\n        <property name=\"format\" value=\"}\\r\\n}\\r\\n\\Z\"/>\n        <property name=\"message\" value=\"Leave an empty line before the last } of class/interface/enum!\"/>\n        <property name=\"fileExtensions\" value=\"groovy,java\"/>\n        <property name=\"matchAcrossLines\" value=\"true\"/>\n    </module>\n\n    <!-- https://checkstyle.org/config_filters.html#SuppressWithPlainTextCommentFilter -->\n    <module name=\"SuppressWithPlainTextCommentFilter\"/>\n\n    <module name=\"TreeWalker\">\n        <!-- https://checkstyle.org/config_misc.html#ArrayTypeStyle -->\n        <module name=\"ArrayTypeStyle\"/>\n\n        <!-- https://checkstyle.org/config_javadoc.html#AtclauseOrder -->\n        <module name=\"AtclauseOrder\">\n            <property name=\"violateExecutionOnNonTightHtml\" value=\"true\"/>\n            <property name=\"tagOrder\"\n                      value=\"@author, @deprecated, @exception, @param, @return, @serial, @serialData, @serialField, @throws, @see, @since, @sinceMinecraft, @version\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_imports.html#AvoidStarImport -->\n        <module name=\"AvoidStarImport\"/>\n\n        <!-- https://checkstyle.sourceforge.io/config_blocks.html#NeedBraces -->\n        <module name=\"NeedBraces\"/>\n\n        <!-- https://checkstyle.org/config_misc.html#AvoidEscapedUnicodeCharacters -->\n        <module name=\"AvoidEscapedUnicodeCharacters\">\n            <property name=\"allowByTailComment\" value=\"true\"/>\n            <property name=\"allowEscapesForControlCharacters\" value=\"true\"/>\n            <property name=\"allowNonPrintableEscapes\" value=\"true\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_misc.html#CommentsIndentation -->\n        <module name=\"CommentsIndentation\"/>\n\n        <!-- https://checkstyle.org/config_imports.html#CustomImportOrder -->\n        <module name=\"CustomImportOrder\">\n            <property name=\"customImportOrderRules\" value=\"THIRD_PARTY_PACKAGE###STATIC\"/>\n            <property name=\"standardPackageRegExp\" value=\"^$\"/>\n            <property name=\"sortImportsInGroupAlphabetically\" value=\"true\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_whitespace.html#EmptyForInitializerPad -->\n        <module name=\"EmptyForInitializerPad\"/>\n\n        <!-- https://checkstyle.org/config_whitespace.html#EmptyForIteratorPad -->\n        <module name=\"EmptyForIteratorPad\"/>\n\n        <!-- https://checkstyle.org/config_whitespace.html#EmptyLineSeparator -->\n        <module name=\"EmptyLineSeparator\">\n            <property name=\"allowMultipleEmptyLines\" value=\"false\"/>\n            <property name=\"allowMultipleEmptyLinesInsideClassMembers\" value=\"false\"/>\n            <property name=\"allowNoEmptyLineBetweenFields\" value=\"true\"/>\n            <property name=\"tokens\"\n                      value=\"CLASS_DEF, CTOR_DEF, ENUM_DEF, IMPORT, INSTANCE_INIT, INTERFACE_DEF, METHOD_DEF, STATIC_IMPORT, STATIC_INIT, VARIABLE_DEF\"/> <!-- remove PACKAGE_DEF, temporarily remove COMPACT_CTOR_DEF, RECORD_DEF -->\n        </module>\n\n        <!-- https://checkstyle.org/config_coding.html#FallThrough -->\n        <module name=\"FallThrough\">\n            <property name=\"checkLastCaseGroup\" value=\"true\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_design.html#FinalClass -->\n        <module name=\"FinalClass\"/>\n\n        <!-- https://checkstyle.org/config_coding.html#FinalLocalVariable -->\n        <module name=\"FinalLocalVariable\">\n            <property name=\"tokens\" value=\"PARAMETER_DEF, VARIABLE_DEF\"/> <!-- add PARAMETER_DEF -->\n            <property name=\"validateEnhancedForLoopVariable\" value=\"true\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_whitespace.html#GenericWhitespace -->\n        <module name=\"GenericWhitespace\"/>\n\n        <!-- https://checkstyle.org/config_design.html#HideUtilityClassConstructor -->\n        <module name=\"HideUtilityClassConstructor\"/>\n\n        <!-- https://checkstyle.org/config_imports.html#IllegalImport -->\n        <module name=\"IllegalImport\">\n            <property name=\"illegalPkgs\" value=\"sun, jdk, com.sun\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_coding.html#IllegalTokenText -->\n        <module name=\"IllegalTokenText\">\n            <property name=\"format\"\n                      value=\"($|[^\\\\])\\\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\\\(0(10|11|12|14|15|42|47)|134)\"/>\n            <property name=\"message\"\n                      value=\"Consider using special escape sequence instead of octal value or Unicode escaped value.\"/>\n            <property name=\"tokens\" value=\"CHAR_LITERAL, STRING_LITERAL\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_misc.html#Indentation -->\n        <module name=\"Indentation\">\n            <property name=\"arrayInitIndent\" value=\"4\"/>\n            <property name=\"basicOffset\" value=\"4\"/>\n            <property name=\"braceAdjustment\" value=\"0\"/>\n            <property name=\"caseIndent\" value=\"4\"/>\n            <property name=\"lineWrappingIndentation\" value=\"0\"/>\n            <property name=\"throwsIndent\" value=\"4\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_javadoc.html#InvalidJavadocPosition -->\n        <module name=\"InvalidJavadocPosition\"/>\n\n        <!-- https://checkstyle.org/config_javadoc.html#JavadocContentLocation -->\n        <module name=\"JavadocContentLocation\"/>\n\n        <!-- https://checkstyle.org/config_javadoc.html#JavadocMissingWhitespaceAfterAsterisk -->\n        <module name=\"JavadocMissingWhitespaceAfterAsterisk\"/>\n\n        <!-- https://checkstyle.org/config_javadoc.html#JavadocParagraph -->\n        <module name=\"JavadocParagraph\"/>\n\n        <!-- https://checkstyle.org/config_javadoc.html#JavadocMissingWhitespaceAfterAsterisk -->\n        <module name=\"JavadocTagContinuationIndentation\"/>\n\n        <!-- https://checkstyle.org/config_blocks.html#LeftCurly -->\n        <module name=\"LeftCurly\"/>\n\n        <!-- https://checkstyle.org/config_naming.html#MethodName -->\n        <module name=\"MethodName\">\n            <property name=\"format\" value=\"^(?:(?:.{1,3})|(?:[gs]et[^A-Z].*)|(?:(?:[^gsA-Z]..|.[^e].|..[^t]).+))$\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_whitespace.html#MethodParamPad -->\n        <module name=\"MethodParamPad\"/>\n\n        <!-- https://checkstyle.org/config_javadoc.html#MissingJavadocMethod -->\n        <module name=\"MissingJavadocMethod\"/>\n\n        <!-- https://checkstyle.org/config_javadoc.html#MissingJavadocPackage -->\n        <module name=\"MissingJavadocPackage\"/>\n\n        <!-- https://checkstyle.org/config_javadoc.html#MissingJavadocType -->\n        <module name=\"MissingJavadocType\"/>\n\n        <!-- https://checkstyle.org/config_coding.html#MultipleVariableDeclarations -->\n        <module name=\"MultipleVariableDeclarations\"/>\n\n        <!-- https://checkstyle.org/config_coding.html#NoFinalizer -->\n        <module name=\"NoFinalizer\"/>\n\n        <!-- https://checkstyle.org/config_whitespace.html#NoLineWrap -->\n        <module name=\"NoLineWrap\"/>\n\n        <!-- https://checkstyle.org/config_javadoc.html#NonEmptyAtclauseDescription -->\n        <module name=\"NonEmptyAtclauseDescription\"/>\n\n        <!-- https://checkstyle.org/config_whitespace.html#NoWhitespaceAfter -->\n        <module name=\"NoWhitespaceAfter\">\n            <property name=\"allowLineBreaks\" value=\"false\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_whitespace.html#NoWhitespaceBefore -->\n        <module name=\"NoWhitespaceBefore\">\n            <property name=\"allowLineBreaks\" value=\"true\"/>\n            <property name=\"tokens\"\n                      value=\"COMMA, DOT, LABELED_STAT, METHOD_REF, POST_DEC, POST_INC, SEMI\"/> <!-- remove ELLIPSIS -->\n        </module>\n\n        <!-- https://checkstyle.org/config_coding.html#OneStatementPerLine -->\n        <module name=\"OneStatementPerLine\"/>\n\n        <!-- https://checkstyle.org/config_misc.html#OuterTypeFilename -->\n        <module name=\"OuterTypeFilename\"/>\n\n        <!-- https://checkstyle.org/config_imports.html#RedundantImport -->\n        <module name=\"RedundantImport\"/>\n\n        <!-- https://checkstyle.org/config_modifier.html#RedundantModifier -->\n        <module name=\"RedundantModifier\">\n            <property name=\"tokens\"\n                      value=\"ANNOTATION_FIELD_DEF, CLASS_DEF, CTOR_DEF, ENUM_DEF, INTERFACE_DEF, VARIABLE_DEF\"/> <!-- remove METHOD_DEF and RESOURCE -->\n        </module>\n\n        <!-- https://checkstyle.org/config_javadoc.html#RequireEmptyLineBeforeBlockTagGroup -->\n        <module name=\"RequireEmptyLineBeforeBlockTagGroup\"/>\n\n        <!-- https://checkstyle.org/config_coding.html#RequireThis -->\n        <module name=\"RequireThis\">\n            <property name=\"validateOnlyOverlapping\" value=\"false\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_blocks.html#RightCurly -->\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlyAlone\"/>\n            <property name=\"option\" value=\"alone\"/>\n            <property name=\"tokens\"\n                      value=\"ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_DEF, INSTANCE_INIT, LITERAL_FOR, LITERAL_WHILE, METHOD_DEF, STATIC_INIT\"/>\n        </module>\n        <module name=\"RightCurly\">\n            <property name=\"id\" value=\"RightCurlySame\"/>\n            <property name=\"option\" value=\"same\"/>\n            <property name=\"tokens\"\n                      value=\"LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_IF, LITERAL_TRY\"/> <!-- add LITERAL_DO -->\n        </module>\n\n        <!-- https://checkstyle.org/config_whitespace.html#SeparatorWrap -->\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapEol\"/>\n            <property name=\"option\" value=\"eol\"/>\n            <property name=\"tokens\" value=\"COMMA, SEMI, ELLIPSIS, RBRACK, ARRAY_DECLARATOR, METHOD_REF\"/>\n        </module>\n        <module name=\"SeparatorWrap\">\n            <property name=\"id\" value=\"SeparatorWrapNl\"/>\n            <property name=\"option\" value=\"nl\"/>\n            <property name=\"tokens\" value=\"DOT, AT\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_coding.html#SimplifyBooleanExpression -->\n        <module name=\"SimplifyBooleanExpression\"/>\n\n        <!-- https://checkstyle.org/config_coding.html#SimplifyBooleanReturn -->\n        <module name=\"SimplifyBooleanReturn\"/>\n\n        <!-- https://checkstyle.org/config_whitespace.html#SingleSpaceSeparator -->\n        <module name=\"SingleSpaceSeparator\">\n            <property name=\"validateComments\" value=\"true\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_javadoc.html#SummaryJavadoc -->\n        <module name=\"SummaryJavadoc\"/>\n\n        <!-- https://checkstyle.org/config_annotation.html#SuppressWarningsHolder -->\n        <module name=\"SuppressWarningsHolder\"/>\n\n        <!-- https://checkstyle.org/config_whitespace.html#TypecastParenPad -->\n        <module name=\"TypecastParenPad\"/>\n\n        <!-- carbon -->\n        <!-- https://checkstyle.sourceforge.io/config_whitespace.html#ParenPad -->\n        <module name=\"ParenPad\"/>\n\n        <!-- https://checkstyle.org/config_coding.html#UnnecessaryParentheses -->\n        <module name=\"UnnecessaryParentheses\"/>\n\n        <!-- https://checkstyle.org/config_imports.html#UnusedImports -->\n        <module name=\"UnusedImports\"/>\n\n        <!-- https://checkstyle.org/config_whitespace.html#WhitespaceAfter -->\n        <module name=\"WhitespaceAfter\">\n            <property name=\"tokens\"\n                      value=\"COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE, LITERAL_WHILE, LITERAL_FOR\"/>\n        </module>\n\n        <!-- https://checkstyle.org/config_whitespace.html#WhitespaceAround -->\n        <module name=\"WhitespaceAround\">\n            <property name=\"ignoreEnhancedForColon\" value=\"false\"/>\n            <property name=\"allowEmptyTypes\" value=\"true\"/>\n            <property name=\"allowEmptyLambdas\" value=\"true\"/>\n            <property name=\"tokens\"\n                      value=\"ASSIGN, COLON, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, EQUAL, GE, GT, LAND, LCURLY, LE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR, SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND\"/>\n        </module>\n\n        <!--\n        #####################\n        #### third-party ####\n        #####################\n        -->\n\n        <!-- https://checkstyle.org/config_javadoc.html#WriteTag -->\n        <!-- https://gitlab.com/stellardrift/stylecheck/-/blob/dev/src/main/java/ca/stellardrift/stylecheck/FilteringWriteTag.java -->\n        <module name=\"FilteringWriteTag\">\n            <property name=\"tag\" value=\"@since\\s\"/>\n            <property name=\"tagFormat\" value=\"\\d\\.\\d+\\.\\d+\"/>\n            <property name=\"tagSeverity\" value=\"ignore\"/>\n            <property name=\"minimumScope\" value=\"public\"/>\n            <property name=\"tokens\"\n                      value=\"INTERFACE_DEF, CLASS_DEF, ENUM_DEF, ANNOTATION_DEF, RECORD_DEF, METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, RECORD_DEF, COMPACT_CTOR_DEF\"/>\n        </module>\n\n        <!-- what insane person willingly uses this rule? I'll never know. -->\n        <!--    &lt;!&ndash; https://gitlab.com/stellardrift/stylecheck/-/blob/dev/src/main/java/ca/stellardrift/stylecheck/StatementNoWhitespaceAfter.java &ndash;&gt;-->\n        <!--    <module name=\"StatementNoWhitespaceAfter\">-->\n        <!--      <property name=\"tokens\" value=\"LITERAL_CATCH, LITERAL_FOR, LITERAL_IF, LITERAL_TRY, LITERAL_WHILE\"/>-->\n        <!--    </module>-->\n    </module>\n</module>\n"
  },
  {
    "path": ".checkstyle/suppressions.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<!--  MIT License-->\n\n<!--  Copyright (c) 2017-2021 KyoriPowered-->\n\n<!--  Permission is hereby granted, free of charge, to any person obtaining a copy-->\n<!--  of this software and associated documentation files (the \"Software\"), to deal-->\n<!--  in the Software without restriction, including without limitation the rights-->\n<!--  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell-->\n<!--  copies of the Software, and to permit persons to whom the Software is-->\n<!--  furnished to do so, subject to the following conditions:-->\n\n<!--  The above copyright notice and this permission notice shall be included in all-->\n<!--  copies or substantial portions of the Software.-->\n\n<!--  THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR-->\n<!--  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,-->\n<!--  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE-->\n<!--  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER-->\n<!--  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,-->\n<!--  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE-->\n<!--  SOFTWARE.-->\n\n<!DOCTYPE suppressions PUBLIC\n    \"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN\"\n    \"http://checkstyle.org/dtds/suppressions_1_2.dtd\">\n<suppressions>\n    <!-- add any necessary suppressions here -->\n    <suppress files=\"src[\\\\/]main[\\\\/]java[\\\\/]net[\\\\/]draycia[\\\\/]carbon[\\\\/](paper|sponge|common|velocity|fabric)[\\\\/].*\"\n              checks=\"FilteringWriteTag\"/>\n    <suppress files=\"src[\\\\/]main[\\\\/]java[\\\\/]net[\\\\/]draycia[\\\\/]carbon[\\\\/](paper|sponge|common|velocity|fabric)[\\\\/].*\"\n              checks=\"MissingJavadocMethod\"/>\n    <suppress files=\"src[\\\\/]main[\\\\/]java[\\\\/]net[\\\\/]draycia[\\\\/]carbon[\\\\/](paper|sponge|common|velocity|fabric)[\\\\/].*\"\n              checks=\"MissingJavadocPackage\"/>\n    <suppress files=\"src[\\\\/]main[\\\\/]java[\\\\/]net[\\\\/]draycia[\\\\/]carbon[\\\\/](paper|sponge|common|velocity|fabric)[\\\\/].*\"\n              checks=\"MissingJavadocType\"/>\n    <suppress files=\"src[\\\\/]main[\\\\/]java[\\\\/]com[\\\\/]google[\\\\/]inject[\\\\/]assistedinject[\\\\/].*\"\n              checks=\"[a-zA-Z0-9]*\"/>\n</suppressions>\n"
  },
  {
    "path": ".editorconfig",
    "content": "# MIT License\n#\n# Copyright (c) 2017-2020 KyoriPowered\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to deal\n# in the Software without restriction, including without limitation the rights\n# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n# copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in all\n# copies or substantial portions of the Software.\n#\n# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n# SOFTWARE.\n\nroot = true\n\n[*]\ncharset = utf-8\nindent_size = 4\nindent_style = space\ninsert_final_newline = true\nmax_line_length = off\n\n[*.java]\nij_java_imports_layout = *, |, $*\nij_java_class_count_to_use_import_on_demand = 999\nij_java_names_count_to_use_import_on_demand = 999\n\n[{*.kt,*.kts,*.yml}]\nindent_size = 2\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [Draycia, jpenilla]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: \"\\U0001F41E Bug report\"\nabout: Describe a bug in Carbon\ntitle: \"[Bug]\"\nlabels: unconfirmed bug\nassignees: ''\n\n---\n\n<!-- Before continuing, please ensure you are using the latest version of Carbon and the bug you are about to report still exists. -->\n\n## Bug Description:\n\n### What is not working as it should?\n\n### Steps to reproduce:\n\n<!-- Please describe in as much detail as possible the exact steps needed to reproduce. For example:\n1. Install Carbon, Vault, and LuckPerms\n2. Make a channel called 'uncool' which displays the prefix of a player, using the placeholder %luckperms_prefix%\n3. Add a prefix of '!!COOL!!' to a user, and have them send a message in channel 'uncool'\n4. The prefix colour is always purple, regardless of any settings.\n-->\n\n### System Details:\n\n<!-- Please describe as many system details as you can -->\n\n1. Server Type:          <!-- Bukkit, Sponge, etc. -->\n2. Server Software:    <!-- Paper-163, SpongeForge 2838, etc. -->\n3. MC Version:           <!-- 1.16.2, 1.16.3, etc -->\n4. Carbon Version:\n\n### Pastebins:\n\n<!-- If relevant, please include a pastebin of any error logs, startup logs, and Carbon configs. In full, please. -->\n\n### Any other relevant details:\n\n<!-- Anything else that may be pertinent -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 💬 ​ Ask a question\n    url: https://discord.gg/S8s75Yf\n    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!\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: \"⚡ Feature request\"\nabout: Suggest an idea for Carbon\ntitle: \"[Feature]\"\nlabels: proposed enhancement\nassignees: ''\n\n---\n\n### Proposed Feature Description:\n\n<!-- Please describe what feature you would like to see added -->\n\n### Proposed Feature Functionality:\n\n<!-- Please describe how the feature would work, what it would do, and if you have considered it, how it might be implemented -->\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\non:\n  push:\n    branches: [ \"**\" ]\n    tags: [ \"v**\" ]\n  pull_request:\n  release:\n    types: [ published ]\n\njobs:\n  build:\n    # Only run on PRs if the source branch is on someone else's repo\n    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.')) }}\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        java: [ 21 ]\n      fail-fast: true\n    steps:\n      - uses: actions/checkout@v6\n      - name: JDK ${{ matrix.java }}\n        uses: actions/setup-java@v5\n        with:\n          java-version: ${{ matrix.java }}\n          distribution: 'temurin'\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        # gradle build action can't handle project dir local caches\n      - uses: actions/cache@v5\n        name: Cache Loom Files\n        with:\n          path: |\n            .gradle/loom-cache\n          key: ${{ runner.os }}-gradle-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}\n          restore-keys: ${{ runner.os }}-gradle-\n      - name: Build\n        run: ./gradlew build --stacktrace\n      - name: Determine Snapshot Status\n        run: |\n          if [ \"$(./gradlew properties | awk '/^version:/ { print $2; }' | grep '\\-SNAPSHOT')\" ]; then\n            echo \"STATUS=snapshot\" >> $GITHUB_ENV\n          else\n            echo \"STATUS=release\" >> $GITHUB_ENV\n          fi\n      -   name: \"publish snapshot to sonatype snapshots\"\n          if: \"${{ env.STATUS != 'release' && github.event_name == 'push' && github.ref == 'refs/heads/trunk' }}\"\n          run: ./gradlew publishAllPublicationsToSonatypeSnapshots\n          env:\n            ORG_GRADLE_PROJECT_sonatypeUsername: \"${{ secrets.SONATYPE_USERNAME }}\"\n            ORG_GRADLE_PROJECT_sonatypePassword: \"${{ secrets.SONATYPE_PASSWORD }}\"\n      -   name: \"publish (pre-)release to maven central\"\n          if: \"${{ env.STATUS == 'release' && github.event_name == 'release' }}\"\n          run: ./gradlew publishReleaseCentralPortalBundle\n          env:\n            ORG_GRADLE_PROJECT_sonatypeUsername: \"${{ secrets.SONATYPE_USERNAME }}\"\n            ORG_GRADLE_PROJECT_sonatypePassword: \"${{ secrets.SONATYPE_PASSWORD }}\"\n            ORG_GRADLE_PROJECT_signingKey: \"${{ secrets.SIGNING_KEY }}\"\n            ORG_GRADLE_PROJECT_signingPassword: \"${{ secrets.SIGNING_PASSWORD }}\"\n      - name: Parse tag\n        if: \"${{ github.event_name == 'push' && contains(github.ref, '-beta.') }}\"\n        id: vars\n        run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}\n      - name: Create changelog and Pre-Release\n        if: \"${{ github.event_name == 'push' && contains(github.ref, '-beta.') }}\"\n        uses: MC-Machinations/auto-release-changelog@v1.1.3\n        with:\n          token: ${{ secrets.RELEASE_TOKEN }}\n          title: CarbonChat ${{ steps.vars.outputs.tag }}\n          pre-release: true\n          files: |\n            build/libs/carbonchat-paper-*.jar\n            build/libs/carbonchat-velocity-*.jar\n            build/libs/carbonchat-fabric-*.jar\n      - name: Publish (Pre-)Release to Modrinth\n        if: \"${{ env.STATUS == 'release' && github.event_name == 'release' }}\"\n        run: ./gradlew :carbonchat-paper:publishModrinth :carbonchat-velocity:publishModrinth :carbonchat-fabric:publishModrinth\n        env:\n          MODRINTH_TOKEN: \"${{ secrets.MODRINTH_TOKEN }}\"\n          RELEASE_NOTES: \"${{ github.event.release.body }}\"\n      - name: Publish (Pre-)Release to Hangar\n        if: \"${{ env.STATUS == 'release' && github.event_name == 'release' }}\"\n        run: ./gradlew publishAllPublicationsToHangar\n        env:\n          HANGAR_UPLOAD_KEY: \"${{ secrets.HANGAR_UPLOAD_KEY }}\"\n          RELEASE_NOTES: \"${{ github.event.release.body }}\"\n      - name: Upload Artifacts\n        uses: actions/upload-artifact@v7\n        with:\n          name: Jars\n          path: build/libs/*.jar\n  smoketest:\n    needs: build\n    runs-on: ubuntu-latest\n    services:\n      postgres:\n        image: postgres:13\n        env:\n          POSTGRES_USER: username\n          POSTGRES_PASSWORD: password\n          POSTGRES_DB: carbon\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n      mariadb:\n        image: mariadb:10.11\n        env:\n          MARIADB_USER: username\n          MARIADB_PASSWORD: password\n          MARIADB_ROOT_PASSWORD: rootpassword\n          MARIADB_DATABASE: carbon\n        ports:\n          - 3306:3306\n        options: >-\n          --health-cmd=\"mysqladmin ping\"\n          --health-interval=10s\n          --health-timeout=5s\n          --health-retries=3\n    strategy:\n      matrix:\n        java: [ 21 ]\n      fail-fast: true\n    steps:\n      - uses: actions/checkout@v6\n      - name: JDK ${{ matrix.java }}\n        uses: actions/setup-java@v5\n        with:\n          java-version: ${{ matrix.java }}\n          distribution: 'temurin'\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        # gradle build action can't handle project dir local caches\n      - uses: actions/cache@v5\n        name: Cache Loom Files\n        with:\n          path: |\n            .gradle/loom-cache\n          key: ${{ runner.os }}-gradle-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}\n          restore-keys: ${{ runner.os }}-gradle-\n      - uses: actions/cache@v5\n        name: Cache Smoke Test Files\n        with:\n          path: |\n            paper/build/tmp/smokeTest/cache\n            paper/build/tmp/smokeTest/libraries\n            paper/build/tmp/smokeTest/plugins/CarbonChat/libraries\n            paper/build/tmp/smokeTest/world\n            paper/build/tmp/smokeTest/world_nether\n            paper/build/tmp/smokeTest/world_the_end\n          key: ${{ runner.os }}-gradle-${{ hashFiles('**/libs.versions.*', '**/*.gradle*', '**/gradle-wrapper.properties') }}\n          restore-keys: ${{ runner.os }}-gradle-smoketest-\n      - name: Prime Build\n        run: ./gradlew build --stacktrace\n      - name: Smoke test (Paper, JSON)\n        run: ./gradlew :carbonchat-paper:runServer -PsmokeTest=true -PsmokeTestMode=json\n        timeout-minutes: 5\n      - name: Smoke test (Paper, H2)\n        run: ./gradlew :carbonchat-paper:runServer -PsmokeTest=true -PsmokeTestMode=h2\n        timeout-minutes: 5\n      - name: Smoke test (Paper, MariaDB)\n        run: ./gradlew :carbonchat-paper:runServer -PsmokeTest=true -PsmokeTestMode=mariadb\n        timeout-minutes: 5\n      - name: Smoke test (Paper, Postgres)\n        run: ./gradlew :carbonchat-paper:runServer -PsmokeTest=true -PsmokeTestMode=postgres\n        timeout-minutes: 5\n"
  },
  {
    "path": ".gitignore",
    "content": "# Compiled class file\n*.class\n\n# Kotlin temp files\n.kotlin/\n\n# Log file\n*.log\n\n# BlueJ files\n*.ctxt\n\n# Mobile Tools for Java (J2ME)\n.mtj.tmp/\n\n# Package Files #\n*.jar\n*.war\n*.ear\n*.zip\n*.tar.gz\n*.rar\n\n# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml\nhs_err_pid*\n\n*.iml\n*.ipr\n*.iws\n/out/\n/bin/\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n/.idea/\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n.idea/modules.xml\n.idea/*.iml\n.idea/modules\n.idea/misc.xml\n\n# Dolphin browser keeps recreating this\n.directory\n\n# Gradle\n.gradle\n**/build/\n!src/**/build/\n\n# Ignore Gradle GUI config\ngradle-app.setting\n\n# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)\n!gradle-wrapper.jar\n\n# Cache of project\n.gradletasknamecache\n\n# Mac filesystem dust\n.DS_Store/\n.DS_Store\n\n**/run/\n**/run2/\n\n**/run-plugins.yml\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "LICENSE_HEADER",
    "content": "CarbonChat\n\nCopyright (c) 2024 Josua Parks (Vicarious)\n                   Contributors\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\nThis program is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with this program.  If not, see <https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <img src=\".github/assets/Carbon_Banner.png\" alt=\"Carbon plugin banner.\" width=\"500\" height=\"auto\" /><br>\n<b>Carbon</b> is a modern chat Java Edition plugin built on channels, with just about every single setting and format configurable.\n</p>\n\n## Support\n\nSupport is given through [GitHub Issues](https://github.com/Hexaoxide/Carbon/issues)\nand [Discord](https://discord.gg/S8s75Yf).  \nPlease use the discord for help setting up the plugin, and use issues for bug reports.\n\n## Checkstyle\n\nCarbon uses (a fork of) checkstyle to ensure code style is consistent across the entire project.  \nFor checkstyle support in IDEA:\n\n1. Install the [checkstyle plugin](https://github.com/jshiell/checkstyle-idea).\n2. Compile https://gitlab.com/stellardrift/stylecheck\n3. `Settings` -> `Tools` -> `Checkstyle` `Third-Party Checks`, add the compiled stylecheck jar\n4. While still in the `Checkstyle` tab, go to `Configuration File`, add `.checkstyle/checkstyle.xml` and tick the check\n   box.\n"
  },
  {
    "path": "api/build.gradle.kts",
    "content": "plugins {\n  id(\"carbon.publishing-conventions\")\n  alias(libs.plugins.javadoc.links)\n}\n\ndescription = \"API for interfacing with the CarbonChat Minecraft mod/plugin\"\n\ndependencies {\n  // Doesn't add any dependencies, only version constraints\n  api(platform(libs.adventureBom))\n\n  // Provided by platform\n  compileOnlyApi(libs.adventureApi)\n  compileOnlyApi(libs.adventureTextSerializerPlain)\n  compileOnlyApi(libs.adventureTextSerializerLegacy)\n  compileOnlyApi(libs.adventureTextSerializerGson) {\n    exclude(\"com.google.code.gson\")\n  }\n  compileOnlyApi(libs.minimessage)\n\n  compileOnlyApi(libs.checkerQual)\n\n  // Provided by Minecraft\n  compileOnlyApi(libs.gson)\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/CarbonChat.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api;\n\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.event.CarbonEvent;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.users.UserManager;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * The {@link CarbonChat} interface is the gateway to interacting with the majority of the CarbonChat API.\n *\n * <p>Instances may be obtained through {@link CarbonChatProvider#carbonChat()} once Carbon is loaded.</p>\n *\n * <p>On most platforms, you should use the provided load order mechanism to ensure your addon loads after\n * Carbon.</p>\n *\n * <p>On Fabric, use the {@code carbonchat} entrypoint (type: {@code Consumer<CarbonChat>}) to have a callback\n * when Carbon is loaded.</p>\n *\n * @since 1.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface CarbonChat {\n\n    /**\n     * The {@link CarbonEventHandler event handler}, used for listening to\n     * and emitting {@link CarbonEvent events}.\n     *\n     * @return the event handler\n     * @since 2.0.0\n     */\n    CarbonEventHandler eventHandler();\n\n    /**\n     * The server that carbon is running on.\n     *\n     * @return the server\n     * @since 2.0.0\n     */\n    CarbonServer server();\n\n    /**\n     * The user manager.\n     *\n     * @return the user manager\n     * @since 3.0.0\n     */\n    UserManager<?> userManager();\n\n    /**\n     * The registry that channels are registered to.\n     *\n     * @return the channel registry\n     * @since 2.0.0\n     */\n    ChannelRegistry channelRegistry();\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/CarbonChatProvider.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api;\n\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.ApiStatus;\n\n/**\n * Static accessor for the {@link CarbonChat} instance.\n *\n * @since 1.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic final class CarbonChatProvider {\n\n    private static @Nullable CarbonChat instance;\n\n    private CarbonChatProvider() {\n\n    }\n\n    /**\n     * Registers the {@link CarbonChat} implementation.\n     *\n     * @param carbonChat the carbon implementation\n     * @since 1.0.0\n     */\n    @ApiStatus.Internal\n    public static void register(final CarbonChat carbonChat) {\n        CarbonChatProvider.instance = carbonChat;\n    }\n\n    /**\n     * Gets the currently registered {@link CarbonChat} implementation.\n     *\n     * @return the registered carbon implementation\n     * @since 1.0.0\n     */\n    public static CarbonChat carbonChat() {\n        if (CarbonChatProvider.instance == null) {\n            throw new IllegalStateException(\"CarbonChat not initialized!\");\n        }\n\n        return CarbonChatProvider.instance;\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/CarbonServer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api;\n\nimport java.util.List;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.kyori.adventure.audience.Audience;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * The server that carbon is running on.\n *\n * @since 2.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface CarbonServer extends Audience {\n\n    /**\n     * The server's console.\n     *\n     * @return the server's console\n     * @since 2.0.0\n     */\n    Audience console();\n\n    /**\n     * The players that are online on the server.\n     *\n     * @return the online players\n     * @since 2.0.0\n     */\n    List<? extends CarbonPlayer> players();\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/channels/ChannelPermissionResult.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.channels;\n\nimport java.util.function.Supplier;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Represents the result of a channel permission check.\n *\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface ChannelPermissionResult {\n\n    /**\n     * Check whether the action checked was permitted.\n     *\n     * @return permitted\n     * @since 3.0.0\n     */\n    boolean permitted();\n\n    /**\n     * Reason for permission being denied. When the action\n     * was permitted, this should be equal to {@link Component#empty()}.\n     *\n     * @return deny reason\n     * @since 3.0.0\n     */\n    Component reason();\n\n    /**\n     * Returns a result denoting that the player is permitted for the action.\n     *\n     * @return that the action is allowed\n     * @since 3.0.0\n     */\n    static ChannelPermissionResult allowed() {\n        return ChannelPermissionResultImpl.ALLOWED;\n    }\n\n    /**\n     * Returns a result denoting that the action is denied for the player.\n     *\n     * @param reason the reason the action was denied\n     * @return that the action is denied\n     * @since 3.0.0\n     */\n    static ChannelPermissionResult denied(final Component reason) {\n        return new ChannelPermissionResultImpl(false, () -> reason);\n    }\n\n    /**\n     * Returns a result denoting that the action is denied for the player.\n     *\n     * @param reason the reason the action was denied\n     * @return that the action is denied\n     * @since 3.0.0\n     */\n    static ChannelPermissionResult denied(final Supplier<Component> reason) {\n        return new ChannelPermissionResultImpl(false, reason);\n    }\n\n    /**\n     * Create a {@link ChannelPermissionResult} based on {@code allowed},\n     * computing {@code denyReason} when needed.\n     *\n     * @param allowed    whether the result is allowed\n     * @param denyReason deny reason supplier\n     * @return permission result\n     * @since 3.0.0\n     */\n    static ChannelPermissionResult channelPermissionResult(\n        final boolean allowed,\n        final Supplier<Component> denyReason\n    ) {\n        if (allowed) {\n            return allowed();\n        }\n        return denied(denyReason);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/channels/ChannelPermissionResultImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.channels;\n\nimport java.util.function.Supplier;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\nrecord ChannelPermissionResultImpl(\n    boolean permitted,\n    Supplier<Component> reasonSupplier\n) implements ChannelPermissionResult {\n\n    static final ChannelPermissionResult ALLOWED =\n        new ChannelPermissionResultImpl(true, Component::empty);\n\n    @Override\n    public Component reason() {\n        return this.reasonSupplier.get();\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/channels/ChannelPermissions.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.channels;\n\nimport java.util.function.Function;\nimport net.draycia.carbon.api.users.CarbonPlayer;\n\n/**\n * Permissions handling for a channel.\n *\n * @since 3.0.0\n */\npublic interface ChannelPermissions {\n\n    /**\n     * Checks if the player may join this channel.\n     *\n     * @param carbonPlayer the player attempting to join\n     * @return if the player may join\n     * @since 3.0.0\n     */\n    ChannelPermissionResult joinPermitted(CarbonPlayer carbonPlayer);\n\n    /**\n     * Checks if the player may send messages in this channel.\n     *\n     * @param carbonPlayer the player attempting to speak\n     * @return if the player may speak\n     * @since 3.0.0\n     */\n    ChannelPermissionResult speechPermitted(CarbonPlayer carbonPlayer);\n\n    /**\n     * Checks if the player may receive messages from this channel.\n     *\n     * @param player the player that's receiving messages\n     * @return if the player may receive messages\n     * @since 3.0.0\n     */\n    ChannelPermissionResult hearingPermitted(CarbonPlayer player);\n\n    /**\n     * Returns whether the result of {@link #joinPermitted(CarbonPlayer)} is dynamic.\n     *\n     * <p>An example of a dynamic permissions is the built-in party channel that only allows players in a party to join.</p>\n     *\n     * <p>An example of static permissions is the built-in config channels that simply check permission strings. The fact that a player's\n     * permissions may change during gameplay does not make the permission dynamic, as the server will resend commands on permission changes.</p>\n     *\n     * <p>If the result is static, then we can avoid sending commands to the player that they will just get denied use\n     * of on execute. If it's dynamic, we must send the command regardless in case they gain permission later.</p>\n     *\n     * @return whether the permissions are dynamic\n     * @since 3.0.0\n     */\n    boolean dynamic();\n\n    /**\n     * Creates a new {@link ChannelPermissions} that performs the same check for\n     * {@link #joinPermitted(CarbonPlayer)}, {@link #hearingPermitted(CarbonPlayer)},\n     * and {@link #speechPermitted(CarbonPlayer)}.\n     *\n     * @param check permission check\n     * @return new permissions object\n     * @since 3.0.0\n     */\n    static ChannelPermissions uniformDynamic(final Function<CarbonPlayer, ChannelPermissionResult> check) {\n        return new ChannelPermissions() {\n            @Override\n            public ChannelPermissionResult joinPermitted(final CarbonPlayer player) {\n                return check.apply(player);\n            }\n\n            @Override\n            public ChannelPermissionResult speechPermitted(final CarbonPlayer player) {\n                return check.apply(player);\n            }\n\n            @Override\n            public ChannelPermissionResult hearingPermitted(final CarbonPlayer player) {\n                return check.apply(player);\n            }\n\n            @Override\n            public boolean dynamic() {\n                return true;\n            }\n        };\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/channels/ChannelRegistry.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.channels;\n\nimport java.util.NoSuchElementException;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\n/**\n * Registry for {@link ChatChannel chat channels}.\n *\n * @since 2.0.0\n */\npublic interface ChannelRegistry {\n\n    /**\n     * Registers the chat channel with its key.\n     *\n     * <p>Registrations will persist when reloading Carbon's configuration.</p>\n     *\n     * @param channel the channel to register\n     * @since 3.0.0\n     */\n    void register(ChatChannel channel);\n\n    /**\n     * Retrieve a channel by its key. If there is no matching channel,\n     * returns {@code null}.\n     *\n     * @param key the channel's key\n     * @return the channel\n     * @since 3.0.0\n     */\n    @Nullable ChatChannel channel(Key key);\n\n    /**\n     * Gets the key for the default channel.\n     *\n     * @return the default key\n     * @since 3.0.0\n     */\n    @NonNull Key defaultKey();\n\n    /**\n     * Gets the default channel.\n     *\n     * @return the default value\n     * @since 3.0.0\n     */\n    @NonNull ChatChannel defaultChannel();\n\n    /**\n     * Gets the list of registered channel keys.\n     *\n     * @return the registered channel keys\n     * @since 3.0.0\n     */\n    @NonNull Set<Key> keys();\n\n    /**\n     * Retrieve a channel by its key. If there is no matching channel,\n     * returns {@link #defaultChannel() the default channel}.\n     *\n     * @param key the channel key\n     * @return the channel, or the default one\n     * @since 3.0.0\n     */\n    ChatChannel channelOrDefault(Key key);\n\n    /**\n     * Retrieve a channel by its key. If there is no matching channel,\n     * throws {@link NoSuchElementException}.\n     *\n     * @param key channel key\n     * @return channel\n     * @throws NoSuchElementException when no matching channel is found\n     * @since 3.0.0\n     */\n    ChatChannel channelOrThrow(Key key);\n\n    /**\n     * The provided action will be executed immediately for all currently registered\n     * channels.\n     *\n     * <p>When new channels are registered, the action will be invoked again for each new channel.</p>\n     *\n     * @param action action\n     * @since 3.0.0\n     */\n    void allKeys(Consumer<Key> action);\n\n    /**\n     * Create a {@link ChannelPermissions channel permissions handler} for the provided base permission string.\n     *\n     * <p>The handler will check the base permission for joins, {@literal <base>.see} for receiving/seeing messages,\n     * and {@literal <base>.speak} for speaking/sending messages.</p>\n     *\n     * <p>The built-in deny messages are used, same as user-configured config channels.</p>\n     *\n     * @param permission permission string\n     * @return permission handler\n     * @since 3.0.0\n     */\n    ChannelPermissions permission(String permission);\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/channels/ChatChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.channels;\n\nimport java.util.List;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.ChatComponentRenderer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Keyed;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * ChatChannel interface, supplies a formatter and filters recipients.<br>\n * Extends Keyed for identification purposes.\n *\n * @since 2.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface ChatChannel extends Keyed, ChatComponentRenderer {\n\n    /**\n     * Returns the permissions handler for the channel.\n     *\n     * @return the permissions handler\n     * @since 3.0.0\n     */\n    ChannelPermissions permissions();\n\n    /**\n     * Returns a list of all recipients that will receive messages from the sender.\n     *\n     * @param sender the sender of messages\n     * @return the recipients\n     * @since 2.0.0\n     */\n    List<Audience> recipients(CarbonPlayer sender);\n\n    /**\n     * Messages will be sent in this channel if they start with this prefix.\n     *\n     * @return the message prefix that sends messages in this channel\n     * @since 2.0.0\n     */\n    @Nullable String quickPrefix();\n\n    /**\n     * If commands should be registered for this channel.\n     *\n     * @return if commands should be registered for this channel.\n     * @since 2.0.0\n     */\n    boolean shouldRegisterCommands();\n\n    /**\n     * The text that can be used to refer to this channel in commands.\n     *\n     * @return this channel's name when used in commands\n     * @since 2.0.0\n     */\n    String commandName();\n\n    /**\n     * Alternative command names for this channel.\n     *\n     * @return alternative command names\n     * @since 2.0.0\n     */\n    List<String> commandAliases();\n\n    /**\n     * The distance from the sender players must be to receive chat messages.<br>\n     * Return of '0' means players must be in the same world/server.<br>\n     * Return of '-1' means there is no radius.\n     *\n     * @return the channel radius\n     * @since 3.0.0\n     */\n    double radius();\n\n    /**\n     * If the empty receipt message should be sent to the sender.\n     *\n     * @return Returns true if the channel should display a message when a player is out of range.\n     * @since 3.0.0\n     */\n    boolean emptyRadiusRecipientsMessage();\n\n    /**\n     * The time in milliseconds between player messages.\n     * -1 and 0 disable the cooldown for this channel.\n     *\n     * @return The message cooldown in millis.\n     * @since 3.0.0\n     */\n    long cooldown();\n\n    /**\n     * The epoch time (millis) when the player's cooldown expires.\n     *\n     * @param player The player\n     * @return The epoch time (millis) when the player's cooldown expires.\n     * @since 3.0.0\n     */\n    long playerCooldown(CarbonPlayer player);\n\n    /**\n     * Starts the cooldown timer for the specified player. Duration will be the channel cooldown.\n     * Returns the player's old cooldown time, if they have one.\n     *\n     * @param player The player\n     * @return The player's old cooldown, or 0 if they don't have one.\n     * @since 3.0.0\n     */\n    long startCooldown(CarbonPlayer player);\n\n    /**\n     * Whether messages from this channel should be broadcast and sent to other servers.\n     *\n     * @return if this channel's messages should be sent cross-server\n     * @since 3.0.0\n     */\n    boolean shouldCrossServer();\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/Cancellable.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event;\n\n/**\n * Marks an event as cancellable.\n *\n * @since 3.0.0\n */\npublic interface Cancellable {\n\n    /**\n     * Gets if the event is cancelled.\n     *\n     * @return if the event is cancelled\n     * @since 3.0.0\n     */\n    boolean cancelled();\n\n    /**\n     * Sets the cancelled state.\n     *\n     * @param cancelled new cancelled state\n     * @since 3.0.0\n     */\n    void cancelled(boolean cancelled);\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/CarbonEvent.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event;\n\n/**\n * Marker interface for events.\n *\n * @since 1.0.0\n */\npublic interface CarbonEvent {\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/CarbonEventHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event;\n\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * The {@link CarbonEventHandler} is responsible for managing {@link CarbonEventSubscription event subscriptions}\n * and emitting {@link CarbonEvent events}.\n *\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface CarbonEventHandler {\n\n    /**\n     * Registers a subscriber for the given event class.\n     *\n     * @param eventClass the class to listen for\n     * @param subscriber the subscriber that's executed when the event is emitted\n     * @param <T>        the class to listen for\n     * @return           the subscription, so that it may be unregistered\n     * @since 2.0.0\n     */\n    <T extends CarbonEvent> CarbonEventSubscription<T> subscribe(\n        Class<T> eventClass,\n        CarbonEventSubscriber<T> subscriber\n    );\n\n    /**\n     * Registers a subscriber for the given event class.<br>\n     * Includes extra values to control when the consumer is executed.\n     *\n     * @param eventClass       the class to listen for\n     * @param order            the order of the consumer\n     * @param acceptsCancelled if the consumer should be executed if the event is cancelled early\n     * @param subscriber       the consumer that's executed when the event is emitted\n     * @param <T>              the class to listen for\n     * @return                 the subscription, so that it may be unregistered\n     * @since 2.0.0\n     */\n    <T extends CarbonEvent> CarbonEventSubscription<T> subscribe(\n        Class<T> eventClass,\n        int order,\n        boolean acceptsCancelled,\n        CarbonEventSubscriber<T> subscriber\n    );\n\n    /**\n     * Emits the supplied event.\n     *\n     * <p>Events are modified in place, meaning you must keep a reference to the event\n     * yourself if you wish to inspect it's state after this call.</p>\n     *\n     * @param event the event to be emitted\n     * @param <T> the class to emit\n     * @since 2.0.0\n     */\n    <T extends CarbonEvent> void emit(T event);\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/CarbonEventSubscriber.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event;\n\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * An EventSubscriber.\n *\n * @param <T> CarbonEvent implementations\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface CarbonEventSubscriber<T extends CarbonEvent> {\n\n    /**\n     * Invokes this event consumer.\n     *\n     * @param event the event\n     * @throws Throwable if an exception is thrown\n     * @since 1.0.0\n     */\n    void on(T event) throws Throwable;\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/CarbonEventSubscription.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event;\n\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * A subscription to a specific event type.\n *\n * @param <T> event type\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface CarbonEventSubscription<T extends CarbonEvent> {\n\n    /**\n     * Gets the event type.\n     *\n     * @return the event type\n     * @since 3.0.0\n     */\n    Class<T> event();\n\n    /**\n     * Gets the {@link CarbonEventSubscriber subscriber}.\n     *\n     * @return the subscriber\n     * @since 3.0.0\n     */\n    CarbonEventSubscriber<T> subscriber();\n\n    /**\n     * Disposes this subscription.\n     *\n     * <p>The subscriber held by this subscription will no longer receive events.</p>\n     *\n     * @since 3.0.0\n     */\n    void dispose();\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/events/CarbonChannelRegisterEvent.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event.events;\n\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.event.CarbonEvent;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * {@link CarbonEvent} that's called after new channels are registered.\n *\n * <p>Note that some invocations of this event may be too early for\n * API consumers to be notified. {@link ChannelRegistry#allKeys(Consumer)}\n * is provided as a helper for when knowledge of all registered channels\n * is needed.</p>\n *\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface CarbonChannelRegisterEvent extends CarbonEvent {\n\n    /**\n     * Gets the channel registry.\n     *\n     * @return the channel registry\n     * @since 3.0.0\n     */\n    ChannelRegistry channelRegistry();\n\n    /**\n     * Gets the key(s) that were registered to trigger this event.\n     *\n     * @return key(s) registered\n     * @since 3.0.0\n     */\n    Set<Key> registered();\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/events/CarbonChatEvent.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event.events;\n\nimport java.util.List;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.event.Cancellable;\nimport net.draycia.carbon.api.event.CarbonEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.KeyedRenderer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.chat.SignedMessage;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Event that's called when chat components are rendered for online players.\n *\n * @since 2.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface CarbonChatEvent extends CarbonEvent, Cancellable {\n\n    /**\n     * Get the renderers used to construct components for each of the recipients. The returned list\n     * is mutable.\n     *\n     * @return renderers\n     * @since 2.0.0\n     */\n    List<KeyedRenderer> renderers();\n\n    /**\n     * If the message is being previewed by the player.\n     *\n     * @return if the message is being previewed\n     * @since 3.0.0\n     */\n    @MonotonicNonNull SignedMessage signedMessage();\n\n    /**\n     * Get the sender of the message.\n     *\n     * @return The message sender.\n     * @since 2.0.0\n     */\n    CarbonPlayer sender();\n\n    /**\n     * Get the original message that was sent.\n     *\n     * @return The original message.\n     * @since 2.0.0\n     */\n    Component originalMessage();\n\n    /**\n     * Get the chat message that will be sent.\n     *\n     * @return The chat message.\n     * @since 2.0.0\n     */\n    Component message();\n\n    /**\n     * Set the chat message that will be sent.\n     *\n     * @param message new message\n     * @since 2.0.0\n     */\n    void message(final Component message);\n\n    /**\n     * The chat channel the message was sent in.\n     *\n     * @return the chat channel\n     * @since 2.0.0\n     */\n    @MonotonicNonNull ChatChannel chatChannel();\n\n    /**\n     * The recipients of the message.\n     * List is mutable and elements may be added/removed.\n     *\n     * @return the recipients of the message.\n     *     entries may be players, console, or other audience implementations\n     * @since 2.0.0\n     */\n    List<? extends Audience> recipients();\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/events/CarbonPrivateChatEvent.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event.events;\n\nimport net.draycia.carbon.api.event.Cancellable;\nimport net.draycia.carbon.api.event.CarbonEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Called whenever a player privately messages another player.\n *\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface CarbonPrivateChatEvent extends CarbonEvent, Cancellable {\n\n    /**\n     * Sets the message that will be sent.\n     *\n     * @param message the new message\n     * @throws NullPointerException if message is null\n     * @since 3.0.0\n     */\n    void message(Component message);\n\n    /**\n     * The message that will be sent.\n     *\n     * @return the message\n     * @since 3.0.0\n     */\n    Component message();\n\n    /**\n     * The message sender.\n     *\n     * @return the sender of the message\n     * @since 3.0.0\n     */\n    CarbonPlayer sender();\n\n    /**\n     * The message recipient.\n     *\n     * @return the recipient of the message\n     * @since 3.0.0\n     */\n    CarbonPlayer recipient();\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/events/ChannelSwitchEvent.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event.events;\n\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.event.CarbonEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Called when a player switches channels.\n *\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface ChannelSwitchEvent extends CarbonEvent {\n\n    /**\n     * The player switching channels.\n     *\n     * @since 3.0.0\n     */\n    CarbonPlayer player();\n\n    /**\n     * The channel the player is switching to.\n     *\n     * @since 3.0.0\n     */\n    ChatChannel channel();\n\n    /**\n     * Sets the player's new channel.\n     *\n     * @param chatChannel the new channel\n     * @since 3.0.0\n     */\n    void channel(final ChatChannel chatChannel);\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/events/PartyJoinEvent.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event.events;\n\nimport java.util.UUID;\nimport net.draycia.carbon.api.event.CarbonEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Called when a player is added to a {@link Party}.\n *\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface PartyJoinEvent extends CarbonEvent {\n\n    /**\n     * ID of the player joining a party.\n     *\n     * <p>The player's {@link CarbonPlayer#party()} field is not guaranteed to be updated immediately,\n     * especially if the change needs to propagate cross-server.</p>\n     *\n     * @return player id\n     * @since 3.0.0\n     */\n    UUID playerId();\n\n    /**\n     * The party being joined.\n     *\n     * <p>{@link Party#members()} will reflect the new member.</p>\n     *\n     * @return party\n     * @since 3.0.0\n     */\n    Party party();\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/event/events/PartyLeaveEvent.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.event.events;\n\nimport java.util.UUID;\nimport net.draycia.carbon.api.event.CarbonEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Called when a player is removed from a {@link Party}.\n *\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface PartyLeaveEvent extends CarbonEvent {\n\n    /**\n     * ID of the player leaving a party.\n     *\n     * <p>The player's {@link CarbonPlayer#party()} field is not guaranteed to be updated immediately,\n     * especially if the change needs to propagate cross-server.</p>\n     *\n     * @return player id\n     * @since 3.0.0\n     */\n    UUID playerId();\n\n    /**\n     * The party being left.\n     *\n     * <p>{@link Party#members()} will reflect the removed member.</p>\n     *\n     * @return party\n     * @since 3.0.0\n     */\n    Party party();\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/users/CarbonPlayer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.users;\n\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.util.InventorySlot;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.identity.Identified;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Generic abstraction for players.\n *\n * @since 2.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface CarbonPlayer extends Audience, Identified {\n\n    /**\n     * Returns the distance from the other {@link CarbonPlayer}, or -1 if the players are not in the same world.\n     *\n     * @param other the other player\n     * @return the distance from the other player, or -1\n     * @since 3.0.0\n     */\n    double distanceSquaredFrom(CarbonPlayer other);\n\n    /**\n     * Returns if both players are in the same world or server.\n     *\n     * @param other the other player\n     * @return if both players are in the same world/server\n     * @since 3.0.0\n     */\n    boolean sameWorldAs(CarbonPlayer other);\n\n    /**\n     * Gets the player's username.\n     *\n     * @return the player's username\n     * @since 2.0.0\n     */\n    String username();\n\n    /**\n     * Returns the player's display name.\n     *\n     * <p>The display name is the effective or displayed name of a player.\n     * When the player has a nickname set, either through Carbon or the platform,\n     * it will be reflected here. Else, a plain text component representing\n     * the player's name may be returned.</p>\n     *\n     * @return the player's display name\n     * @since 3.0.0\n     */\n    Component displayName();\n\n    /**\n     * Checks if the player has a nickname set.\n     *\n     * <p>Will always return {@code false} when Carbon's nickname management is disabled.</p>\n     *\n     * @return if the player has a nickname set\n     * @see #nickname()\n     * @see #nickname(Component)\n     * @since 3.0.0\n     */\n    boolean hasNickname();\n\n    /**\n     * Gets the player's nickname, shown in places like chat and tab menu.\n     *\n     * <p>Will always return {@code null} when Carbon's nickname management is disabled.</p>\n     *\n     * @return the player's nickname\n     * @see #hasNickname()\n     * @see #nickname(Component)\n     * @see #displayName()\n     * @since 3.0.0\n     */\n    @Nullable Component nickname();\n\n    /**\n     * Sets the player's nickname.\n     *\n     * <p>Setting {@code null} will remove any current nickname.</p>\n     *\n     * <p>Won't have any visible effect when Carbon's nickname management is disabled.</p>\n     *\n     * @param nickname the new nickname\n     * @see #hasNickname()\n     * @see #nickname()\n     * @see #displayName()\n     * @since 3.0.0\n     */\n    void nickname(@Nullable Component nickname);\n\n    /**\n     * The player's UUID, often used for identification purposes.\n     *\n     * @return the player's UUID\n     * @since 2.0.0\n     */\n    UUID uuid();\n\n    /**\n     * Creates the chat component for the item in the {@link InventorySlot}, or null if the slot is empty.\n     *\n     * @param slot the inventory slot containing the item\n     * @return the chat component for the item in the slot, or null if the slot is empty\n     * @since 2.0.0\n     */\n    @Nullable Component createItemHoverComponent(InventorySlot slot);\n\n    /**\n     * The player's locale.\n     *\n     * @return the player's locale, or null if offline\n     * @since 2.0.0\n     */\n    @Nullable Locale locale();\n\n    /**\n     * The player's selected channel, or null if one isn't set.\n     *\n     * @return the player's selected channel\n     * @since 2.0.0\n     */\n    @Nullable ChatChannel selectedChannel();\n\n    /**\n     * Sets the player's selected channel.\n     *\n     * @param chatChannel the channel\n     * @since 2.0.0\n     */\n    void selectedChannel(@Nullable ChatChannel chatChannel);\n\n    /**\n     * Determines which channel the message should go to, and removes any channel prefixes from the message.\n     *\n     * @param message the message to be sent\n     * @return the channel and message\n     * @since 3.0.0\n     */\n    ChannelMessage channelForMessage(Component message);\n\n    /**\n     * A message and which channel it should be sent in.\n     *\n     * @param message The channel message without any prefixes\n     * @param channel The channel the message should be sent to\n     * @since 3.0.0\n     */\n    record ChannelMessage(Component message, ChatChannel channel) {}\n\n    /**\n     * Checks if the player has the specified permission.\n     *\n     * @param permission the permission to check\n     * @return if the player has the permission\n     * @since 2.0.0\n     */\n    boolean hasPermission(String permission);\n\n    /**\n     * Returns the player's primary group.\n     *\n     * @return the player's primary group\n     * @since 2.0.0\n     */\n    String primaryGroup();\n\n    /**\n     * Returns the complete list of groups the player is in.\n     *\n     * @return the groups the player is in\n     * @since 2.0.0\n     */\n    List<String> groups();\n\n    /**\n     * Returns if the player is muted.\n     *\n     * @return if the player is muted\n     * @since 2.0.0\n     */\n    boolean muted();\n\n    /**\n     * The time the mute will expire, in epoch millis.\n     *\n     * @return the mute expiration time\n     * @since 3.0.0\n     */\n    long muteExpiration();\n\n    /**\n     * Mutes and unmutes the player.\n     *\n     * @param muted if the player is now muted\n     * @since 2.0.0\n     */\n    void muted(boolean muted);\n\n    /**\n     * Sets the epoch time the player's mute will expire.\n     *\n     * @param epochMillis the expiration time\n     * @since 3.0.0\n     */\n    void muteExpiration(long epochMillis);\n\n    /**\n     * Gets the ids of the players this player is currently ignoring.\n     *\n     * @return the players currently ignored\n     * @since 3.0.0\n     */\n    Set<UUID> ignoring();\n\n    /**\n     * Checks if the other player is being ignored by this player.\n     *\n     * @param player the potential source of a message\n     * @return if this player is ignoring the sender\n     * @since 2.0.5\n     */\n    boolean ignoring(UUID player);\n\n    /**\n     * Checks if the other player is being ignored by this player.\n     *\n     * @param player the potential source of a message\n     * @return if this player is ignoring the sender\n     * @since 2.0.0\n     */\n    boolean ignoring(CarbonPlayer player);\n\n    /**\n     * Adds the player to and removes the player from the ignore list.\n     *\n     * @param player      the player to be added/removed\n     * @param nowIgnoring if the player should be ignored\n     * @since 2.0.0\n     */\n    void ignoring(UUID player, boolean nowIgnoring);\n\n    /**\n     * Adds the player to and removes the player from the ignore list.\n     *\n     * @param player      the player to be added/removed\n     * @param nowIgnoring if the player should be ignored\n     * @since 2.0.0\n     */\n    void ignoring(CarbonPlayer player, boolean nowIgnoring);\n\n    /**\n     * Returns if the player is deafened and unable to read messages.\n     *\n     * @return if the player is deafened\n     * @since 2.0.0\n     */\n    boolean deafened();\n\n    /**\n     * Deafens and undeafens the player.\n     *\n     * @since 2.0.0\n     */\n    void deafened(boolean deafened);\n\n    /**\n     * Returns if the player is spying on messages and able to read muted/private messages.\n     *\n     * @return if the player is spying on messages\n     * @since 2.0.0\n     */\n    boolean spying();\n\n    /**\n     * Sets and unsets the player's ability to spy.\n     *\n     * @since 2.0.0\n     */\n    void spying(boolean spying);\n\n    /**\n     * Controls if the player should receive direct messages or if they should be hidden.\n     *\n     * @return if the player is ignoring direct messages\n     * @since 3.0.0\n     */\n    boolean ignoringDirectMessages();\n\n    /**\n     * Sets whether the player should receive direct messages or if they should be hidden.\n     *\n     * @param ignoring if the player is ignoring direct messages\n     * @since 3.0.0\n     */\n    void ignoringDirectMessages(boolean ignoring);\n\n    /**\n     * Sends the message as the player.\n     *\n     * @param message the message to be sent\n     * @since 2.0.0\n     */\n    void sendMessageAsPlayer(String message);\n\n    /**\n     * Returns whether the player is online.\n     *\n     * @return if the player is online.\n     * @since 2.0.0\n     */\n    boolean online();\n\n    /**\n     * The UUID of the player that replies will be sent to.\n     *\n     * @return the player's reply target\n     * @since 2.0.0\n     */\n    @Nullable UUID whisperReplyTarget();\n\n    /**\n     * Sets the whisper reply target for this player.\n     *\n     * @param uuid the uuid of the reply target\n     * @since 2.0.0\n     */\n    void whisperReplyTarget(@Nullable UUID uuid);\n\n    /**\n     * The last player this player has whispered.\n     *\n     * @return the player's last whisper target\n     * @since 2.0.0\n     */\n    @Nullable UUID lastWhisperTarget();\n\n    /**\n     * Sets the last player this player has whispered.\n     *\n     * @param uuid the uuid of the whisper target\n     * @since 2.0.0\n     */\n    void lastWhisperTarget(@Nullable UUID uuid);\n\n    /**\n     * If this player is vanished in another supported plugin.\n     * Other players will be unaware of this player.\n     * There is no way to set this state through Carbon, we do not store this information; but merely bridge it.\n     *\n     * @return If this player is vanished in another plugin.\n     * @since 2.0.0\n     */\n    boolean vanished();\n\n    /**\n     * Whether this player can see the other player.\n     *\n     * @param other the other, potentially vanished, player\n     * @return if this player is aware of the other player\n     * @since 2.0.0\n     */\n    boolean awareOf(CarbonPlayer other);\n\n    /**\n     * A list of all the channels the player has left\n     * using the leave command.\n     *\n     * <p>The returned collection is immutable, use\n     * {@link #joinChannel(ChatChannel)} and {@link #leaveChannel(ChatChannel)} to mutate.</p>\n     *\n     * @return a list of the channels.\n     * @since 3.0.0\n     */\n    List<Key> leftChannels();\n\n    /**\n     * Join a channel for this player if they have left it.\n     *\n     * @param channel the channel to join.\n     * @since 3.0.0\n     */\n    void joinChannel(ChatChannel channel);\n\n    /**\n     * Leave a channel for this player.\n     *\n     * @param channel the channel to leave.\n     * @since 3.0.0\n     */\n    void leaveChannel(ChatChannel channel);\n\n    /**\n     * Get this player's current {@link Party}.\n     *\n     * @return party future\n     * @since 3.0.0\n     */\n    CompletableFuture<@Nullable Party> party();\n\n    /**\n     * Whether the optional chat filters apply to messages send to this player or not.\n     *\n     * @return if this player's using the optional chat filters\n     * @since 3.0.0\n     */\n    boolean applyOptionalChatFilters();\n\n    /**\n     * Whether the optional chat filters apply to messages send to this player or not.\n     *\n     * @param applyOptionalChatFilters if this player's using the optional chat filters\n     * @since 3.0.0\n     */\n    void applyOptionalChatFilters(boolean applyOptionalChatFilters);\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/users/Party.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.users;\n\nimport java.util.Set;\nimport java.util.UUID;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Reference to a chat party.\n *\n * @see UserManager#createParty(Component)\n * @see UserManager#party(UUID)\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface Party {\n\n    /**\n     * Get the name of this party.\n     *\n     * @return party name\n     * @since 3.0.0\n     */\n    Component name();\n\n    /**\n     * Get the unique id of this party.\n     *\n     * @return party id\n     * @since 3.0.0\n     */\n    UUID id();\n\n    /**\n     * Get a snapshot of the current party members.\n     *\n     * @return party members\n     * @since 3.0.0\n     */\n    Set<UUID> members();\n\n    /**\n     * Add a user to this party. They will automatically be removed from their previous party if necessary.\n     *\n     * @param id user id\n     * @since 3.0.0\n     */\n    void addMember(UUID id);\n\n    /**\n     * Remove a user from this party.\n     *\n     * @param id user id\n     * @since 3.0.0\n     */\n    void removeMember(UUID id);\n\n    /**\n     * Disband this party. Will remove all members and delete persistent data.\n     *\n     * @since 3.0.0\n     */\n    void disband();\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/users/UserManager.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.users;\n\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Manager used to load/obtain and save {@link CarbonPlayer CarbonPlayers}.\n *\n * @since 2.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface UserManager<C extends CarbonPlayer> {\n\n    /**\n     * Gets the {@link CarbonPlayer} for the provided player {@link UUID}, whether they are online or not.\n     *\n     * <p>Note that the returned user object/future is <i>not</i> guaranteed to be the same for subsequent calls.</p>\n     *\n     * <p>Because of this, the return value should <i>not</i> be cached, it should be queried each time it is needed. The implementation handles caching as is appropriate.</p>\n     *\n     * @param uuid the player's id\n     * @return the player\n     * @since 3.0.0\n     */\n    CompletableFuture<C> user(UUID uuid);\n\n    /**\n     * Create a new {@link Party} with the specified name.\n     *\n     * <p>Parties with no users will not be saved. Use {@link Party#disband()} to discard.</p>\n     *\n     * <p>The returned reference will expire after one minute, store {@link Party#id()} rather than the instance and use {@link #party(UUID)} to retrieve.</p>\n     *\n     * @param name party name\n     * @return new party\n     * @since 3.0.0\n     */\n    Party createParty(Component name);\n\n    /**\n     * Look up an existing party by its id.\n     *\n     * <p>As parties that have never had a user are not saved, they are not retrievable here.</p>\n     *\n     * <p>The returned reference will expire after one minute, do not cache it. The implementation handles caching as is appropriate.</p>\n     *\n     * @param id party id\n     * @return existing party\n     * @see #createParty(Component)\n     * @since 3.0.0\n     */\n    CompletableFuture<@Nullable Party> party(UUID id);\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/util/ChatComponentRenderer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.util;\n\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Renderer used to construct chat components on a per-player basis.\n *\n * @since 2.0.0\n */\n@FunctionalInterface\n@DefaultQualifier(NonNull.class)\npublic interface ChatComponentRenderer {\n\n    /**\n     * Renders a Component for the specified recipient.\n     *\n     * @param sender          the player that sent the message\n     * @param recipient       a recipient of the message.\n     *                        may be a player, console, or other Audience implementations\n     * @param message         the message being sent\n     * @param originalMessage the original message that was sent\n     * @return the component to be shown to the recipient,\n     *     or empty if the recipient should not receive the message\n     * @since 2.0.0\n     */\n    Component render(CarbonPlayer sender,\n                     Audience recipient,\n                     Component message,\n                     Component originalMessage);\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/util/InventorySlot.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.util;\n\nimport java.util.List;\n\n/**\n * A slot in a player's inventory.\n *\n * @since 2.0.0\n */\npublic final class InventorySlot {\n\n    /**\n     * An {@link InventorySlot} instance, usable in chat with the given placeholders.\n     *\n     * @param placeholders the placeholders that can be used in chat\n     * @return the instance\n     * @since 2.0.0\n     */\n    public static InventorySlot of(final String... placeholders) {\n        return new InventorySlot(placeholders);\n    }\n\n    private final List<String> placeholders;\n\n    private InventorySlot(final String... placeholders) {\n        this.placeholders = List.of(placeholders);\n    }\n\n    /**\n     * Returns this slot's placeholders, which can be used in chat to show the item in said slot.\n     *\n     * @return this slot's placeholders\n     * @since 2.0.0\n     */\n    public List<String> placeholders() {\n        return this.placeholders;\n    }\n\n    public static final InventorySlot HELMET = InventorySlot.of(\"helm\", \"helmet\", \"hat\", \"head\");\n    public static final InventorySlot CHEST = InventorySlot.of(\"chest\", \"chestplate\");\n    public static final InventorySlot LEGS = InventorySlot.of(\"legs\", \"leggings\");\n    public static final InventorySlot BOOTS = InventorySlot.of(\"boots\", \"feet\");\n    public static final InventorySlot MAIN_HAND = InventorySlot.of(\"main_hand\", \"hand\", \"item\");\n    public static final InventorySlot OFF_HAND = InventorySlot.of(\"off_hand\");\n\n    public static List<InventorySlot> SLOTS = List.of(HELMET, CHEST, LEGS, BOOTS, MAIN_HAND, OFF_HAND);\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/util/KeyedRenderer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.util;\n\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.key.Keyed;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * A {@link ChatComponentRenderer chat renderer} that's identifiable by key.\n *\n * @since 2.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic interface KeyedRenderer extends Keyed, ChatComponentRenderer {\n\n    /**\n     * Creates a new renderer with the corresponding key.\n     *\n     * @param key      the renderer's key\n     * @param renderer the chat renderer\n     * @return the keyed renderer\n     * @since 2.0.0\n     */\n    static KeyedRenderer keyedRenderer(final Key key, final ChatComponentRenderer renderer) {\n        return new KeyedRendererImpl(key, renderer);\n    }\n\n}\n"
  },
  {
    "path": "api/src/main/java/net/draycia/carbon/api/util/KeyedRendererImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.api.util;\n\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\nrecord KeyedRendererImpl(Key key, ChatComponentRenderer renderer) implements KeyedRenderer {\n\n    @Override\n    public Component render(\n        final CarbonPlayer sender,\n        final Audience recipient,\n        final Component message,\n        final Component originalMessage\n    ) {\n        return this.renderer.render(sender, recipient, message, originalMessage);\n    }\n\n}\n"
  },
  {
    "path": "build-logic/build.gradle.kts",
    "content": "plugins {\n  `kotlin-dsl`\n}\n\nrepositories {\n  gradlePluginPortal()\n  maven(\"https://oss.sonatype.org/content/repositories/snapshots/\") {\n    mavenContent { snapshotsOnly() }\n  }\n}\n\ndependencies {\n  implementation(libs.shadow)\n  implementation(libs.indraCommon)\n  implementation(libs.cloud.build.logic)\n  implementation(libs.indraLicenseHeader)\n  implementation(libs.mod.publish.plugin)\n  implementation(libs.configurateYaml)\n  implementation(libs.gremlin.gradle)\n  implementation(libs.run.task)\n  implementation(libs.gson)\n\n  // https://github.com/gradle/gradle/issues/15383#issuecomment-779893192\n  implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))\n}\n"
  },
  {
    "path": "build-logic/settings.gradle.kts",
    "content": "dependencyResolutionManagement {\n  versionCatalogs {\n    create(\"libs\") {\n      from(files(\"../gradle/libs.versions.toml\"))\n    }\n  }\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/CarbonPermissionsExtension.kt",
    "content": "import io.leangen.geantyref.TypeToken\nimport org.gradle.api.file.RegularFileProperty\nimport org.gradle.api.model.ObjectFactory\nimport org.gradle.api.provider.ListProperty\nimport org.gradle.api.provider.Provider\nimport org.gradle.kotlin.dsl.listProperty\nimport org.spongepowered.configurate.ConfigurationNode\nimport org.spongepowered.configurate.yaml.NodeStyle\nimport org.spongepowered.configurate.yaml.YamlConfigurationLoader\nimport javax.inject.Inject\n\nabstract class CarbonPermissionsExtension @Inject constructor(private val objects: ObjectFactory) {\n  abstract val yaml: RegularFileProperty\n\n  val permissions: Provider<List<Permission>> = create()\n\n  private fun create(): ListProperty<Permission> = objects.listProperty<Permission>().also {\n    it.set(yaml.map { file ->\n      val loader = YamlConfigurationLoader.builder()\n        .path(file.asFile.toPath())\n        .nodeStyle(NodeStyle.BLOCK)\n        .build()\n      loader.load().childrenMap().map { (name, child) ->\n        loadPermission(name as String, child)\n      }\n    })\n    it.disallowChanges()\n    it.finalizeValueOnRead()\n  }\n\n  private fun loadPermission(name: String, node: ConfigurationNode): Permission {\n    return if (node.isMap) {\n      Permission(\n        name,\n        node.node(\"description\").string,\n        node.node(\"children\").takeIf { c -> !c.virtual() }\n          ?.get(object : TypeToken<HashMap<String, Boolean>>() {})\n      )\n    } else {\n      Permission(name, node.string, null)\n    }\n  }\n\n  data class Permission(\n    val string: String,\n    val description: String?,\n    val children: Map<String, Boolean>?\n  )\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/CarbonPlatformExtension.kt",
    "content": "import org.gradle.api.file.RegularFileProperty\n\nabstract class CarbonPlatformExtension {\n  abstract val productionJar: RegularFileProperty\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/ConfigurablePluginsExt.kt",
    "content": "import org.gradle.api.Action\nimport org.gradle.api.artifacts.ExternalModuleDependency\nimport org.gradle.api.artifacts.MinimalExternalModuleDependency\nimport org.gradle.api.provider.ListProperty\nimport org.gradle.api.provider.Provider\n\nabstract class ConfigurablePluginsExt {\n  data class DepPlugin(\n    val dep: Provider<MinimalExternalModuleDependency>,\n    val op: Action<in ExternalModuleDependency>?,\n    val defaultEnabled: Boolean = false,\n    val name: String = dep.get().name\n  )\n\n  abstract val gradleDependencyBased: ListProperty<DepPlugin>\n\n  fun dependency(lib: Provider<MinimalExternalModuleDependency>, op: Action<in ExternalModuleDependency>? = null) {\n    gradleDependencyBased.add(DepPlugin(lib, op))\n  }\n\n  fun dependency(lib: Provider<MinimalExternalModuleDependency>, defaultEnabled: Boolean, op: Action<in ExternalModuleDependency>? = null) {\n    gradleDependencyBased.add(DepPlugin(lib, op, defaultEnabled))\n  }\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/FetchLuckPermsDownloads.kt",
    "content": "import org.gradle.api.DefaultTask\nimport org.gradle.api.file.ProjectLayout\nimport org.gradle.api.file.RegularFileProperty\nimport org.gradle.api.tasks.OutputFile\nimport org.gradle.api.tasks.TaskAction\nimport org.gradle.api.tasks.UntrackedTask\nimport java.net.URI\nimport javax.inject.Inject\n\n@UntrackedTask(because = \"Always check for new metadata\")\nabstract class FetchLuckPermsDownloads : DefaultTask() {\n  companion object {\n    const val ENDPOINT: String = \"https://metadata.luckperms.net/data/downloads\"\n  }\n\n  @get:Inject\n  abstract val layout: ProjectLayout\n\n  @get:OutputFile\n  abstract val outputFile: RegularFileProperty\n\n  init {\n    init()\n  }\n\n  private fun init() {\n    outputFile.convention(layout.buildDirectory.file(\"luckperms/downloads.json\"))\n  }\n\n  @TaskAction\n  fun run () {\n    val url = URI.create(ENDPOINT).toURL()\n    val data = url.readText(Charsets.UTF_8)\n    val outFile = outputFile.get().asFile.also {\n      it.parentFile.mkdirs()\n      if (it.exists()) {\n        it.delete()\n      }\n    }\n    outFile.writeText(data, Charsets.UTF_8)\n  }\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/FetchLuckPermsJar.kt",
    "content": "import com.google.gson.Gson\nimport com.google.gson.JsonElement\nimport org.gradle.api.DefaultTask\nimport org.gradle.api.Project\nimport org.gradle.api.file.ProjectLayout\nimport org.gradle.api.file.RegularFileProperty\nimport org.gradle.api.provider.Property\nimport org.gradle.api.tasks.CacheableTask\nimport org.gradle.api.tasks.Input\nimport org.gradle.api.tasks.InputFile\nimport org.gradle.api.tasks.OutputFile\nimport org.gradle.api.tasks.PathSensitive\nimport org.gradle.api.tasks.PathSensitivity\nimport org.gradle.api.tasks.TaskAction\nimport org.gradle.api.tasks.TaskProvider\nimport org.gradle.kotlin.dsl.register\nimport java.net.URI\nimport javax.inject.Inject\n\n@CacheableTask\nabstract class FetchLuckPermsJar : DefaultTask() {\n  companion object {\n    fun setup(\n      project: Project,\n      type: String,\n    ): TaskProvider<FetchLuckPermsJar> {\n      val getMeta = project.tasks.register<FetchLuckPermsDownloads>(\"fetchLuckPermsDownloads\")\n      return project.tasks.register<FetchLuckPermsJar>(\"fetchLuckPermsJar\") {\n        this.type.set(type)\n        inputFile.set(getMeta.flatMap { it.outputFile })\n      }\n    }\n  }\n\n  @get:Inject\n  abstract val layout: ProjectLayout\n\n  @get:Input\n  abstract val type: Property<String>\n\n  @get:InputFile\n  @get:PathSensitive(PathSensitivity.NONE)\n  abstract val inputFile: RegularFileProperty\n\n  @get:OutputFile\n  abstract val outputFile: RegularFileProperty\n\n  init {\n    init()\n  }\n\n  private fun init() {\n    outputFile.convention(type.flatMap {\n      layout.buildDirectory.file(\"luckperms/${it}.jar\")\n    })\n  }\n\n  @TaskAction\n  fun run () {\n    val json = inputFile.get().asFile.readText(Charsets.UTF_8)\n    val map = Gson().fromJson(json, JsonElement::class.java).asJsonObject.get(\"downloads\").asJsonObject\n    val url = map.get(type.get()).asString\n    val data = URI.create(url).toURL().readBytes()\n    val outFile = outputFile.get().asFile.also {\n      it.parentFile.mkdirs()\n      if (it.exists()) {\n        it.delete()\n      }\n    }\n    outFile.writeBytes(data)\n  }\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/FileCopyTask.kt",
    "content": "import org.gradle.api.DefaultTask\nimport org.gradle.api.tasks.InputFile\nimport org.gradle.api.tasks.OutputFile\nimport org.gradle.api.tasks.TaskAction\n\nabstract class FileCopyTask : DefaultTask() {\n  @InputFile\n  val fileToCopy = project.objects.fileProperty()\n\n  @OutputFile\n  val destination = project.objects.fileProperty()\n\n  @TaskAction\n  fun copyFile() {\n    destination.get().asFile.parentFile.mkdirs()\n    fileToCopy.get().asFile.copyTo(destination.get().asFile, overwrite = true)\n  }\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/carbon.base-conventions.gradle.kts",
    "content": "plugins {\n  id(\"net.kyori.indra\")\n  id(\"net.kyori.indra.git\")\n  id(\"net.kyori.indra.checkstyle\")\n  id(\"net.kyori.indra.licenser.spotless\")\n}\n\nversion = rootProject.version\n\nindra {\n  gpl3OnlyLicense()\n\n  javaVersions {\n    target(21)\n  }\n\n  github(GITHUB_ORGANIZATION, GITHUB_REPO)\n}\n\nspotless {\n  java {\n    targetExclude(\n      \"src/main/java/net/draycia/carbon/common/messages/PrefixedDelegateIterator.java\",\n      \"src/main/java/net/draycia/carbon/common/messages/StandardPlaceholderResolverStrategyButDifferent.java\",\n      \"src/main/java/com/google/inject/assistedinject/**\"\n    )\n  }\n}\n\nindraSpotlessLicenser {\n  licenseHeaderFile(rootProject.file(\"LICENSE_HEADER\"))\n}\n\ntasks {\n  withType<JavaCompile> {\n    // disable unclaimed annotation and missing annotation warnings\n    options.compilerArgs.add(\"-Xlint:-processing,-classfile\")\n    options.compilerArgs.add(\"-parameters\")\n  }\n}\n\ndependencies {\n  checkstyle(libs.stylecheck)\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/carbon.build-logic.gradle.kts",
    "content": "plugins {\n  id(\"base\")\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/carbon.configurable-plugins.gradle.kts",
    "content": "import org.spongepowered.configurate.objectmapping.ConfigSerializable\nimport org.spongepowered.configurate.yaml.NodeStyle\nimport org.spongepowered.configurate.yaml.YamlConfigurationLoader\nimport xyz.jpenilla.runtask.task.RunWithPlugins\n\nval pluginsExt = extensions.create(\"configurablePlugins\", ConfigurablePluginsExt::class.java)\n\nafterEvaluate {\n  val configs = pluginsExt.gradleDependencyBased.get().map { entry ->\n    val c = configurations.register(entry.name + \"Plugin\") {\n      isTransitive = false\n    }\n    dependencies {\n      c.name(entry.dep) { entry.op?.execute(this) }\n    }\n    entry to c\n  }\n\n  tasks.withType(RunWithPlugins::class).configureEach {\n    val cfg = readConfig()\n    configs.forEach { (entry, configuration) ->\n      val enabled = cfg.taskOverrides[name]?.get(entry.name)\n        ?: cfg.defaults[entry.name]\n        ?: false\n      if (enabled) {\n        pluginJars.from(configuration)\n      }\n    }\n  }\n}\n\n@ConfigSerializable\nclass Config {\n  var defaults: MutableMap<String, Boolean> = mutableMapOf()\n  var taskOverrides: MutableMap<String, MutableMap<String, Boolean>> = mutableMapOf(\n    \"someTaskName\" to mutableMapOf(\"somePlugin\" to false)\n  )\n}\n\n@Synchronized\nfun readConfig(): Config {\n  val loader = YamlConfigurationLoader.builder()\n    .file(file(\"run-plugins.yml\"))\n    .nodeStyle(NodeStyle.BLOCK)\n    .defaultOptions {\n      it.header(\"Enable and disable optional plugins for run tasks in this project\")\n    }\n    .build()\n  val n = loader.load()\n  val c = n.get(Config::class.java) as Config\n  var write = false\n  for (e in pluginsExt.gradleDependencyBased.get()) {\n    if (!c.defaults.containsKey(e.name)) {\n      write = true\n      c.defaults[e.name] = e.defaultEnabled\n    }\n  }\n  if (write) {\n    n.set(c)\n    loader.save(n)\n  }\n  return c\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/carbon.permissions.gradle.kts",
    "content": "val ext = extensions.create(\"carbonPermission\", CarbonPermissionsExtension::class.java)\next.yaml.convention(rootProject.layout.projectDirectory.file(\"common/src/main/resources/carbon-permissions.yml\"))\n"
  },
  {
    "path": "build-logic/src/main/kotlin/carbon.platform-conventions.gradle.kts",
    "content": "import me.modmuss50.mpp.ReleaseType\n\nplugins {\n  id(\"carbon.base-conventions\")\n  id(\"me.modmuss50.mod-publish-plugin\")\n  id(\"xyz.jpenilla.gremlin-gradle\")\n}\n\ndecorateVersion()\n\nconfigurations.runtimeDownload {\n  exclude(\"org.slf4j\", \"slf4j-api\")\n  exclude(\"com.google.errorprone\", \"error_prone_annotations\")\n  exclude(\"io.leangen.geantyref\", \"geantyref\")\n}\n\nval platformExtension = extensions.create<CarbonPlatformExtension>(\"carbonPlatform\")\n\ndependencies {\n  runtimeDownload(libs.h2)\n  runtimeDownload(libs.postgresql)\n  runtimeDownload(libs.mariadb)\n  runtimeDownload(libs.zstdjni)\n  runtimeDownload(libs.jdbiCore)\n  runtimeDownload(libs.jdbiObject)\n  runtimeDownload(libs.jdbiPostgres)\n  runtimeDownload(libs.caffeine)\n  runtimeDownload(libs.jedis) {\n    exclude(\"com.google.code.gson\", \"gson\")\n  }\n  runtimeDownload(libs.rabbitmq)\n  runtimeDownload(libs.nats)\n  runtimeDownload(libs.guice) {\n    exclude(\"com.google.guava\")\n  }\n  runtimeDownload(libs.assistedInject) {\n    isTransitive = false\n  }\n  runtimeDownload(libs.flyway) {\n    exclude(\"com.google.code.gson\", \"gson\")\n  }\n  runtimeDownload(libs.flywayMysql) {\n    isTransitive = false\n  }\n  runtimeDownload(libs.flywayPostgres) {\n    isTransitive = false\n  }\n}\n\ntasks {\n  jar {\n    manifest {\n      attributes(\n        \"carbon-version\" to project.version,\n        \"carbon-commit\" to lastCommitHash(),\n        \"carbon-branch\" to currentBranch(),\n      )\n    }\n  }\n  val copyJar = register<FileCopyTask>(\"copyJar\") {\n    fileToCopy = platformExtension.productionJar\n    destination = rootProject.layout.buildDirectory.dir(\"libs\").flatMap {\n      it.file(fileToCopy.map { file -> file.asFile.name })\n    }\n  }\n  build {\n    dependsOn(copyJar)\n  }\n  javadoc {\n    enabled = false\n  }\n}\n\nval projectVersion = project.version as String\n\npublishMods.modrinth {\n  projectId = \"QzooIsZI\"\n  type = if (projectVersion.contains(\"-beta.\")) ReleaseType.BETA else ReleaseType.STABLE\n  file = platformExtension.productionJar\n  changelog = releaseNotes\n  accessToken = providers.environmentVariable(\"MODRINTH_TOKEN\")\n  requires(\"luckperms\")\n  optional(\"miniplaceholders\")\n  minecraftVersions.addAll(\n    \"1.21.4\",\n    \"1.21.5\",\n    \"1.21.6\",\n    \"1.21.7\",\n    \"1.21.8\",\n    \"1.21.9\",\n    \"1.21.10\",\n    \"1.21.11\",\n    \"26.1\",\n    \"26.1.1\",\n    \"26.1.2\",\n  )\n}\n\ntasks.writeDependencies {\n  outputFileName = \"carbon-dependencies.txt\"\n  repos.add(\"https://repo.papermc.io/repository/maven-public/\")\n  repos.add(\"https://repo.maven.apache.org/maven2/\")\n}\n\ngremlin {\n  defaultJarRelocatorDependencies = false\n  defaultGremlinRuntimeDependency = false\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/carbon.publishing-conventions.gradle.kts",
    "content": "plugins {\n  id(\"carbon.base-conventions\")\n  id(\"net.kyori.indra.publishing\")\n  id(\"org.incendo.cloud-build-logic.publishing\")\n}\n\nsigning {\n  val signingKey: String? by project\n  val signingPassword: String? by project\n  useInMemoryPgpKeys(signingKey, signingPassword)\n}\n\nindra {\n  configurePublications {\n    pom {\n      developers {\n        developer {\n          id.set(\"Vicarious\")\n          name.set(\"Josua Parks\")\n        }\n        developer {\n          id.set(\"jmp\")\n          name.set(\"Jason Penilla\")\n        }\n      }\n    }\n  }\n}\n\njavadocLinks {\n  defaultJavadocProvider = \"https://www.javadocs.dev/{group}/{name}/{version}\"\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/carbon.shadow-platform.gradle.kts",
    "content": "plugins {\n  id(\"carbon.platform-conventions\")\n  id(\"com.gradleup.shadow\")\n}\n\ntasks {\n  jar {\n    archiveClassifier = \"unshaded\"\n  }\n  shadowJar {\n    archiveClassifier.set(null as String?)\n    configureShadowJar()\n\n    mergeServiceFiles()\n    // Needed for mergeServiceFiles to work properly in Shadow 9+\n    filesMatching(\"META-INF/services/**\") {\n      duplicatesStrategy = DuplicatesStrategy.INCLUDE\n    }\n  }\n}\n\nextensions.configure<CarbonPlatformExtension> {\n  productionJar = tasks.shadowJar.flatMap { it.archiveFile }\n}\n"
  },
  {
    "path": "build-logic/src/main/kotlin/constants.kt",
    "content": "const val GITHUB_ORGANIZATION = \"Hexaoxide\"\nconst val GITHUB_REPO = \"Carbon\"\nconst val GITHUB_REPO_URL = \"https://github.com/$GITHUB_ORGANIZATION/$GITHUB_REPO\"\n"
  },
  {
    "path": "build-logic/src/main/kotlin/extensions.kt",
    "content": "import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar\nimport net.kyori.indra.git.IndraGitExtension\nimport org.apache.tools.ant.filters.ReplaceTokens\nimport org.gradle.accessors.dm.LibrariesForLibs\nimport org.gradle.api.Project\nimport org.gradle.api.Task\nimport org.gradle.api.provider.Provider\nimport org.gradle.kotlin.dsl.filter\nimport org.gradle.kotlin.dsl.the\nimport org.gradle.language.jvm.tasks.ProcessResources\nimport xyz.jpenilla.gremlin.gradle.ShadowGremlin\n\nfun ProcessResources.replace(\n  pattern: String,\n  tokens: Map<String, Any?>\n) {\n  inputs.properties(tokens)\n  filesMatching(pattern) {\n    filter<ReplaceTokens>(\n      \"beginToken\" to \"\\${\",\n      \"endToken\" to \"}\",\n      \"tokens\" to tokens\n    )\n  }\n}\n\nval Project.releaseNotes: Provider<String>\n  get() = providers.environmentVariable(\"RELEASE_NOTES\")\n\n/**\n * Relocate a package into the `carbonchat.libs.` namespace.\n */\nfun Task.relocateDependency(pkg: String) {\n  ShadowGremlin.relocateWithPrefix(this, \"carbonchat.libs\", pkg)\n}\n\nfun Task.standardRuntimeRelocations() {\n  relocateDependency(\"com.github.benmanes\")\n  // relocateDependency(\"com.github.luben.zstd\") // natives don't like relocation - hopefully nothing breaks :)\n  relocateDependency(\"com.google.protobuf\")\n  relocateDependency(\"com.mysql.cj\")\n  relocateDependency(\"com.mysql.jdbc\")\n  relocateDependency(\"com.rabbitmq\")\n  relocateDependency(\"io.nats\")\n  relocateDependency(\"net.i2p.crypto\")\n  relocateDependency(\"org.apache.commons.pool2\")\n  relocateDependency(\"org.jdbi\")\n  relocateDependency(\"org.mariadb.jdbc\")\n  relocateDependency(\"org.postgresql\")\n  relocateDependency(\"redis.clients.jedis\")\n  relocateDependency(\"org.flywaydb\")\n  relocateDependency(\"com.fasterxml\")\n  relocateDependency(\"org.h2\")\n}\n\n/**\n * Relocates dependencies which we bundle and relocate on all platforms.\n */\nfun Task.standardRelocations() {\n  relocateDependency(\"org.bstats\")\n  relocateDependency(\"net.kyori.adventure.serializer.configurate4\")\n  relocateDependency(\"com.sasorio.event\")\n  relocateDependency(\"net.kyori.moonshine\")\n  relocateDependency(\"com.seiama.registry\")\n  relocateDependency(\"org.spongepowered.configurate\")\n  relocateDependency(\"com.google.thirdparty.publicsuffix\")\n  relocateDependency(\"com.zaxxer.hikari\")\n  relocateDependency(\"ninja.egg82.messenger\")\n  relocateDependency(\"org.antlr\")\n  relocateDependency(\"com.electronwill\")\n  relocateDependency(\"xyz.jpenilla.gremlin\")\n}\n\nfun Task.relocateCloud() {\n  relocateDependency(\"org.incendo.cloud\")\n}\n\nfun Task.relocateGuice() {\n  relocateDependency(\"com.google.inject\")\n  relocateDependency(\"org.aopalliance\")\n  relocateDependency(\"jakarta.inject\")\n}\n\nfun ShadowJar.configureShadowJar() {\n  //minimize()\n  standardRelocations()\n  dependencies {\n    // not needed or provided by platform at runtime\n    exclude(dependency(\"com.google.code.findbugs:jsr305\"))\n    exclude(dependency(\"com.google.errorprone:error_prone_annotations\"))\n    exclude { it.moduleGroup == \"com.google.guava\" }\n    exclude(dependency(\"com.google.j2objc:j2objc-annotations\"))\n    exclude(dependency(\"io.netty:netty-all\"))\n    exclude(dependency(\"io.netty:netty-buffer\"))\n    exclude(dependency(\"it.unimi.dsi:fastutil\"))\n    exclude(dependency(\"org.checkerframework:checker-qual\"))\n    exclude(dependency(\"org.slf4j:slf4j-api\"))\n  }\n}\n\nfun Project.lastCommitHash(): String =\n  the<IndraGitExtension>().commit().orNull?.name?.substring(0, 7)\n    ?: error(\"Could not determine commit hash\")\n\nfun Project.decorateVersion() {\n  val versionString = version as String\n  version = if (versionString.endsWith(\"-SNAPSHOT\")) {\n    \"$versionString+${lastCommitHash()}\"\n  } else {\n    versionString\n  }\n}\n\nfun Project.currentBranch(): String {\n  System.getenv(\"GITHUB_HEAD_REF\")?.takeIf { it.isNotEmpty() }\n    ?.let { return it }\n  System.getenv(\"GITHUB_REF\")?.takeIf { it.isNotEmpty() }\n    ?.let { return it.replaceFirst(\"refs/heads/\", \"\") }\n\n  val indraGit = the<IndraGitExtension>().takeIf { it.isPresent }\n\n  return indraGit?.branchName()?.orNull ?: \"detached-head\"\n}\n\nval Project.libs: LibrariesForLibs\n  get() = the()\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "plugins {\n  id(\"carbon.build-logic\")\n  alias(libs.plugins.hangar.publish)\n  alias(libs.plugins.cloud.buildLogic.rootProject.publishing)\n}\n\nval projectVersion: String by project // get from gradle.properties\nversion = projectVersion\n\nfun Project.platformJar(): Provider<RegularFile> =\n  extensions.getByType<CarbonPlatformExtension>().productionJar\n\nhangarPublish.publications.register(\"plugin\") {\n  version = projectVersion\n  id = \"Carbon\"\n  channel = if (projectVersion.contains(\"-beta.\")) \"Beta\" else \"Release\"\n  changelog = releaseNotes\n  apiKey = providers.environmentVariable(\"HANGAR_UPLOAD_KEY\")\n  platforms.paper {\n    jar = project(\":carbonchat-paper\").platformJar()\n    platformVersions.add(\"1.21.4-26.1.2\")\n    dependencies {\n      url(\"LuckPerms\", \"https://luckperms.net/\")\n      hangar(\"Essentials\") {\n        required = false\n      }\n      url(\"DiscordSRV\", \"https://www.spigotmc.org/resources/discordsrv.18494/\") {\n        required = false\n      }\n      url(\"PlaceholderAPI\", \"https://www.spigotmc.org/resources/placeholderapi.6245/\") {\n        required = false\n      }\n      hangar(\"MiniPlaceholders\") {\n        required = false\n      }\n    }\n  }\n  platforms.velocity {\n    jar = project(\":carbonchat-velocity\").platformJar()\n    platformVersions.add(\"3.5\")\n    dependencies {\n      url(\"LuckPerms\", \"https://luckperms.net/\")\n      hangar(\"MiniPlaceholders\") {\n        required = false\n      }\n      hangar(\"SignedVelocity\") {\n        required = false\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "common/build.gradle.kts",
    "content": "plugins {\n  id(\"carbon.base-conventions\")\n}\n\ndependencies {\n  api(projects.carbonchatApi)\n  api(libs.gremlin.runtime)\n  compileOnlyApi(platform(libs.log4jBom))\n  compileOnlyApi(libs.log4jApi)\n\n  // Configs\n  api(libs.configurateHocon) {\n    // Provided at platform level (usually through adventure)\n    exclude(\"net.kyori\", \"option\")\n  }\n  // Bring in option for -common compile\n  compileOnly(libs.configurateHocon)\n  api(libs.adventureSerializerConfigurate4) {\n    isTransitive = false\n  }\n\n  // Cloud\n  api(platform(libs.cloudBom))\n  api(libs.cloudCore)\n  api(platform(libs.cloudMinecraftBom))\n  api(libs.cloudMinecraftExtras) {\n    isTransitive = false\n  }\n  api(libs.cloudSigned)\n\n  // Other\n  compileOnlyApi(libs.guice) {\n    exclude(\"com.google.guava\")\n  }\n  compileOnlyApi(libs.assistedInject) {\n    isTransitive = false\n  }\n  compileOnlyApi(libs.luckPermsApi)\n  compileOnlyApi(libs.event)\n\n  // Storage\n  compileOnlyApi(libs.jdbiCore)\n  compileOnlyApi(libs.jdbiObject)\n  compileOnlyApi(libs.jdbiPostgres)\n  api(libs.hikariCP)\n  compileOnlyApi(libs.flyway) {\n    exclude(\"com.google.code.gson\", \"gson\")\n  }\n  compileOnlyApi(libs.flywayMysql) {\n    isTransitive = false\n  }\n  compileOnlyApi(libs.flywayPostgres) {\n    isTransitive = false\n  }\n\n  // Messaging\n  api(libs.messenger)\n  api(libs.messengerNats)\n  api(libs.messengerRabbitmq)\n  api(libs.messengerRedis)\n  compileOnlyApi(libs.netty)\n\n  api(libs.event)\n  api(libs.registry) {\n    exclude(\"com.google.guava\")\n  }\n  api(libs.kyoriMoonshine)\n  api(libs.kyoriMoonshineCore)\n  api(libs.kyoriMoonshineStandard)\n\n  compileOnlyApi(libs.caffeine)\n\n  // we shade and relocate a newer version than minecraft provides\n  compileOnlyApi(libs.guava)\n\n  // Plugins\n  compileOnly(libs.miniplaceholders)\n}\n"
  },
  {
    "path": "common/src/main/java/com/google/inject/assistedinject/FactoryProvider3.java",
    "content": "/*\n * Copyright (C) 2008 Google Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage com.google.inject.assistedinject;\n\nimport static com.google.common.base.Preconditions.checkState;\nimport static com.google.common.collect.Iterables.getOnlyElement;\n\nimport com.google.common.base.MoreObjects;\nimport com.google.common.base.Objects;\nimport com.google.common.collect.HashMultimap;\nimport com.google.common.collect.ImmutableList;\nimport com.google.common.collect.ImmutableMap;\nimport com.google.common.collect.ImmutableSet;\nimport com.google.common.collect.Iterables;\nimport com.google.common.collect.Lists;\nimport com.google.common.collect.Multimap;\nimport com.google.common.collect.Sets;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Binder;\nimport com.google.inject.Binding;\nimport com.google.inject.ConfigurationException;\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.google.inject.Key;\nimport com.google.inject.Module;\nimport com.google.inject.Provider;\nimport com.google.inject.ProvisionException;\nimport com.google.inject.Scopes;\nimport com.google.inject.TypeLiteral;\nimport com.google.inject.internal.Annotations;\nimport com.google.inject.internal.Errors;\nimport com.google.inject.internal.ErrorsException;\nimport com.google.inject.internal.UniqueAnnotations;\nimport com.google.inject.internal.util.Classes;\nimport com.google.inject.spi.BindingTargetVisitor;\nimport com.google.inject.spi.Dependency;\nimport com.google.inject.spi.HasDependencies;\nimport com.google.inject.spi.InjectionPoint;\nimport com.google.inject.spi.Message;\nimport com.google.inject.spi.ProviderInstanceBinding;\nimport com.google.inject.spi.ProviderWithExtensionVisitor;\nimport com.google.inject.spi.Toolable;\nimport com.google.inject.util.Providers;\nimport java.lang.annotation.Annotation;\nimport java.lang.invoke.MethodHandle;\nimport java.lang.invoke.MethodHandles;\nimport java.lang.invoke.MethodType;\nimport java.lang.reflect.Constructor;\nimport java.lang.reflect.Field;\nimport java.lang.reflect.InvocationHandler;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Modifier;\nimport java.lang.reflect.Proxy;\nimport java.util.Arrays;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.function.Supplier;\nimport java.util.logging.Level;\nimport java.util.logging.Logger;\n\n/**\n * The newer implementation of factory provider. This implementation uses a child injector to create\n * values.\n *\n * <p>Carbon - modified from FactoryProvider2 for default method support</p>\n *\n * @author jessewilson@google.com (Jesse Wilson)\n * @author dtm@google.com (Daniel Martin)\n * @author schmitt@google.com (Peter Schmitt)\n * @author sameb@google.com (Sam Berlin)\n */\npublic final class FactoryProvider3<F> // Carbon - public\n    implements InvocationHandler,\n        ProviderWithExtensionVisitor<F>,\n        HasDependencies,\n        AssistedInjectBinding<F> {\n\n  /** A constant annotation to denote the return value, instead of creating a new one each time. */\n  static final Annotation RETURN_ANNOTATION = UniqueAnnotations.create();\n\n  // use the logger under a well-known name, not FactoryProvider2\n  static final Logger logger = Logger.getLogger(AssistedInject.class.getName());\n\n  /**\n   * A constant that determines if we allow fallback to using the JDK internals to make a \"private\n   * lookup\". Typically always true, but reflectively set to false in tests.\n   */\n  @SuppressWarnings(\"FieldCanBeFinal\") // non-final for testing\n  private static boolean allowPrivateLookupFallback = true;\n\n  /**\n   * A constant that determines if we allow fallback to using method handle workarounds (if\n   * required). Typically always true, but reflectively set to false in tests.\n   */\n  @SuppressWarnings(\"FieldCanBeFinal\") // non-final for testing\n  private static boolean allowMethodHandleWorkaround = true;\n\n  /** if a factory method parameter isn't annotated, it gets this annotation. */\n  static final Assisted DEFAULT_ANNOTATION =\n      new Assisted() {\n        @Override\n        public String value() {\n          return \"\";\n        }\n\n        @Override\n        public Class<? extends Annotation> annotationType() {\n          return Assisted.class;\n        }\n\n        @Override\n        public boolean equals(Object o) {\n          return o instanceof Assisted && ((Assisted) o).value().isEmpty();\n        }\n\n        @Override\n        public int hashCode() {\n          return 127 * \"value\".hashCode() ^ \"\".hashCode();\n        }\n\n        @Override\n        public String toString() {\n          return \"@\"\n              + Assisted.class.getName()\n              + \"(\"\n              + Annotations.memberValueString(\"value\", \"\")\n              + \")\";\n        }\n      };\n\n  /** All the data necessary to perform an assisted inject. */\n  private static class AssistData implements AssistedMethod {\n    /** the constructor the implementation is constructed with. */\n    final Constructor<?> constructor;\n    /** the return type in the factory method that the constructor is bound to. */\n    final Key<?> returnType;\n    /** the parameters in the factory method associated with this data. */\n    final ImmutableList<Key<?>> paramTypes;\n    /** the type of the implementation constructed */\n    final TypeLiteral<?> implementationType;\n\n    /** All non-assisted dependencies required by this method. */\n    final Set<Dependency<?>> dependencies;\n    /** The factory method associated with this data */\n    final Method factoryMethod;\n\n    /** true if {@link #isValidForOptimizedAssistedInject} returned true. */\n    final boolean optimized;\n    /** the list of optimized providers, empty if not optimized. */\n    final List<ThreadLocalProvider> providers;\n    /** used to perform optimized factory creations. */\n    volatile Binding<?> cachedBinding; // TODO: volatile necessary?\n\n    AssistData(\n        Constructor<?> constructor,\n        Key<?> returnType,\n        ImmutableList<Key<?>> paramTypes,\n        TypeLiteral<?> implementationType,\n        Method factoryMethod,\n        Set<Dependency<?>> dependencies,\n        boolean optimized,\n        List<ThreadLocalProvider> providers) {\n      this.constructor = constructor;\n      this.returnType = returnType;\n      this.paramTypes = paramTypes;\n      this.implementationType = implementationType;\n      this.factoryMethod = factoryMethod;\n      this.dependencies = dependencies;\n      this.optimized = optimized;\n      this.providers = providers;\n    }\n\n    @Override\n    public String toString() {\n      return MoreObjects.toStringHelper(getClass())\n          .add(\"ctor\", constructor)\n          .add(\"return type\", returnType)\n          .add(\"param type\", paramTypes)\n          .add(\"implementation type\", implementationType)\n          .add(\"dependencies\", dependencies)\n          .add(\"factory method\", factoryMethod)\n          .add(\"optimized\", optimized)\n          .add(\"providers\", providers)\n          .add(\"cached binding\", cachedBinding)\n          .toString();\n    }\n\n    @Override\n    public Set<Dependency<?>> getDependencies() {\n      return dependencies;\n    }\n\n    @Override\n    public Method getFactoryMethod() {\n      return factoryMethod;\n    }\n\n    @Override\n    public Constructor<?> getImplementationConstructor() {\n      return constructor;\n    }\n\n    @Override\n    public TypeLiteral<?> getImplementationType() {\n      return implementationType;\n    }\n  }\n\n  /** Mapping from method to the data about how the method will be assisted. */\n  private final ImmutableMap<Method, AssistData> assistDataByMethod;\n\n  /** Mapping from method to method handle, for generated default methods. */\n  private final ImmutableMap<Method, MethodHandle> methodHandleByMethod;\n\n  /** the hosting injector, or null if we haven't been initialized yet */\n  private Injector injector;\n\n  /** the factory interface, implemented and provided */\n  private final F factory;\n\n  /** The key that this is bound to. */\n  private final Key<F> factoryKey;\n\n  /** The binding collector, for equality/hashing purposes. */\n  private final BindingCollector collector;\n\n  /**\n   * Utility class for collecting factory bindings. Used for configuring {@link FactoryProvider3}.\n   *\n   * @author schmitt@google.com (Peter Schmitt)\n   */\n  static class BindingCollector {\n\n    private final Map<Key<?>, TypeLiteral<?>> bindings = new HashMap<>();\n\n    public BindingCollector addBinding(Key<?> key, TypeLiteral<?> target) {\n      if (bindings.containsKey(key)) {\n        throw new ConfigurationException(\n            ImmutableSet.of(new Message(\"Only one implementation can be specified for \" + key)));\n      }\n\n      bindings.put(key, target);\n\n      return this;\n    }\n\n    public Map<Key<?>, TypeLiteral<?>> getBindings() {\n      return Collections.unmodifiableMap(bindings);\n    }\n\n    @Override\n    public int hashCode() {\n      return bindings.hashCode();\n    }\n\n    @Override\n    public boolean equals(Object obj) {\n      return (obj instanceof BindingCollector) && bindings.equals(((BindingCollector) obj).bindings);\n    }\n  }\n\n  /**\n   * @param factoryKey a key for a Java interface that defines one or more create methods.\n   * @param collector binding configuration that maps method return types to implementation types.\n   * @param userLookups user provided lookups, optional.\n   */\n  public FactoryProvider3(\n      Key<F> factoryKey, BindingCollector collector, MethodHandles.Lookup userLookups) {\n    this.factoryKey = factoryKey;\n    this.collector = collector == null ? new BindingCollector() : collector; collector = this.collector; // Carbon\n\n    TypeLiteral<F> factoryType = factoryKey.getTypeLiteral();\n    Errors errors = new Errors();\n\n    @SuppressWarnings(\"unchecked\") // we imprecisely treat the class literal of T as a Class<T>\n    Class<F> factoryRawType = (Class<F>) (Class<?>) factoryType.getRawType();\n\n    try {\n      if (!factoryRawType.isInterface()) {\n        throw errors.addMessage(\"%s must be an interface.\", factoryRawType).toException();\n      }\n\n      Multimap<String, Method> defaultMethods = HashMultimap.create();\n      Multimap<String, Method> otherMethods = HashMultimap.create();\n      ImmutableMap.Builder<Method, AssistData> assistDataBuilder = ImmutableMap.builder();\n      // TODO: also grab methods from superinterfaces\n      for (Method method : factoryRawType.getMethods()) {\n        // Skip static methods\n        if (Modifier.isStatic(method.getModifiers())) {\n          continue;\n        }\n\n        // Skip default methods that java8 may have created.\n        if (method.isDefault()/*isDefault(method) && (method.isBridge() || method.isSynthetic())*/) { // Carbon - allow non-generated default methods\n          // Even synthetic default methods need the return type validation...\n          // unavoidable consequence of javac8. :-(\n          validateFactoryReturnType(errors, method.getReturnType(), factoryRawType);\n          defaultMethods.put(method.getName(), method);\n          continue;\n        }\n        otherMethods.put(method.getName(), method);\n\n        TypeLiteral<?> returnTypeLiteral = factoryType.getReturnType(method);\n        Key<?> returnType;\n        try {\n          returnType =\n              Annotations.getKey(returnTypeLiteral, method, method.getAnnotations(), errors);\n        } catch (ConfigurationException ce) {\n          // If this was an error due to returnTypeLiteral not being specified, rephrase\n          // it as our factory not being specified, so it makes more sense to users.\n          if (isTypeNotSpecified(returnTypeLiteral, ce)) {\n            throw errors.keyNotFullySpecified(TypeLiteral.get(factoryRawType)).toException();\n          } else {\n            throw ce;\n          }\n        }\n        validateFactoryReturnType(errors, returnType.getTypeLiteral().getRawType(), factoryRawType);\n        List<TypeLiteral<?>> params = factoryType.getParameterTypes(method);\n        Annotation[][] paramAnnotations = method.getParameterAnnotations();\n        int p = 0;\n        List<Key<?>> keys = Lists.newArrayList();\n        for (TypeLiteral<?> param : params) {\n          Key<?> paramKey = Annotations.getKey(param, method, paramAnnotations[p++], errors);\n          Class<?> underlylingType = paramKey.getTypeLiteral().getRawType();\n          if (underlylingType.equals(Provider.class)\n              || underlylingType.equals(jakarta.inject.Provider.class)) {\n            errors.addMessage(\n                \"A Provider may not be a type in a factory method of an AssistedInject.\"\n                    + \"\\n  Offending instance is parameter [%s] with key [%s] on method [%s]\",\n                p, paramKey, method);\n          }\n          keys.add(assistKey(method, paramKey, errors));\n        }\n        ImmutableList<Key<?>> immutableParamList = ImmutableList.copyOf(keys);\n\n        // try to match up the method to the constructor\n        TypeLiteral<?> implementation = collector.getBindings().get(returnType);\n        if (implementation == null) {\n          implementation = returnType.getTypeLiteral();\n        }\n        Class<? extends Annotation> scope =\n            Annotations.findScopeAnnotation(errors, implementation.getRawType());\n        if (scope != null) {\n          errors.addMessage(\n              \"Found scope annotation [%s] on implementation class \"\n                  + \"[%s] of AssistedInject factory [%s].\\nThis is not allowed, please\"\n                  + \" remove the scope annotation.\",\n              scope, implementation.getRawType(), factoryType);\n        }\n\n        InjectionPoint ctorInjectionPoint;\n        try {\n          ctorInjectionPoint =\n              findMatchingConstructorInjectionPoint(\n                  method, returnType, implementation, immutableParamList);\n        } catch (ErrorsException ee) {\n          errors.merge(ee.getErrors());\n          continue;\n        }\n\n        Constructor<?> constructor = (Constructor<?>) ctorInjectionPoint.getMember();\n        List<ThreadLocalProvider> providers = Collections.emptyList();\n        Set<Dependency<?>> deps = getDependencies(ctorInjectionPoint, implementation);\n        boolean optimized = false;\n        // Now go through all dependencies of the implementation and see if it is OK to\n        // use an optimized form of assistedinject2.  The optimized form requires that\n        // all injections directly inject the object itself (and not a Provider of the object,\n        // or an Injector), because it caches a single child injector and mutates the Provider\n        // of the arguments in a ThreadLocal.\n        if (isValidForOptimizedAssistedInject(deps, implementation.getRawType(), factoryType)) {\n          ImmutableList.Builder<ThreadLocalProvider> providerListBuilder = ImmutableList.builder();\n          for (int i = 0; i < params.size(); i++) {\n            providerListBuilder.add(new ThreadLocalProvider());\n          }\n          providers = providerListBuilder.build();\n          optimized = true;\n        }\n\n        AssistData data =\n            new AssistData(\n                constructor,\n                returnType,\n                immutableParamList,\n                implementation,\n                method,\n                removeAssistedDeps(deps),\n                optimized,\n                providers);\n        assistDataBuilder.put(method, data);\n      }\n\n      factory =\n          factoryRawType.cast(\n              Proxy.newProxyInstance(\n                  factoryRawType.getClassLoader(), new Class<?>[] {factoryRawType}, this));\n\n      // Now go back through default methods. Try to use MethodHandles to make things\n      // work.  If that doesn't work, fallback to trying to find compatible method\n      // signatures.\n      Map<Method, AssistData> dataSoFar = assistDataBuilder.build();\n      ImmutableMap.Builder<Method, MethodHandle> methodHandleBuilder = ImmutableMap.builder();\n      boolean warnedAboutUserLookups = false;\n      for (Map.Entry<String, Method> entry : defaultMethods.entries()) {\n        if (!warnedAboutUserLookups\n            && userLookups == null\n            && !Modifier.isPublic(factory.getClass().getModifiers())) {\n          warnedAboutUserLookups = true;\n          logger.log(\n              Level.WARNING,\n              \"AssistedInject factory {0} is non-public and has default methods. \" // Carbon - adjust message\n                  + \" Please pass a `MethodHandles.lookup()` with\"\n                  + \" FactoryModuleBuilder.withLookups when using this factory so that Guice can\"\n                  + \" properly call the default methods. Guice will try to workaround this, but \"\n                  + \"it does not always work (depending on the method signatures of the factory).\",\n              new Object[] {factoryType});\n        }\n\n        // Note: If the user didn't supply a valid lookup, we always try to fallback to the hacky\n        // signature comparing workaround below.\n        // This is because all these shenanigans are only necessary because we're implementing\n        // AssistedInject through a Proxy. If we were to generate a subclass (which we theoretically\n        // _could_ do), then we wouldn't inadvertantly proxy the javac-generated default methods\n        // too (and end up with a stack overflow from infinite recursion).\n        // As such, we try our hardest to \"make things work\" requiring requiring extra effort from\n        // the user.\n\n        Method defaultMethod = entry.getValue();\n        MethodHandle handle = null;\n        try {\n          handle =\n              superMethodHandle(\n                  SuperMethodSupport.METHOD_LOOKUP, defaultMethod, factory, userLookups);\n        } catch (ReflectiveOperationException e1) {\n          // If the user-specified lookup failed, try again w/ the private lookup hack.\n          // If _that_ doesn't work, try the below workaround.\n          if (allowPrivateLookupFallback\n              && SuperMethodSupport.METHOD_LOOKUP != SuperMethodLookup.PRIVATE_LOOKUP) {\n            try {\n              handle =\n                  superMethodHandle(\n                      SuperMethodLookup.PRIVATE_LOOKUP, defaultMethod, factory, userLookups);\n            } catch (ReflectiveOperationException e2) {\n              // ignored, use below workaround.\n            }\n          }\n        }\n\n        Supplier<String> failureMsg =\n            () ->\n                \"Unable to use non-public factory \"\n                    + factoryRawType.getName()\n                    + \". Please call\"\n                    + \" FactoryModuleBuilder.withLookups(MethodHandles.lookup()) (with a\"\n                    + \" lookups that has access to the factory), or make the factory\"\n                    + \" public.\";\n        if (handle != null) {\n          methodHandleBuilder.put(defaultMethod, handle);\n        } else if (!allowMethodHandleWorkaround) {\n          errors.addMessage(failureMsg.get());\n        } else {\n          boolean foundMatch = false;\n          for (Method otherMethod : otherMethods.get(defaultMethod.getName())) {\n            if (dataSoFar.containsKey(otherMethod) && isCompatible(defaultMethod, otherMethod)) {\n              if (foundMatch) {\n                errors.addMessage(failureMsg.get());\n                break;\n              } else {\n                assistDataBuilder.put(defaultMethod, dataSoFar.get(otherMethod));\n                foundMatch = true;\n              }\n            }\n          }\n          // We always expect to find at least one match, because we only deal with javac-generated\n          // default methods. If we ever allow user-specified default methods, this will need to\n          // change.\n          if (!foundMatch) {\n            throw new IllegalStateException(\"Can't find method compatible with: \" + defaultMethod);\n          }\n        }\n      }\n\n      // If we generated any errors (from finding matching constructors, for instance), throw an\n      // exception.\n      if (errors.hasErrors()) {\n        throw errors.toException();\n      }\n\n      assistDataByMethod = assistDataBuilder.build();\n      methodHandleByMethod = methodHandleBuilder.build();\n    } catch (ErrorsException e) {\n      throw new ConfigurationException(e.getErrors().getMessages());\n    }\n  }\n\n  static boolean isDefault(Method method) {\n    // Per the javadoc, default methods are non-abstract, public, non-static.\n    // They're also in interfaces, but we can guarantee that already since we only act\n    // on interfaces.\n    return (method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC))\n        == Modifier.PUBLIC;\n  }\n\n  private boolean isCompatible(Method src, Method dst) {\n    if (!src.getReturnType().isAssignableFrom(dst.getReturnType())) {\n      return false;\n    }\n    Class<?>[] srcParams = src.getParameterTypes();\n    Class<?>[] dstParams = dst.getParameterTypes();\n    if (srcParams.length != dstParams.length) {\n      return false;\n    }\n    for (int i = 0; i < srcParams.length; i++) {\n      if (!srcParams[i].isAssignableFrom(dstParams[i])) {\n        return false;\n      }\n    }\n    return true;\n  }\n\n  @Override\n  public F get() {\n    return factory;\n  }\n\n  @Override\n  public Set<Dependency<?>> getDependencies() {\n    Set<Dependency<?>> combinedDeps = new HashSet<>();\n    for (AssistData data : assistDataByMethod.values()) {\n      combinedDeps.addAll(data.dependencies);\n    }\n    return ImmutableSet.copyOf(combinedDeps);\n  }\n\n  @Override\n  public Key<F> getKey() {\n    return factoryKey;\n  }\n\n  // Safe cast because values are typed to AssistedData, which is an AssistedMethod, and\n  // the collection is immutable.\n  @Override\n  @SuppressWarnings(\"unchecked\")\n  public Collection<AssistedMethod> getAssistedMethods() {\n    return (Collection<AssistedMethod>) (Collection<?>) assistDataByMethod.values();\n  }\n\n  @Override\n  @SuppressWarnings(\"unchecked\")\n  public <T, V> V acceptExtensionVisitor(\n      BindingTargetVisitor<T, V> visitor, ProviderInstanceBinding<? extends T> binding) {\n    if (visitor instanceof AssistedInjectTargetVisitor) {\n      return ((AssistedInjectTargetVisitor<T, V>) visitor).visit((AssistedInjectBinding<T>) this);\n    }\n    return visitor.visit(binding);\n  }\n\n  private void validateFactoryReturnType(Errors errors, Class<?> returnType, Class<?> factoryType) {\n    if (Modifier.isPublic(factoryType.getModifiers())\n        && !Modifier.isPublic(returnType.getModifiers())) {\n      errors.addMessage(\n          \"%s is public, but has a method that returns a non-public type: %s. \"\n              + \"Due to limitations with java.lang.reflect.Proxy, this is not allowed. \"\n              + \"Please either make the factory non-public or the return type public.\",\n          factoryType, returnType);\n    }\n  }\n\n  /**\n   * Returns true if the ConfigurationException is due to an error of TypeLiteral not being fully\n   * specified.\n   */\n  private boolean isTypeNotSpecified(TypeLiteral<?> typeLiteral, ConfigurationException ce) {\n    Collection<Message> messages = ce.getErrorMessages();\n    if (messages.size() == 1) {\n      Message msg =\n          Iterables.getOnlyElement(new Errors().keyNotFullySpecified(typeLiteral).getMessages());\n      return msg.getMessage().equals(Iterables.getOnlyElement(messages).getMessage());\n    } else {\n      return false;\n    }\n  }\n\n  /**\n   * Finds a constructor suitable for the method. If the implementation contained any constructors\n   * marked with {@link AssistedInject}, this requires all {@link Assisted} parameters to exactly\n   * match the parameters (in any order) listed in the method. Otherwise, if no {@link\n   * AssistedInject} constructors exist, this will default to looking for an {@literal @}{@link\n   * Inject} constructor.\n   */\n  private <T> InjectionPoint findMatchingConstructorInjectionPoint(\n      Method method, Key<?> returnType, TypeLiteral<T> implementation, List<Key<?>> paramList)\n      throws ErrorsException {\n    Errors errors = new Errors(method);\n    if (returnType.getTypeLiteral().equals(implementation)) {\n      errors = errors.withSource(implementation);\n    } else {\n      errors = errors.withSource(returnType).withSource(implementation);\n    }\n\n    Class<?> rawType = implementation.getRawType();\n    if (Modifier.isInterface(rawType.getModifiers())) {\n      errors.addMessage(\n          \"%s is an interface, not a concrete class.  Unable to create AssistedInject factory.\",\n          implementation);\n      throw errors.toException();\n    } else if (Modifier.isAbstract(rawType.getModifiers())) {\n      errors.addMessage(\n          \"%s is abstract, not a concrete class.  Unable to create AssistedInject factory.\",\n          implementation);\n      throw errors.toException();\n    } else if (Classes.isInnerClass(rawType)) {\n      errors.cannotInjectInnerClass(rawType);\n      throw errors.toException();\n    }\n\n    Constructor<?> matchingConstructor = null;\n    boolean anyAssistedInjectConstructors = false;\n    // Look for AssistedInject constructors...\n    for (Constructor<?> constructor : rawType.getDeclaredConstructors()) {\n      if (constructor.isAnnotationPresent(AssistedInject.class)) {\n        anyAssistedInjectConstructors = true;\n        if (constructorHasMatchingParams(implementation, constructor, paramList, errors)) {\n          if (matchingConstructor != null) {\n            errors.addMessage(\n                \"%s has more than one constructor annotated with @AssistedInject\"\n                    + \" that matches the parameters in method %s.  Unable to create \"\n                    + \"AssistedInject factory.\",\n                implementation, method);\n            throw errors.toException();\n          } else {\n            matchingConstructor = constructor;\n          }\n        }\n      }\n    }\n\n    if (!anyAssistedInjectConstructors) {\n      // If none existed, use @Inject or a no-arg constructor.\n      try {\n        return InjectionPoint.forConstructorOf(implementation);\n      } catch (ConfigurationException e) {\n        errors.merge(e.getErrorMessages());\n        throw errors.toException();\n      }\n    } else {\n      // Otherwise, use it or fail with a good error message.\n      if (matchingConstructor != null) {\n        // safe because we got the constructor from this implementation.\n        @SuppressWarnings(\"unchecked\")\n        InjectionPoint ip =\n            InjectionPoint.forConstructor(\n                (Constructor<? super T>) matchingConstructor, implementation);\n        return ip;\n      } else {\n        errors.addMessage(\n            \"%s has @AssistedInject constructors, but none of them match the\"\n                + \" parameters in method %s.  Unable to create AssistedInject factory.\",\n            implementation, method);\n        throw errors.toException();\n      }\n    }\n  }\n\n  /**\n   * Matching logic for constructors annotated with AssistedInject. This returns true if and only if\n   * all @Assisted parameters in the constructor exactly match (in any order) all @Assisted\n   * parameters the method's parameter.\n   */\n  private boolean constructorHasMatchingParams(\n      TypeLiteral<?> type, Constructor<?> constructor, List<Key<?>> paramList, Errors errors)\n      throws ErrorsException {\n    List<TypeLiteral<?>> params = type.getParameterTypes(constructor);\n    Annotation[][] paramAnnotations = constructor.getParameterAnnotations();\n    int p = 0;\n    List<Key<?>> constructorKeys = Lists.newArrayList();\n    for (TypeLiteral<?> param : params) {\n      Key<?> paramKey = Annotations.getKey(param, constructor, paramAnnotations[p++], errors);\n      constructorKeys.add(paramKey);\n    }\n    // Require that every key exist in the constructor to match up exactly.\n    for (Key<?> key : paramList) {\n      // If it didn't exist in the constructor set, we can't use it.\n      if (!constructorKeys.remove(key)) {\n        return false;\n      }\n    }\n    // If any keys remain and their annotation is Assisted, we can't use it.\n    for (Key<?> key : constructorKeys) {\n      if (key.getAnnotationType() == Assisted.class) {\n        return false;\n      }\n    }\n    // All @Assisted params match up to the method's parameters.\n    return true;\n  }\n\n  /** Calculates all dependencies required by the implementation and constructor. */\n  private Set<Dependency<?>> getDependencies(\n      InjectionPoint ctorPoint, TypeLiteral<?> implementation) {\n    ImmutableSet.Builder<Dependency<?>> builder = ImmutableSet.builder();\n    builder.addAll(ctorPoint.getDependencies());\n    if (!implementation.getRawType().isInterface()) {\n      for (InjectionPoint ip : InjectionPoint.forInstanceMethodsAndFields(implementation)) {\n        builder.addAll(ip.getDependencies());\n      }\n    }\n    return builder.build();\n  }\n\n  /** Return all non-assisted dependencies. */\n  private Set<Dependency<?>> removeAssistedDeps(Set<Dependency<?>> deps) {\n    ImmutableSet.Builder<Dependency<?>> builder = ImmutableSet.builder();\n    for (Dependency<?> dep : deps) {\n      Class<?> annotationType = dep.getKey().getAnnotationType();\n      if (annotationType == null || !annotationType.equals(Assisted.class)) {\n        builder.add(dep);\n      }\n    }\n    return builder.build();\n  }\n\n  /**\n   * Returns true if all dependencies are suitable for the optimized version of AssistedInject. The\n   * optimized version caches the binding and uses a ThreadLocal Provider, so can only be applied if\n   * the assisted bindings are immediately provided. This looks for hints that the values may be\n   * lazily retrieved, by looking for injections of Injector or a Provider for the assisted values.\n   */\n  private boolean isValidForOptimizedAssistedInject(\n      Set<Dependency<?>> dependencies, Class<?> implementation, TypeLiteral<?> factoryType) {\n    Set<Dependency<?>> badDeps = null; // optimization: create lazily\n    for (Dependency<?> dep : dependencies) {\n      if (isInjectorOrAssistedProvider(dep)) {\n        if (badDeps == null) {\n          badDeps = Sets.newHashSet();\n        }\n        badDeps.add(dep);\n      }\n    }\n    if (badDeps != null && !badDeps.isEmpty()) {\n      logger.log(\n          Level.WARNING,\n          \"AssistedInject factory {0} will be slow \"\n              + \"because {1} has assisted Provider dependencies or injects the Injector. \"\n              + \"Stop injecting @Assisted Provider<T> (instead use @Assisted T) \"\n              + \"or Injector to speed things up. (It will be a ~6500% speed bump!)  \"\n              + \"The exact offending deps are: {2}\",\n          new Object[] {factoryType, implementation, badDeps});\n      return false;\n    }\n    return true;\n  }\n\n  /**\n   * Returns true if the dependency is for {@link Injector} or if the dependency is a {@link\n   * Provider} for a parameter that is {@literal @}{@link Assisted}.\n   */\n  private boolean isInjectorOrAssistedProvider(Dependency<?> dependency) {\n    Class<?> annotationType = dependency.getKey().getAnnotationType();\n    if (annotationType != null && annotationType.equals(Assisted.class)) { // If it's assisted..\n      if (dependency\n          .getKey()\n          .getTypeLiteral()\n          .getRawType()\n          .equals(Provider.class)) { // And a Provider...\n        return true;\n      }\n    } else if (dependency\n        .getKey()\n        .getTypeLiteral()\n        .getRawType()\n        .equals(Injector.class)) { // If it's the Injector...\n      return true;\n    }\n    return false;\n  }\n\n  /**\n   * Returns a key similar to {@code key}, but with an {@literal @}Assisted binding annotation. This\n   * fails if another binding annotation is clobbered in the process. If the key already has the\n   * {@literal @}Assisted annotation, it is returned as-is to preserve any String value.\n   */\n  private <T> Key<T> assistKey(Method method, Key<T> key, Errors errors) throws ErrorsException {\n    if (key.getAnnotationType() == null) {\n      return key.withAnnotation(DEFAULT_ANNOTATION);\n    } else if (key.getAnnotationType() == Assisted.class) {\n      return key;\n    } else {\n      errors\n          .withSource(method)\n          .addMessage(\n              \"Only @Assisted is allowed for factory parameters, but found @%s\",\n              key.getAnnotationType());\n      throw errors.toException();\n    }\n  }\n\n  /**\n   * At injector-creation time, we initialize the invocation handler. At this time we make sure all\n   * factory methods will be able to build the target types.\n   */\n  @Inject\n  @Toolable\n  void initialize(Injector injector) {\n    if (this.injector != null) {\n      throw new ConfigurationException(\n          ImmutableList.of(\n              new Message(\n                  FactoryProvider3.class,\n                  \"Factories.create() factories may only be used in one Injector!\")));\n    }\n\n    this.injector = injector;\n\n    for (Map.Entry<Method, AssistData> entry : assistDataByMethod.entrySet()) {\n      Method method = entry.getKey();\n      AssistData data = entry.getValue();\n      Object[] args;\n      if (!data.optimized) {\n        args = new Object[method.getParameterTypes().length];\n        Arrays.fill(args, \"dummy object for validating Factories\");\n      } else {\n        args = null; // won't be used -- instead will bind to data.providers.\n      }\n      getBindingFromNewInjector(\n          method, args, data); // throws if the binding isn't properly configured\n    }\n  }\n\n  /**\n   * Creates a child injector that binds the args, and returns the binding for the method's result.\n   */\n  public Binding<?> getBindingFromNewInjector(\n      final Method method, final Object[] args, final AssistData data) {\n    checkState(\n        injector != null,\n        \"Factories.create() factories cannot be used until they're initialized by Guice.\");\n\n    final Key<?> returnType = data.returnType;\n\n    // We ignore any pre-existing binding annotation.\n    final Key<?> returnKey = Key.get(returnType.getTypeLiteral(), RETURN_ANNOTATION);\n\n    Module assistedModule =\n        new AbstractModule() {\n          @Override\n          @SuppressWarnings({\n            \"unchecked\",\n            \"rawtypes\"\n          }) // raw keys are necessary for the args array and return value\n          protected void configure() {\n            Binder binder = binder().withSource(method);\n\n            int p = 0;\n            if (!data.optimized) {\n              for (Key<?> paramKey : data.paramTypes) {\n                // Wrap in a Provider to cover null, and to prevent Guice from injecting the\n                // parameter\n                binder.bind((Key) paramKey).toProvider(Providers.of(args[p++]));\n              }\n            } else {\n              for (Key<?> paramKey : data.paramTypes) {\n                // Bind to our ThreadLocalProviders.\n                binder.bind((Key) paramKey).toProvider(data.providers.get(p++));\n              }\n            }\n\n            Constructor constructor = data.constructor;\n            // Constructor *should* always be non-null here,\n            // but if it isn't, we'll end up throwing a fairly good error\n            // message for the user.\n            if (constructor != null) {\n              binder\n                  .bind(returnKey)\n                  .toConstructor(constructor, (TypeLiteral) data.implementationType)\n                  .in(Scopes.NO_SCOPE); // make sure we erase any scope on the implementation type\n            }\n          }\n        };\n\n    Injector forCreate = injector.createChildInjector(assistedModule);\n    Binding<?> binding = forCreate.getBinding(returnKey);\n    // If we have providers cached in data, cache the binding for future optimizations.\n    if (data.optimized) {\n      data.cachedBinding = binding;\n    }\n    return binding;\n  }\n\n  /**\n   * When a factory method is invoked, we create a child injector that binds all parameters, then\n   * use that to get an instance of the return type.\n   */\n  @Override\n  public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable {\n    // If we setup a method handle earlier for this method, call it.\n    // This is necessary for default methods that java8 creates, so we\n    // can call the default method implementation (and not our proxied version of it).\n    if (methodHandleByMethod.containsKey(method)) {\n      return methodHandleByMethod.get(method).invokeWithArguments(args);\n    }\n\n    if (method.getDeclaringClass().equals(Object.class)) {\n      if (\"equals\".equals(method.getName())) {\n        return proxy == args[0];\n      } else if (\"hashCode\".equals(method.getName())) {\n        return System.identityHashCode(proxy);\n      } else {\n        return method.invoke(this, args);\n      }\n    }\n\n    AssistData data = assistDataByMethod.get(method);\n    checkState(data != null, \"No data for method: %s\", method);\n    Provider<?> provider;\n    if (data.cachedBinding != null) { // Try to get optimized form...\n      provider = data.cachedBinding.getProvider();\n    } else {\n      provider = getBindingFromNewInjector(method, args, data).getProvider();\n    }\n    try {\n      int p = 0;\n      for (ThreadLocalProvider tlp : data.providers) {\n        tlp.set(args[p++]);\n      }\n      return provider.get();\n    } catch (ProvisionException e) {\n      // if this is an exception declared by the factory method, throw it as-is\n      if (e.getErrorMessages().size() == 1) {\n        Message onlyError = getOnlyElement(e.getErrorMessages());\n        Throwable cause = onlyError.getCause();\n        if (cause != null && canRethrow(method, cause)) {\n          throw cause;\n        }\n      }\n      throw e;\n    } finally {\n      for (ThreadLocalProvider tlp : data.providers) {\n        tlp.remove();\n      }\n    }\n  }\n\n  @Override\n  public String toString() {\n    return factory.getClass().getInterfaces()[0].getName();\n  }\n\n  @Override\n  public int hashCode() {\n    return Objects.hashCode(factoryKey, collector);\n  }\n\n  @Override\n  public boolean equals(Object obj) {\n    if (!(obj instanceof FactoryProvider3)) {\n      return false;\n    }\n    FactoryProvider3<?> other = (FactoryProvider3<?>) obj;\n    return factoryKey.equals(other.factoryKey) && Objects.equal(collector, other.collector);\n  }\n\n  /** Returns true if {@code thrown} can be thrown by {@code invoked} without wrapping. */\n  static boolean canRethrow(Method invoked, Throwable thrown) {\n    if (thrown instanceof Error || thrown instanceof RuntimeException) {\n      return true;\n    }\n\n    for (Class<?> declared : invoked.getExceptionTypes()) {\n      if (declared.isInstance(thrown)) {\n        return true;\n      }\n    }\n\n    return false;\n  }\n\n  // not <T> because we'll never know and this is easier than suppressing warnings.\n  private static class ThreadLocalProvider extends ThreadLocal<Object> implements Provider<Object> {\n    @Override\n    protected Object initialValue() {\n      throw new IllegalStateException(\n          \"Cannot use optimized @Assisted provider outside the scope of the constructor.\"\n              + \" (This should never happen.  If it does, please report it.)\");\n    }\n  }\n\n  /**\n   * Holder for the appropriate kind of method lookup to use. Due to bugs in Java releases, we have\n   * to evaluate what approach to take at runtime. We do this by emulating the buggy scenarios: can\n   * a lookup access private details that it should be able to see? If not, we fail down to using\n   * full private access. Unfortunately, private access doesn't work in the JDK17+.... but it\n   * shouldn't be necessary there either, because the buggy lookup checks should be fixed.\n   */\n  private static class SuperMethodSupport {\n    private static final SuperMethodLookup METHOD_LOOKUP;\n\n    static {\n      SuperMethodLookup workingLookup = null;\n      try {\n        Class<?> hidden =\n            Class.forName(\"com.google.inject.assistedinject.internal.LookupTester$Hidden\");\n        Method method = hidden.getMethod(\"method\");\n        Field lookupsField = hidden.getEnclosingClass().getDeclaredField(\"LOOKUP\");\n        lookupsField.setAccessible(true);\n        MethodHandles.Lookup lookups = (MethodHandles.Lookup) lookupsField.get(null);\n        for (SuperMethodLookup attempt : SuperMethodLookup.values()) {\n          try {\n            attempt.superMethodHandle(method, lookups);\n            workingLookup = attempt;\n            break;\n          } catch (ReflectiveOperationException ignored) {\n            // Keep looping to find a working lookup\n          }\n        }\n      } catch (ReflectiveOperationException ignored) {\n        // Bail if our internal tests don't exist.\n      }\n      // If everything failed, use the worst option.\n      if (workingLookup == null) {\n        workingLookup = SuperMethodLookup.PRIVATE_LOOKUP;\n      }\n      METHOD_LOOKUP = workingLookup;\n    }\n  }\n\n  private static MethodHandle superMethodHandle(\n      SuperMethodLookup strategy, Method method, Object proxy, MethodHandles.Lookup userLookups)\n      throws ReflectiveOperationException {\n    MethodHandles.Lookup lookup = userLookups == null ? MethodHandles.lookup() : userLookups;\n    MethodHandle handle = strategy.superMethodHandle(method, lookup);\n    return handle != null ? handle.bindTo(proxy) : null;\n  }\n\n  private static enum SuperMethodLookup {\n    UNREFLECT_SPECIAL {\n      @Override\n      MethodHandle superMethodHandle(Method method, MethodHandles.Lookup lookup)\n          throws ReflectiveOperationException {\n        return lookup.unreflectSpecial(method, method.getDeclaringClass());\n      }\n    },\n    FIND_SPECIAL {\n      @Override\n      MethodHandle superMethodHandle(Method method, MethodHandles.Lookup lookup)\n          throws ReflectiveOperationException {\n        Class<?> declaringClass = method.getDeclaringClass();\n        // Before JDK14, unreflectSpecial didn't work in some scenarios.\n        // So we workaround using findSpecial. See: https://bugs.openjdk.java.net/browse/JDK-8209005\n        return lookup.findSpecial(\n            declaringClass,\n            method.getName(),\n            MethodType.methodType(method.getReturnType(), method.getParameterTypes()),\n            declaringClass);\n      }\n    },\n    PRIVATE_LOOKUP {\n      @Override\n      MethodHandle superMethodHandle(Method method, MethodHandles.Lookup unused)\n          throws ReflectiveOperationException {\n        // Even findSpecial fails on JDK8, so we need to manually reflect on private details.\n        // But note that this will fail 100% of the time on JDK17+, which doesn't allow reflection\n        // into the JDK internals.\n        return PrivateLookup.superMethodHandle(method);\n      }\n    };\n\n    abstract MethodHandle superMethodHandle(Method method, MethodHandles.Lookup lookup)\n        throws ReflectiveOperationException;\n  };\n\n  // Note: this isn't a public API, but we need to use it in order to call default methods on (or\n  // with) non-public types. If it doesn't exist, the code falls back to a less precise check.\n  static class PrivateLookup {\n    PrivateLookup() {}\n\n    private static final int ALL_MODES =\n        Modifier.PRIVATE | Modifier.STATIC /* package */ | Modifier.PUBLIC | Modifier.PROTECTED;\n\n    private static final Constructor<MethodHandles.Lookup> privateLookupCxtor =\n        findPrivateLookupCxtor();\n\n    private static Constructor<MethodHandles.Lookup> findPrivateLookupCxtor() {\n      try {\n        Constructor<MethodHandles.Lookup> cxtor;\n        try {\n          cxtor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);\n        } catch (NoSuchMethodException ignored) {\n          cxtor =\n              MethodHandles.Lookup.class.getDeclaredConstructor(\n                  Class.class, Class.class, int.class);\n        }\n        cxtor.setAccessible(true);\n        return cxtor;\n      } catch (Exception e) {\n        // Note: we catch Exception because we want to handle InaccessibleObjectException too,\n        // but we target JDK8.\n        // TODO(sameb): When we drop JDK8 support, catch ReflectiveOperation|Security|Inaccessible\n        return null;\n      }\n    }\n\n    static MethodHandle superMethodHandle(Method method) throws ReflectiveOperationException {\n      if (privateLookupCxtor == null) {\n        return null; // fall back to assistDataBuilder workaround\n      }\n      Class<?> declaringClass = method.getDeclaringClass();\n      MethodHandles.Lookup lookup;\n      if (privateLookupCxtor.getParameterCount() == 2) {\n        lookup = privateLookupCxtor.newInstance(declaringClass, ALL_MODES);\n      } else {\n        lookup = privateLookupCxtor.newInstance(declaringClass, null, ALL_MODES);\n      }\n      return lookup.unreflectSpecial(method, declaringClass);\n    }\n  }\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/CarbonChatInternal.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common;\n\nimport com.google.inject.Injector;\nimport com.google.inject.Key;\nimport com.google.inject.Provider;\nimport com.google.inject.TypeLiteral;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.ExecutionCoordinatorHolder;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.Integration;\nimport net.draycia.carbon.common.listeners.Listener;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.messaging.packets.PacketFactory;\nimport net.draycia.carbon.common.users.PlayerUtils;\nimport net.draycia.carbon.common.users.ProfileCache;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport net.draycia.carbon.common.util.CloudUtils;\nimport net.draycia.carbon.common.util.ConcurrentUtil;\nimport net.draycia.carbon.common.util.UpdateChecker;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic abstract class CarbonChatInternal implements CarbonChat {\n\n    private final Injector injector;\n    private final Logger logger;\n    private final ScheduledExecutorService periodicTasks;\n    private final ProfileCache profileCache;\n    private final ProfileResolver profileResolver;\n    private final UserManagerInternal<?> userManager;\n    private final ExecutionCoordinatorHolder commandExecutor;\n    private final CarbonServer carbonServer;\n    private final CarbonMessages carbonMessages;\n    private final CarbonEventHandler eventHandler;\n    private final CarbonChannelRegistry channelRegistry;\n    private final Provider<MessagingManager> messagingManager;\n\n    protected CarbonChatInternal(\n        final Injector injector,\n        final Logger logger,\n        final ScheduledExecutorService periodicTasks,\n        final ProfileCache profileCache,\n        final ProfileResolver profileResolver,\n        final UserManagerInternal<?> userManager,\n        final ExecutionCoordinatorHolder commandExecutor,\n        final CarbonServer carbonServer,\n        final CarbonMessages carbonMessages,\n        final CarbonEventHandler eventHandler,\n        final CarbonChannelRegistry channelRegistry,\n        final Provider<MessagingManager> messagingManagerProvider\n    ) {\n        this.injector = injector;\n        this.logger = logger;\n        this.periodicTasks = periodicTasks;\n        this.profileCache = profileCache;\n        this.profileResolver = profileResolver;\n        this.userManager = userManager;\n        this.commandExecutor = commandExecutor;\n        this.carbonServer = carbonServer;\n        this.carbonMessages = carbonMessages;\n        this.eventHandler = eventHandler;\n        this.channelRegistry = channelRegistry;\n        this.messagingManager = messagingManagerProvider;\n    }\n\n    protected void init() {\n        // Listeners\n        final Set<Listener> listeners = this.injector.getInstance(Key.get(new TypeLiteral<Set<Listener>>() {}));\n\n        // Commands\n        // This is a bit awkward looking\n        final Set<CarbonCommand> commands = this.injector.getInstance(Key.get(new TypeLiteral<Set<CarbonCommand>>() {}));\n        CloudUtils.registerCommands(commands, this.injector.getInstance(ConfigManager.class).loadCommandSettings());\n\n        this.periodicTasks.scheduleAtFixedRate(\n            () -> PlayerUtils.saveLoggedInPlayers(this.carbonServer, this.userManager, this.logger),\n            5,\n            5,\n            TimeUnit.MINUTES\n        );\n        this.periodicTasks.scheduleAtFixedRate(\n            this.profileCache::save,\n            15,\n            15,\n            TimeUnit.MINUTES\n        );\n        this.periodicTasks.scheduleAtFixedRate(\n            this.userManager::cleanup,\n            30,\n            30,\n            TimeUnit.SECONDS\n        );\n\n        this.initIntegrations();\n\n        // Load channels\n        this.channelRegistry().loadConfigChannels(this.carbonMessages);\n\n        this.messagingManager.get();\n    }\n\n    protected void initIntegrations() {\n        // Integration\n        final Set<Integration> integrations = this.injector().getInstance(Key.get(new TypeLiteral<>() {}));\n\n        for (final Integration integration : integrations) {\n            if (!integration.eligible()) {\n                continue;\n            }\n\n            integration.register();\n        }\n    }\n\n    protected final void checkVersion() {\n        if (!this.injector.getInstance(ConfigManager.class).primaryConfig().updateChecker()) {\n            return;\n        }\n        CompletableFuture.runAsync(() -> new UpdateChecker(this.logger).checkVersion()).whenComplete(($, thr) -> {\n            if (thr != null) {\n                this.logger.warn(\"Exception fetching version information\", thr);\n            }\n        });\n    }\n\n    protected void shutdown() {\n        this.messagingManager.get().queuePacket(() -> this.injector.getInstance(PacketFactory.class).clearLocalPlayersPacket());\n        this.messagingManager.get().onShutdown();\n        ConcurrentUtil.shutdownExecutor(this.periodicTasks, TimeUnit.MILLISECONDS, 500);\n        this.profileCache.save();\n        this.profileResolver.shutdown();\n        this.userManager.shutdown();\n        this.commandExecutor.shutdown();\n    }\n\n    public Logger logger() {\n        return this.logger;\n    }\n\n    @Override\n    public CarbonServer server() {\n        return this.carbonServer;\n    }\n\n    @Override\n    public UserManager<?> userManager() {\n        return this.userManager;\n    }\n\n    @Override\n    public CarbonEventHandler eventHandler() {\n        return this.eventHandler;\n    }\n\n    @Override\n    public CarbonChannelRegistry channelRegistry() {\n        return this.channelRegistry;\n    }\n\n    public boolean isProxy() {\n        return false;\n    }\n\n    public Injector injector() {\n        return this.injector;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/CarbonCommonModule.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common;\n\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Injector;\nimport com.google.inject.Module;\nimport com.google.inject.Provider;\nimport com.google.inject.Provides;\nimport com.google.inject.Scopes;\nimport com.google.inject.Singleton;\nimport com.google.inject.TypeLiteral;\nimport com.google.inject.assistedinject.FactoryModuleBuilder;\nimport com.google.inject.assistedinject.FactoryProvider3;\nimport com.google.inject.multibindings.Multibinder;\nimport io.leangen.geantyref.TypeToken;\nimport java.io.IOException;\nimport java.lang.invoke.MethodHandles;\nimport java.nio.file.Path;\nimport java.util.UUID;\nimport java.util.concurrent.ScheduledExecutorService;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.ExecutionCoordinatorHolder;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.command.argument.PlayerSuggestions;\nimport net.draycia.carbon.common.command.commands.ClearChatCommand;\nimport net.draycia.carbon.common.command.commands.ContinueCommand;\nimport net.draycia.carbon.common.command.commands.DebugCommand;\nimport net.draycia.carbon.common.command.commands.FilterCommand;\nimport net.draycia.carbon.common.command.commands.HelpCommand;\nimport net.draycia.carbon.common.command.commands.IgnoreCommand;\nimport net.draycia.carbon.common.command.commands.IgnoreListCommand;\nimport net.draycia.carbon.common.command.commands.JoinCommand;\nimport net.draycia.carbon.common.command.commands.LeaveCommand;\nimport net.draycia.carbon.common.command.commands.MuteCommand;\nimport net.draycia.carbon.common.command.commands.MuteInfoCommand;\nimport net.draycia.carbon.common.command.commands.NicknameCommand;\nimport net.draycia.carbon.common.command.commands.PartyCommands;\nimport net.draycia.carbon.common.command.commands.RealNameCommand;\nimport net.draycia.carbon.common.command.commands.ReloadCommand;\nimport net.draycia.carbon.common.command.commands.ReplyCommand;\nimport net.draycia.carbon.common.command.commands.SpyCommand;\nimport net.draycia.carbon.common.command.commands.ToggleMessagesCommand;\nimport net.draycia.carbon.common.command.commands.UnignoreCommand;\nimport net.draycia.carbon.common.command.commands.UnmuteCommand;\nimport net.draycia.carbon.common.command.commands.UpdateUsernameCommand;\nimport net.draycia.carbon.common.command.commands.WhisperCommand;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.config.DatabaseSettings;\nimport net.draycia.carbon.common.config.PrimaryConfig;\nimport net.draycia.carbon.common.event.CarbonEventHandlerImpl;\nimport net.draycia.carbon.common.listeners.DeafenHandler;\nimport net.draycia.carbon.common.listeners.FilterHandler;\nimport net.draycia.carbon.common.listeners.HyperlinkHandler;\nimport net.draycia.carbon.common.listeners.IgnoreHandler;\nimport net.draycia.carbon.common.listeners.ItemLinkHandler;\nimport net.draycia.carbon.common.listeners.Listener;\nimport net.draycia.carbon.common.listeners.MessagePacketHandler;\nimport net.draycia.carbon.common.listeners.MuteHandler;\nimport net.draycia.carbon.common.listeners.PartyChatSpyHandler;\nimport net.draycia.carbon.common.listeners.PartyPingHandler;\nimport net.draycia.carbon.common.listeners.PingHandler;\nimport net.draycia.carbon.common.listeners.RadiusListener;\nimport net.draycia.carbon.common.messages.CarbonMessageRenderer;\nimport net.draycia.carbon.common.messages.CarbonMessageSender;\nimport net.draycia.carbon.common.messages.CarbonMessageSource;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messages.Option;\nimport net.draycia.carbon.common.messages.RenderForTagResolver;\nimport net.draycia.carbon.common.messages.SourcedReceiverResolver;\nimport net.draycia.carbon.common.messages.StandardPlaceholderResolverStrategyButDifferent;\nimport net.draycia.carbon.common.messages.placeholders.BooleanPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.ComponentPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.IntPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.KeyPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.LongPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.OptionPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.StringPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.UUIDPlaceholderResolver;\nimport net.draycia.carbon.common.messaging.ServerId;\nimport net.draycia.carbon.common.messaging.packets.PacketFactory;\nimport net.draycia.carbon.common.users.Backing;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.users.NetworkUsers;\nimport net.draycia.carbon.common.users.PlatformUserManager;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport net.draycia.carbon.common.users.db.DatabaseUserManager;\nimport net.draycia.carbon.common.users.db.argument.BinaryUUIDArgumentFactory;\nimport net.draycia.carbon.common.users.db.mapper.BinaryUUIDColumnMapper;\nimport net.draycia.carbon.common.users.db.mapper.NativeUUIDColumnMapper;\nimport net.draycia.carbon.common.users.json.JSONUserManager;\nimport net.draycia.carbon.common.util.CloudUtils;\nimport net.draycia.carbon.common.util.ConcurrentUtil;\nimport net.draycia.carbon.common.util.Exceptions;\nimport net.draycia.carbon.common.util.FileUtil;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.moonshine.Moonshine;\nimport net.kyori.moonshine.exception.scan.UnscannableMethodException;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jdbi.v3.core.h2.H2DatabasePlugin;\nimport org.jdbi.v3.postgres.PostgresPlugin;\nimport org.spongepowered.configurate.util.NamingSchemes;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonCommonModule extends AbstractModule {\n\n    @Provides\n    @Backing\n    @Singleton\n    public UserManagerInternal<CarbonPlayerCommon> userManager(\n        final @DataDirectory Path dataDirectory,\n        final ConfigManager configManager,\n        final Logger logger,\n        final Injector injector\n    ) throws IOException {\n        PrimaryConfig.StorageType storageType = configManager.primaryConfig().storageType();\n\n        final String smokeTestMode = System.getProperty(\"carbonchat.smokeTestMode\");\n        if (smokeTestMode != null) {\n            logger.info(\"Smoke test: Overriding storage manager.\");\n            switch (smokeTestMode) {\n                case \"h2\" -> storageType = PrimaryConfig.StorageType.H2;\n                case \"json\" -> storageType = PrimaryConfig.StorageType.JSON;\n                case \"postgres\" -> {\n                    storageType = PrimaryConfig.StorageType.PSQL;\n                    configManager.primaryConfig().databaseSettings().url(\"jdbc:postgresql://localhost:5432/carbon\");\n                }\n                case \"mariadb\" -> {\n                    storageType = PrimaryConfig.StorageType.MYSQL;\n                    configManager.primaryConfig().databaseSettings().url(\"jdbc:mariadb://localhost:3306/carbon\");\n                }\n                default -> throw new IllegalArgumentException(\"Unknown smoke test mode: \" + smokeTestMode);\n            }\n        }\n\n        logger.info(\"Initializing {} storage manager...\", storageType);\n\n        return switch (storageType) {\n            case MYSQL -> injector.getInstance(DatabaseUserManager.Factory.class).create(\n                \"queries/migrations/mysql\",\n                jdbi -> jdbi.registerArgument(new BinaryUUIDArgumentFactory())\n                    .registerColumnMapper(UUID.class, new BinaryUUIDColumnMapper())\n            );\n            case PSQL -> injector.getInstance(DatabaseUserManager.Factory.class).create(\n                \"queries/migrations/postgresql\",\n                jdbi -> jdbi.registerColumnMapper(UUID.class, new NativeUUIDColumnMapper())\n                    .installPlugin(new PostgresPlugin())\n            );\n            case H2 -> injector.getInstance(DatabaseUserManager.Factory.class).create(\n                \"queries/migrations/h2\",\n                jdbi -> jdbi.installPlugin(new H2DatabasePlugin()),\n                new DatabaseSettings(\"jdbc:h2:\" + FileUtil.mkParentDirs(dataDirectory.resolve(\"users/userdata-h2\")).toAbsolutePath() + \";MODE=MySQL\", \"\", \"\")\n            );\n            case JSON -> injector.getInstance(JSONUserManager.class);\n        };\n    }\n\n    @Provides\n    @PeriodicTasks\n    @Singleton\n    public ScheduledExecutorService periodicTasksExecutor(final Logger logger) {\n        return ConcurrentUtil.createPeriodicTasksPool(logger);\n    }\n\n    @Provides\n    @Singleton\n    public CarbonMessages carbonMessages(\n        final SourcedReceiverResolver receiverResolver,\n        final ComponentPlaceholderResolver<Audience> componentPlaceholderResolver,\n        final UUIDPlaceholderResolver<Audience> uuidPlaceholderResolver,\n        final StringPlaceholderResolver<Audience> stringPlaceholderResolver,\n        final IntPlaceholderResolver<Audience> intPlaceholderResolver,\n        final LongPlaceholderResolver<Audience> longPlaceholderResolver,\n        final KeyPlaceholderResolver<Audience> keyPlaceholderResolver,\n        final BooleanPlaceholderResolver<Audience> booleanPlaceholderResolver,\n        final CarbonMessageSource carbonMessageSource,\n        final CarbonMessageSender carbonMessageSender,\n        final CarbonMessageRenderer messageRenderer\n    ) throws UnscannableMethodException {\n        return Moonshine.<CarbonMessages, Audience>builder(new TypeToken<>() {})\n            .receiverLocatorResolver(receiverResolver, 0)\n            .sourced(carbonMessageSource)\n            .rendered(messageRenderer)\n            .sent(carbonMessageSender)\n            .resolvingWithStrategy(new StandardPlaceholderResolverStrategyButDifferent<>(NamingSchemes.SNAKE_CASE))\n            .weightedPlaceholderResolver(Component.class, componentPlaceholderResolver, 0)\n            .weightedPlaceholderResolver(UUID.class, uuidPlaceholderResolver, 0)\n            .weightedPlaceholderResolver(String.class, stringPlaceholderResolver, 0)\n            .weightedPlaceholderResolver(Integer.class, intPlaceholderResolver, 0)\n            .weightedPlaceholderResolver(Long.class, longPlaceholderResolver, 0)\n            .weightedPlaceholderResolver(Key.class, keyPlaceholderResolver, 0)\n            .weightedPlaceholderResolver(Boolean.class, booleanPlaceholderResolver, 0)\n            .weightedPlaceholderResolver(Option.class, new OptionPlaceholderResolver<>(), 0)\n            .create(this.getClass().getClassLoader());\n    }\n\n    @Provides\n    @Singleton\n    public ExecutionCoordinatorHolder executionCoordinatorHolder(final Logger logger) {\n        return ExecutionCoordinatorHolder.create(logger);\n    }\n\n    @Override\n    protected void configure() {\n        this.install(new FactoryModuleBuilder().build(ParserFactory.class));\n        this.install(new FactoryModuleBuilder().build(RenderForTagResolver.Factory.class));\n        this.install(factoryModule(PacketFactory.class));\n        this.bind(ServerId.KEY).toInstance(UUID.randomUUID());\n        this.bind(ChannelRegistry.class).to(CarbonChannelRegistry.class);\n        this.bind(CarbonEventHandler.class).to(CarbonEventHandlerImpl.class);\n        this.bind(PlayerSuggestions.class).to(NetworkUsers.class);\n        this.bind(new TypeLiteral<UserManager<?>>() {}).to(PlatformUserManager.class);\n        this.bind(new TypeLiteral<UserManagerInternal<?>>() {}).to(PlatformUserManager.class);\n\n        this.configureListeners();\n        this.configureCommands();\n    }\n\n    private void configureListeners() {\n        final Multibinder<Listener> listeners = Multibinder.newSetBinder(this.binder(), Listener.class);\n        listeners.addBinding().to(DeafenHandler.class);\n        listeners.addBinding().to(FilterHandler.class);\n        listeners.addBinding().to(HyperlinkHandler.class);\n        listeners.addBinding().to(IgnoreHandler.class);\n        listeners.addBinding().to(ItemLinkHandler.class);\n        listeners.addBinding().to(MessagePacketHandler.class);\n        listeners.addBinding().to(MuteHandler.class);\n        listeners.addBinding().to(PartyChatSpyHandler.class);\n        listeners.addBinding().to(PartyPingHandler.class);\n        listeners.addBinding().to(PingHandler.class);\n        listeners.addBinding().to(RadiusListener.class);\n    }\n\n    private void configureCommands() {\n        this.requestStaticInjection(CloudUtils.class);\n\n        final Multibinder<CarbonCommand> commands = Multibinder.newSetBinder(this.binder(), CarbonCommand.class);\n        commands.addBinding().to(ClearChatCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(ContinueCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(DebugCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(FilterCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(HelpCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(IgnoreCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(MuteCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(MuteInfoCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(NicknameCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(ReloadCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(RealNameCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(ReplyCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(SpyCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(ToggleMessagesCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(UnignoreCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(UnmuteCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(UpdateUsernameCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(WhisperCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(JoinCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(LeaveCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(IgnoreListCommand.class).in(Scopes.SINGLETON);\n        commands.addBinding().to(PartyCommands.class).in(Scopes.SINGLETON);\n    }\n\n    // Helper to create a FactoryProvider3 module\n    private static <T> Module factoryModule(final Class<T> factoryInterface) {\n        return new AbstractModule() {\n            @Override\n            protected void configure() {\n                try {\n                    final Provider<T> factoryProvider = new FactoryProvider3<>(\n                        com.google.inject.Key.get(TypeLiteral.get(factoryInterface)),\n                        null,\n                        MethodHandles.privateLookupIn(factoryInterface, MethodHandles.lookup())\n                    );\n                    this.binder().bind(factoryInterface).toProvider(factoryProvider);\n                } catch (final Exception e) {\n                    throw Exceptions.rethrow(e);\n                }\n            }\n        };\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/CarbonPlatformModule.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common;\n\nimport com.google.inject.AbstractModule;\nimport com.google.inject.multibindings.Multibinder;\nimport net.draycia.carbon.common.integration.Integration;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersIntegration;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic abstract class CarbonPlatformModule extends AbstractModule {\n\n    @Override\n    protected final void configure() {\n        this.configurePlatform();\n\n        this.configureIntegrations(\n            Multibinder.newSetBinder(this.binder(), Integration.class),\n            Multibinder.newSetBinder(this.binder(), Integration.ConfigMeta.class)\n        );\n    }\n\n    protected abstract void configurePlatform();\n\n    protected void configureIntegrations(\n        final Multibinder<Integration> integrations,\n        final Multibinder<Integration.ConfigMeta> configs\n    ) {\n        integrations.addBinding().to(MiniPlaceholdersIntegration.class);\n        configs.addBinding().toInstance(MiniPlaceholdersIntegration.configMeta());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/DataDirectory.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common;\n\nimport com.google.inject.BindingAnnotation;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.nio.file.Path;\n\n/**\n * Injection binding annotation for the {@link Path} which is Carbon's data directory.\n */\n@BindingAnnotation\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.PARAMETER, ElementType.FIELD})\npublic @interface DataDirectory {\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/PeriodicTasks.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common;\n\nimport com.google.inject.BindingAnnotation;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.util.concurrent.ScheduledExecutorService;\n\n/**\n * Injection binding annotation for the {@link ScheduledExecutorService} used for\n * periodic tasks.\n */\n@BindingAnnotation\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})\npublic @interface PeriodicTasks {\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/PlatformScheduler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic interface PlatformScheduler {\n\n    void scheduleForPlayer(CarbonPlayer carbonPlayer, Runnable runnable);\n\n    @Singleton\n    final class RunImmediately implements PlatformScheduler {\n        @Inject\n        private RunImmediately() {\n        }\n\n        @Override\n        public void scheduleForPlayer(final CarbonPlayer carbonPlayer, final Runnable runnable) {\n            runnable.run();\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/RawChat.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common;\n\nimport com.google.inject.BindingAnnotation;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Injection binding annotation for the raw chat type key.\n */\n@BindingAnnotation\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})\npublic @interface RawChat {\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/channels/CarbonChannelRegistry.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.channels;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.google.inject.Singleton;\nimport com.google.inject.TypeLiteral;\nimport com.seiama.registry.Holder;\nimport com.seiama.registry.Registry;\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.NoSuchElementException;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport java.util.stream.Collectors;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChannelRegisterEvent;\nimport net.draycia.carbon.api.event.events.ChannelSwitchEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.DataDirectory;\nimport net.draycia.carbon.common.RawChat;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.event.events.CarbonChatEventImpl;\nimport net.draycia.carbon.common.event.events.CarbonEarlyChatEvent;\nimport net.draycia.carbon.common.event.events.CarbonReloadEvent;\nimport net.draycia.carbon.common.event.events.ChannelRegisterEventImpl;\nimport net.draycia.carbon.common.event.events.ChannelSwitchEventImpl;\nimport net.draycia.carbon.common.listeners.ChatListenerInternal;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.draycia.carbon.common.util.Exceptions;\nimport net.draycia.carbon.common.util.FileUtil;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.chat.ChatType;\nimport net.kyori.adventure.chat.SignedMessage;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.Command;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.minecraft.signed.SignedString;\nimport org.incendo.cloud.permission.Permission;\nimport org.incendo.cloud.permission.PredicatePermission;\nimport org.spongepowered.configurate.ConfigurateException;\nimport org.spongepowered.configurate.ConfigurationNode;\nimport org.spongepowered.configurate.loader.ConfigurationLoader;\nimport org.spongepowered.configurate.transformation.ConfigurationTransformation;\n\nimport static org.incendo.cloud.minecraft.signed.SignedGreedyStringParser.signedGreedyStringParser;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic class CarbonChannelRegistry extends ChatListenerInternal implements ChannelRegistry {\n\n    private final Path configChannelDir;\n    private final Injector injector;\n    private final Logger logger;\n    private final ConfigManager config;\n    private @MonotonicNonNull Key defaultKey;\n    private final CarbonMessages carbonMessages;\n    private final CarbonEventHandler eventHandler;\n    private final Key rawChatKey;\n    private final Map<String, SpecialHandler<?>> handlers = new HashMap<>();\n\n    private record SpecialHandler<T extends ConfigChatChannel>(Class<T> cls, Supplier<T> defaultSupplier) {}\n\n    public <T extends ConfigChatChannel> void registerSpecialConfigChannel(final String fileName, final Class<T> type) {\n        if (this.handlers.containsKey(fileName)) {\n            throw new IllegalStateException(\"Attempting to register duplicate entry (existing: \" + this.handlers.get(fileName)\n                + \", new: \" + type + \") for key \" + fileName);\n        }\n        this.handlers.put(fileName, new SpecialHandler<>(type, () -> this.injector.getInstance(type)));\n    }\n\n    private volatile Registry<Key, ChatChannel> channelRegistry = Registry.create();\n    private final Set<Key> configChannels = ConcurrentHashMap.newKeySet();\n    //\n    // private final BiMap<Key, ChatChannel> channelMap = Maps.synchronizedBiMap(HashBiMap.create());\n\n    @Inject\n    public CarbonChannelRegistry(\n        @DataDirectory final Path dataDirectory,\n        final Injector injector,\n        final Logger logger,\n        final ConfigManager config,\n        final CarbonMessages carbonMessages,\n        final CarbonEventHandler events,\n        @RawChat final Key rawChatKey\n    ) {\n        super(events, carbonMessages, config);\n        this.configChannelDir = dataDirectory.resolve(\"channels\");\n        this.injector = injector;\n        this.logger = logger;\n        this.config = config;\n        this.carbonMessages = carbonMessages;\n        this.eventHandler = events;\n        this.rawChatKey = rawChatKey;\n\n        if (config.primaryConfig().partyChat().enabled) {\n            this.registerSpecialConfigChannel(PartyChatChannel.FILE_NAME, PartyChatChannel.class);\n        }\n\n        events.subscribe(CarbonReloadEvent.class, -99, true, event -> this.reloadConfigChannels());\n    }\n\n    public static ConfigurationTransformation.Versioned configChatChannelUpgrader() {\n        // final ConfigurationTransformation initial;\n\n        return ConfigurationTransformation.versionedBuilder()\n            .versionKey(ConfigManager.CONFIG_VERSION_KEY)\n            // .addVersion(0, initial)\n            .build();\n    }\n\n    public static <N extends ConfigurationNode> N upgradeConfigChatChannelNode(final N node) throws ConfigurateException {\n        if (true) {\n            // No transformations yet!\n            return node;\n        }\n\n        if (!node.virtual()) { // we only want to migrate existing data\n            final ConfigurationTransformation.Versioned upgrader = configChatChannelUpgrader();\n            final int from = upgrader.version(node);\n            upgrader.apply(node);\n            final int to = upgrader.version(node);\n\n            ConfigManager.configVersionComment(node, upgrader);\n\n            if (from != to) { // we might not have made any changes\n                // TODO: use logger\n                //CarbonChatProvider.carbonChat().logger().info(\"Updated config schema from \" + from + \" to \" + to);\n            }\n        }\n\n        return node;\n    }\n\n    public void reloadConfigChannels() {\n        final Registry<Key, ChatChannel> newRegistry = Registry.create();\n\n        // Copy API registrations over\n        for (final Key registered : this.channelRegistry.keys()) {\n            if (!this.configChannels.contains(registered)) {\n                newRegistry.register(registered, this.channelRegistry.getHolder(registered).valueOrThrow());\n            }\n        }\n\n        final Set<Key> oldConfigChannels = Set.copyOf(this.configChannels);\n        this.configChannels.clear();\n\n        final Registry<Key, ChatChannel> oldRegistry = this.channelRegistry;\n        this.channelRegistry = newRegistry;\n\n        this.loadConfigChannels_(this.carbonMessages);\n\n        // Re-add any deleted channels; users must restart for them to be removed\n        // (don't want to leave behind commands that just error, or confuse addons)\n        for (final Key old : oldConfigChannels) {\n            if (!this.configChannels.contains(old)) {\n                this.configChannels.add(old);\n                this.channelRegistry.register(old, oldRegistry.getHolder(old).valueOrThrow());\n                this.logger.warn(\"The config file for channel [{}] was deleted, but removing \" +\n                    \"channels at runtime is not currently supported. You must restart the plugin \" +\n                    \"for the removal to take effect.\", old);\n            }\n        }\n\n        // Determine new channels and fire event if needed\n        final Set<Key> newConfigChannels = new HashSet<>();\n        for (final Key configChannel : this.configChannels) {\n            if (!oldConfigChannels.contains(configChannel)) {\n                newConfigChannels.add(configChannel);\n            }\n        }\n        if (!newConfigChannels.isEmpty()) {\n            this.eventHandler.emit(new ChannelRegisterEventImpl(this, Set.copyOf(newConfigChannels)));\n        }\n    }\n\n    public void loadConfigChannels(final CarbonMessages messages) {\n        this.loadConfigChannels_(messages);\n        this.eventHandler.emit(new ChannelRegisterEventImpl(this, Set.copyOf(this.configChannels)));\n    }\n\n    private void loadConfigChannels_(final CarbonMessages messages) {\n        this.logger.info(\"Loading config channels...\");\n        this.defaultKey = this.config.primaryConfig().defaultChannel();\n\n        this.saveSpecialDefaults();\n\n        List<Path> channelConfigs = FileUtil.listDirectoryEntries(this.configChannelDir, \"*.conf\");\n\n        final Set<String> channelConfigFileNames = channelConfigs\n            .stream()\n            .map(Path::getFileName)\n            .map(Path::toString)\n            .collect(Collectors.toSet());\n\n        final Set<String> expectedHandlerFileNames = this.handlers.keySet();\n\n        if (channelConfigs.size() == this.handlers.size() && channelConfigFileNames.containsAll(expectedHandlerFileNames)) {\n            this.saveDefaultChannelConfig();\n            channelConfigs = FileUtil.listDirectoryEntries(this.configChannelDir, \"*.conf\");\n        }\n\n        for (final Path channelConfigFile : channelConfigs) {\n            final String fileName = channelConfigFile.getFileName().toString();\n            if (!fileName.endsWith(\".conf\")) {\n                continue;\n            }\n\n            final @Nullable ChatChannel chatChannel = this.loadChannel(channelConfigFile);\n            if (chatChannel == null) {\n                continue;\n            }\n            final Key channelKey = chatChannel.key();\n            if (this.defaultKey.equals(channelKey)) {\n                this.logger.info(\"Default channel is [{}]\", channelKey);\n            }\n\n            if (this.channelRegistry.keys().contains(channelKey)) {\n                this.logger.warn(\"Channel with key [{}] has already been registered, skipping {}\", channelKey, channelConfigFile);\n                continue;\n            }\n\n            this.injector.injectMembers(chatChannel);\n            this.configChannels.add(chatChannel.key());\n            this.register(chatChannel, false);\n        }\n\n        if (this.channel(this.defaultKey) == null) {\n            this.logger.warn(\"No default channel found! Default channel key: [{}]\", this.defaultKey());\n        }\n\n        final List<String> channelList = new ArrayList<>();\n\n        for (final Key key : this.keys()) {\n            channelList.add(key.asString());\n        }\n\n        final String channels = String.join(\", \", channelList);\n\n        this.logger.info(\"Registered channels: [{}]\", channels);\n    }\n\n    private void saveSpecialDefaults() {\n        for (final Map.Entry<String, SpecialHandler<?>> e : this.handlers.entrySet()) {\n            final Path configFile = this.configChannelDir.resolve(e.getKey());\n            if (Files.isRegularFile(configFile)) {\n                continue;\n            }\n            try {\n                final ConfigChatChannel configChannel = e.getValue().defaultSupplier().get();\n                final ConfigurationLoader<?> loader = this.config.configurationLoader(FileUtil.mkParentDirs(configFile), ConfigManager.extractHeader(e.getValue().cls()));\n                final ConfigurationNode node = loader.createNode();\n                node.set(e.getValue().cls(), configChannel);\n                loader.save(node);\n            } catch (final IOException exception) {\n                throw Exceptions.rethrow(exception);\n            }\n        }\n    }\n\n    private void saveDefaultChannelConfig() {\n        try {\n            final Path configFile = this.configChannelDir.resolve(\"global.conf\");\n            final ConfigChatChannel configChannel = this.injector.getInstance(ConfigChatChannel.class);\n            final ConfigurationLoader<?> loader = this.config.configurationLoader(FileUtil.mkParentDirs(configFile), ConfigManager.extractHeader(ConfigChatChannel.class));\n            final ConfigurationNode node = loader.createNode();\n            node.set(ConfigChatChannel.class, configChannel);\n            loader.save(node);\n        } catch (final IOException exception) {\n            throw Exceptions.rethrow(exception);\n        }\n    }\n\n    private @Nullable ChatChannel loadChannel(final Path channelFile) {\n        try {\n            final @Nullable SpecialHandler<?> special = this.handlers.get(channelFile.getFileName().toString());\n            final Class<? extends ConfigChatChannel> type = special == null ? ConfigChatChannel.class : special.cls();\n\n            final ConfigurationLoader<?> loader = this.config.configurationLoader(channelFile, ConfigManager.extractHeader(type));\n            final ConfigurationNode loaded = upgradeConfigChatChannelNode(loader.load());\n            final @Nullable ConfigChatChannel channel = loaded.get(type);\n            if (channel == null) {\n                throw new ConfigurateException(\"Config deserialized to null.\");\n            }\n\n            loaded.set(type, channel);\n            loader.save(loaded);\n\n            return channel;\n        } catch (final ConfigurateException exception) {\n            this.logger.warn(\"Failed to load channel from file '{}'\", channelFile, exception);\n        }\n\n        return null;\n    }\n\n    private void sendMessageInChannelAsConsole(\n        final Audience sender,\n        final ChatChannel channel,\n        final String plainMessage\n    ) {\n        this.sendMessageInChannel(new ConsoleCarbonPlayer(sender), channel, SignedString.unsigned(plainMessage));\n    }\n\n    private void sendMessageInChannel(\n        final CarbonPlayer sender,\n        final ChatChannel channel,\n        final SignedString message\n    ) {\n        final @Nullable SignedMessage signedMessage = message.signedMessage();\n        final Component originalMessage;\n\n        if (signedMessage == null) {\n            originalMessage = Component.text(message.string());\n        } else {\n            originalMessage = Objects.requireNonNullElse(\n                signedMessage.unsignedContent(),\n                Component.text(signedMessage.message())\n            );\n        }\n\n        final @Nullable CarbonEarlyChatEvent earlyChatEvent = this.prepareAndEmitPreChatEvent(sender, originalMessage);\n\n        if (earlyChatEvent == null || earlyChatEvent.cancelled()) {\n            return;\n        }\n\n        final Component parsedMessage = this.parseTags(sender, earlyChatEvent.message());\n        if (parsedMessage == null) {\n            return;\n        }\n\n        final @Nullable CarbonChatEventImpl chatEvent = this.prepareAndEmitChatEvent(sender, parsedMessage, signedMessage, channel);\n\n        if (chatEvent == null || chatEvent.cancelled()) {\n            return;\n        }\n\n        for (final Audience recipient : chatEvent.recipients()) {\n            message.sendMessage(recipient, ChatType.chatType(this.rawChatKey), chatEvent.renderFor(recipient));\n        }\n    }\n\n    private void registerChannelCommands(final ChatChannel channel) {\n        final CommandManager<Commander> commandManager =\n            this.injector.getInstance(com.google.inject.Key.get(new TypeLiteral<CommandManager<Commander>>() {}));\n        if (!commandManager.isCommandRegistrationAllowed() || commandManager.commandTree().getNamedNode(channel.commandName()) != null) {\n            return;\n        }\n\n        Command.Builder<Commander> builder = commandManager.commandBuilder(channel.commandName(),\n                channel.commandAliases(), commandManager.createDefaultCommandMeta())\n            .optional(\"message\", signedGreedyStringParser());\n\n        if (!channel.permissions().dynamic()) {\n            builder = builder.permission(PredicatePermission.of(sender -> {\n                if (!(sender instanceof PlayerCommander player)) {\n                    return true;\n                }\n                return channel.permissions().joinPermitted(player.carbonPlayer()).permitted();\n            }));\n        }\n\n        final Key channelKey = channel.key();\n\n        final Command<Commander> command = builder.senderType(Commander.class)\n            .handler(handler -> {\n                final Commander commander = handler.sender();\n                @Nullable ChatChannel chatChannel = this.channel(channelKey);\n\n                if (!(commander instanceof PlayerCommander playerCommander)) {\n                    if (chatChannel != null && handler.contains(\"message\")) {\n                        final SignedString message = handler.get(\"message\");\n\n                        // TODO: trigger platform events related to chat\n                        this.sendMessageInChannelAsConsole(commander, chatChannel, message.string());\n                    }\n\n                    return;\n                }\n\n                final var player = playerCommander.carbonPlayer();\n\n                if (player.muted()) {\n                    this.carbonMessages.muteCannotSpeak(player);\n                    return;\n                }\n                if (player.leftChannels().contains(channelKey) && chatChannel != null) {\n                    player.joinChannel(chatChannel);\n                    this.carbonMessages.channelJoined(player);\n                }\n                if (handler.contains(\"message\")) {\n                    final SignedString message = handler.get(\"message\");\n\n                    // TODO: trigger platform events related to chat\n                    this.sendMessageInChannel(player, chatChannel, message);\n                } else {\n                    final @Nullable ChatChannel fromChannel = player.selectedChannel();\n                    if (this.config.primaryConfig().returnToDefaultChannel() && fromChannel != null && fromChannel.key().equals(channelKey)) {\n                        chatChannel = this.defaultChannel();\n                    }\n\n                    final ChannelSwitchEvent switchEvent = new ChannelSwitchEventImpl(player, chatChannel);\n                    this.eventHandler.emit(switchEvent);\n\n                    player.selectedChannel(switchEvent.channel());\n                    this.carbonMessages.changedChannels(player, chatChannel.key().value());\n                }\n            })\n            .build();\n\n        commandManager.command(command);\n\n        Command.Builder<Commander> proxyBuilder = commandManager.commandBuilder(\"channel\", \"ch\");\n\n        if (!channel.permissions().dynamic() && channel.permissions() instanceof ChannelPermissionsImpl permissions) {\n            proxyBuilder = proxyBuilder.permission(Permission.allOf(Permission.of(\"carbon.channel\"), Permission.of(permissions.permission())));\n        }\n\n        commandManager.command(proxyBuilder.literal(channelKey.value()).proxies(command).build());\n    }\n\n    @Override\n    public void register(final ChatChannel channel) {\n        this.register(channel, true);\n    }\n\n    public void register(final ChatChannel channel, final boolean fireRegisterEvent) {\n        this.channelRegistry.register(channel.key(), channel);\n        if (channel.shouldRegisterCommands()) {\n            this.registerChannelCommands(channel);\n        }\n        if (fireRegisterEvent) {\n            this.eventHandler.emit(new ChannelRegisterEventImpl(this, Set.of(channel.key())));\n        }\n    }\n\n    @Override\n    public @Nullable ChatChannel channel(final Key key) {\n        final @Nullable Holder<Key, ChatChannel> holder = this.channelRegistry.getHolder(key);\n        return holder == null ? null : holder.value();\n    }\n\n    public @Nullable ChatChannel channelByValue(final String value) {\n        if (value.contains(\":\")) {\n            return this.channel(Key.key(value));\n        }\n\n        for (final Key key : this.keys()) {\n            if (key.value().equalsIgnoreCase(value)) {\n                return this.channel(key);\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    public @NonNull Set<Key> keys() {\n        return Collections.unmodifiableSet(this.channelRegistry.keys());\n    }\n\n    @Override\n    public ChatChannel defaultChannel() {\n        return Objects.requireNonNull(this.channel(this.defaultKey));\n    }\n\n    @Override\n    public Key defaultKey() {\n        return this.defaultKey;\n    }\n\n    @Override\n    public ChatChannel channelOrDefault(final Key key) {\n        final @Nullable ChatChannel channel = this.channel(key);\n\n        if (channel != null) {\n            return channel;\n        }\n\n        return this.defaultChannel();\n    }\n\n    @Override\n    public ChatChannel channelOrThrow(final Key key) {\n        final @Nullable ChatChannel channel = this.channel(key);\n        if (channel != null) {\n            return channel;\n        }\n        throw new NoSuchElementException(\"No channel registered with key '\" + key.asString() + \"'\");\n    }\n\n    @Override\n    public void allKeys(final Consumer<Key> action) {\n        for (final Key key : this.channelRegistry.keys()) {\n            action.accept(key);\n        }\n        this.eventHandler.subscribe(\n            CarbonChannelRegisterEvent.class,\n            event -> {\n                for (final Key key : event.registered()) {\n                    action.accept(key);\n                }\n            }\n        );\n    }\n\n    @Override\n    public ChannelPermissions permission(final String permission) {\n        return new ChannelPermissionsImpl(permission, this.carbonMessages);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/channels/ChannelPermissionsImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.channels;\n\nimport net.draycia.carbon.api.channels.ChannelPermissionResult;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.messages.CarbonMessages;\n\nimport static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult;\n\npublic record ChannelPermissionsImpl(String permission, CarbonMessages messages) implements ChannelPermissions {\n    @Override\n    public ChannelPermissionResult joinPermitted(final CarbonPlayer player) {\n        return channelPermissionResult(\n            player.hasPermission(this.permission()),\n            () -> this.messages.channelNoPermission(player)\n        );\n    }\n\n    @Override\n    public ChannelPermissionResult speechPermitted(final CarbonPlayer player) {\n        return channelPermissionResult(\n            player.hasPermission(this.permission() + \".speak\"),\n            () -> this.messages.channelNoPermission(player)\n        );\n    }\n\n    @Override\n    public ChannelPermissionResult hearingPermitted(final CarbonPlayer player) {\n        return channelPermissionResult(\n            player.hasPermission(this.permission() + \".see\"),\n            () -> this.messages.channelNoPermission(player)\n        );\n    }\n\n    @Override\n    public boolean dynamic() {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/channels/ConfigChannelSettings.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.channels;\n\nimport java.util.Collections;\nimport java.util.List;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\nimport org.spongepowered.configurate.objectmapping.meta.Setting;\n\n@ConfigSerializable\npublic final class ConfigChannelSettings {\n\n    @Comment(\"\"\"\n        The channel's key, used to track the channel.\n        You only need to change the second part of the key. \"global\" by default.\n        The value is what's used in commands, this is probably what you want to change.\n        \"\"\")\n    private final @Nullable Key key = Key.key(\"carbon\", \"basic\");\n\n    @Comment(\"\"\"\n        The permission required to use the channel.\n        To read messages you must have the permission carbon.channel.basic.see\n        To send messages you must have the permission carbon.channel.basic.speak\n        If you want to give both, grant carbon.channel.basic or carbon.channel.basic.*\n        \"\"\")\n    private final @Nullable String permission = \"carbon.channel.basic\";\n\n    @Setting(\"format\")\n    @Comment(\"The chat formats for this channel.\")\n    private final @Nullable ConfigChannelMessageSource messageSource = new ConfigChannelMessageSource();\n\n    @Comment(\"Messages will be sent in this channel if they start with this prefix.\")\n    private final @Nullable String quickPrefix = \"\";\n\n    private final @Nullable Boolean shouldRegisterCommands = true;\n\n    private final @Nullable String commandName = null;\n\n    private final @Nullable List<String> commandAliases = Collections.emptyList();\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/channels/ConfigChatChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.channels;\n\nimport com.google.inject.Inject;\nimport io.leangen.geantyref.TypeToken;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessages;\nimport net.draycia.carbon.common.messages.CarbonMessageRenderer;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messages.SourcedAudience;\nimport net.draycia.carbon.common.messages.SourcedMessageSender;\nimport net.draycia.carbon.common.messages.SourcedReceiverResolver;\nimport net.draycia.carbon.common.messages.placeholders.BooleanPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.ComponentPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.IntPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.KeyPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.LongPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.StringPlaceholderResolver;\nimport net.draycia.carbon.common.messages.placeholders.UUIDPlaceholderResolver;\nimport net.draycia.carbon.common.util.Exceptions;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.moonshine.Moonshine;\nimport net.kyori.moonshine.exception.scan.UnscannableMethodException;\nimport net.kyori.moonshine.strategy.StandardPlaceholderResolverStrategy;\nimport net.kyori.moonshine.strategy.supertype.StandardSupertypeThenInterfaceSupertypeStrategy;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\nimport org.spongepowered.configurate.objectmapping.meta.Setting;\n\nimport static java.util.Objects.requireNonNull;\n\n@ConfigSerializable\n@DefaultQualifier(NonNull.class)\npublic class ConfigChatChannel implements ChatChannel {\n\n    protected transient @MonotonicNonNull @Inject CarbonServer server;\n    private transient @MonotonicNonNull @Inject CarbonMessageRenderer renderer;\n    protected transient @MonotonicNonNull @Inject CarbonMessages messages;\n\n    @Comment(\"\"\"\n        The channel's key, used to track the channel.\n        You only need to change the second part of the key. \"global\" by default.\n        The value is what's used in commands, this is probably what you want to change.\"\"\")\n    protected @Nullable Key key = Key.key(\"carbon\", \"global\");\n\n    @Comment(\"\"\"\n        The permission required to use the /channel <channelname> and /<channelname> commands.\n        \n        Assuming permission = \"carbon.channel.global\"\n        To read messages you must have the permission carbon.channel.global.see\n        To send messages you must have the permission carbon.channel.global.speak\"\"\")\n    private @Nullable String permission = null;\n\n    @Setting(\"format\")\n    @Comment(\"The chat formats for this channel.\")\n    protected @Nullable ConfigChannelMessageSource messageSource = new ConfigChannelMessageSource();\n\n    @Comment(\"Messages will be sent in this channel if they start with this prefix. (Leave empty/blank to disable quick prefix for this channel)\")\n    private @Nullable String quickPrefix = \"\";\n\n    private @Nullable Boolean shouldRegisterCommands = true;\n\n    private @Nullable String commandName = null;\n\n    protected @Nullable List<String> commandAliases = Collections.emptyList();\n\n    private transient @Nullable ConfigChannelMessages carbonMessages = null;\n\n    @Comment(\"\"\"\n        The distance players must be within to see each other's messages.\n        A value of '0' requires that both players are in the same world.\n        On velocity, '0' requires that both players are in the same server.\"\"\")\n    private int radius = -1;\n\n    @Comment(\"\"\"\n        If true, players will be able to see if they're not sending messages to anyone\n        because they're out of range from the radius.\"\"\")\n    private boolean emptyRadiusRecipientsMessage = true;\n\n    private Map<UUID, Long> cooldowns = new HashMap<>();\n\n    private long cooldown = -1;\n\n    @Comment(\"Whether this channel's messages should be sent cross-server.\")\n    private boolean crossServer = true;\n\n    @Override\n    public @Nullable String quickPrefix() {\n        if (this.quickPrefix == null || this.quickPrefix.isBlank()) {\n            return null;\n        }\n\n        return this.quickPrefix;\n    }\n\n    @Override\n    public boolean shouldRegisterCommands() {\n        return Objects.requireNonNullElse(this.shouldRegisterCommands, true);\n    }\n\n    @Override\n    public String commandName() {\n        return Objects.requireNonNullElse(this.commandName, this.key.value());\n    }\n\n    @Override\n    public List<String> commandAliases() {\n        return Objects.requireNonNullElse(this.commandAliases, Collections.emptyList());\n    }\n\n    @Override\n    public @NotNull Component render(\n        final CarbonPlayer sender,\n        final Audience recipient,\n        final Component message,\n        final Component originalMessage\n    ) {\n        return this.carbonMessages().chatFormat(\n            SourcedAudience.of(sender, recipient),\n            sender.uuid(),\n            this.key(),\n            sender.displayName(),\n            sender.username(),\n            message,\n            Component.text(\"null\")\n        );\n    }\n\n    @Override\n    public ChannelPermissions permissions() {\n        return new ChannelPermissionsImpl(this.permission(), this.messages);\n    }\n\n    @Override\n    public List<Audience> recipients(final CarbonPlayer sender) {\n        final List<Audience> recipients = new ArrayList<>();\n\n        for (final CarbonPlayer player : this.server.players()) {\n            if (this.permissions().hearingPermitted(player).permitted() && !player.leftChannels().contains(this.key)) {\n                recipients.add(player);\n            }\n        }\n\n        // console too!\n        recipients.add(this.server.console());\n\n        return recipients;\n    }\n\n    @Override\n    public long cooldown() {\n        return this.cooldown * 1000; // Seconds to millis\n    }\n\n    @Override\n    public long playerCooldown(final CarbonPlayer player) {\n        if (this.cooldown() <= 0) {\n            return 0;\n        }\n\n        return Objects.requireNonNullElse(this.cooldowns.get(player.uuid()), 0L);\n    }\n\n    public long startCooldown(final CarbonPlayer player) {\n        final long expiresAt = System.currentTimeMillis() + this.cooldown();\n        return Objects.requireNonNullElse(this.cooldowns.put(player.uuid(), expiresAt), 0L);\n    }\n\n    @Override\n    public @NonNull Key key() {\n        return Objects.requireNonNull(this.key);\n    }\n\n    public String messageFormat(final CarbonPlayer sender) {\n        return this.messageSource.messageOf(SourcedAudience.of(sender, sender), \"\");\n    }\n\n    private ConfigChannelMessages loadMessages() {\n        final SourcedReceiverResolver serverReceiverResolver = new SourcedReceiverResolver();\n        final ComponentPlaceholderResolver<SourcedAudience> componentPlaceholderResolver = new ComponentPlaceholderResolver<>();\n        final UUIDPlaceholderResolver<SourcedAudience> uuidPlaceholderResolver = new UUIDPlaceholderResolver<>();\n        final StringPlaceholderResolver<SourcedAudience> stringPlaceholderResolver = new StringPlaceholderResolver<>();\n        final KeyPlaceholderResolver<SourcedAudience> keyPlaceholderResolver = new KeyPlaceholderResolver<>();\n        final BooleanPlaceholderResolver<SourcedAudience> booleanPlaceholderResolver = new BooleanPlaceholderResolver<>();\n        final SourcedMessageSender carbonMessageSender = new SourcedMessageSender();\n\n        try {\n            return Moonshine.<ConfigChannelMessages, SourcedAudience>builder(new TypeToken<ConfigChannelMessages>() {})\n                .receiverLocatorResolver(serverReceiverResolver, 0)\n                .sourced(this.messageSource)\n                .rendered(this.renderer.asSourced())\n                .sent(carbonMessageSender)\n                .resolvingWithStrategy(new StandardPlaceholderResolverStrategy<>(new StandardSupertypeThenInterfaceSupertypeStrategy(false)))\n                .weightedPlaceholderResolver(Component.class, componentPlaceholderResolver, 0)\n                .weightedPlaceholderResolver(UUID.class, uuidPlaceholderResolver, 0)\n                .weightedPlaceholderResolver(String.class, stringPlaceholderResolver, 0)\n                .weightedPlaceholderResolver(Integer.class, new IntPlaceholderResolver<>(), 0)\n                .weightedPlaceholderResolver(Long.class, new LongPlaceholderResolver<>(), 0)\n                .weightedPlaceholderResolver(Key.class, keyPlaceholderResolver, 0)\n                .weightedPlaceholderResolver(Boolean.class, booleanPlaceholderResolver, 0)\n                .create(this.getClass().getClassLoader());\n        } catch (final UnscannableMethodException e) {\n            throw Exceptions.rethrow(e);\n        }\n    }\n\n    protected ConfigChannelMessages carbonMessages() {\n        if (this.carbonMessages == null) {\n            this.carbonMessages = this.loadMessages();\n        }\n\n        return requireNonNull(this.carbonMessages, \"Channel message service must not be null!\");\n    }\n\n    private String permission() {\n        if (this.permission == null) {\n            return \"carbon.channel.\" + this.key().value();\n        }\n\n        return this.permission;\n    }\n\n    @Override\n    public double radius() {\n        return this.radius;\n    }\n\n    @Override\n    public boolean emptyRadiusRecipientsMessage() {\n        return this.emptyRadiusRecipientsMessage;\n    }\n\n    @Override\n    public boolean shouldCrossServer() {\n        return this.crossServer;\n    }\n\n    @Override\n    public boolean equals(final Object other) {\n        if (!(other instanceof ConfigChatChannel otherChannel)) {\n            return false;\n        }\n        return otherChannel.key().equals(this.key());\n    }\n\n    @Override\n    public int hashCode() {\n        return Objects.hash(this.key());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/channels/PartyChatChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.channels;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.draycia.carbon.common.messages.SourcedAudience;\nimport net.draycia.carbon.common.users.WrappedCarbonPlayer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\nimport static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult;\n\n@ConfigSerializable\n@DefaultQualifier(NonNull.class)\npublic class PartyChatChannel extends ConfigChatChannel {\n\n    public static final String FILE_NAME = \"partychat.conf\";\n\n    public PartyChatChannel() {\n        this.key = Key.key(\"carbon\", \"partychat\");\n        this.commandAliases = List.of(\"pc\");\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n            \"default_format\", \"(party: <party_name>) <display_name>: <message>\",\n            \"console\", \"[party: <party_name>] <username>: <message>\"\n        );\n    }\n\n    @Override\n    public ChannelPermissions permissions() {\n        return ChannelPermissions.uniformDynamic(player -> channelPermissionResult(\n            player.party().join() != null,\n            () -> this.messages.cannotUsePartyChannel(player)\n        ));\n    }\n\n    @Override\n    public List<Audience> recipients(final CarbonPlayer sender) {\n        final WrappedCarbonPlayer wrapped = (WrappedCarbonPlayer) sender;\n        final @Nullable UUID party = wrapped.partyId();\n        if (party == null) {\n            if (sender.online()) {\n                sender.sendMessage(this.messages.cannotUsePartyChannel(sender));\n            }\n            return new ArrayList<>();\n        }\n        final List<Audience> recipients = super.recipients(sender);\n        recipients.removeIf(r -> r instanceof WrappedCarbonPlayer p && !Objects.equals(p.partyId(), party));\n        return recipients;\n    }\n\n    @Override\n    public @NotNull Component render(\n        final CarbonPlayer sender,\n        final Audience recipient,\n        final Component message,\n        final Component originalMessage\n    ) {\n        final @Nullable Party party = sender.party().join();\n        return this.carbonMessages().chatFormat(\n            SourcedAudience.of(sender, recipient),\n            sender.uuid(),\n            this.key(),\n            sender.displayName(),\n            sender.username(),\n            message,\n            party == null ? Component.text(\"null\") : party.name()\n        );\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessageSource.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.channels.messages;\n\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Objects;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.messages.SourcedAudience;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.draycia.carbon.common.util.DiscordRecipient;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.moonshine.message.IMessageSource;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\nimport org.spongepowered.configurate.objectmapping.meta.Setting;\n\n@ConfigSerializable\n@DefaultQualifier(NonNull.class)\npublic class ConfigChannelMessageSource implements IMessageSource<SourcedAudience, String> {\n\n    // Map<String, String> -> Map<Group, Format>\n    // \"default\" key will be configurable but let's not worry about that for now\n    @Setting(\"basic\")\n    @Comment(\"\"\"\n        Basic chat formats.\n        The \"default_format\" format is the main one you want to edit.\n        The \"console\" format is what's shown to console.\n        The \"discord\" format is what's shown to supported discord integrations.\n        If PlaceholderAPI is installed, PAPI placeholders (with %) are supported.\n        If MiniPlaceholders is installed, its placeholders (with <>) are supported.\n        The keys are group names, the values are chat formats (MiniMessage).\n        For example:\n            basic {\n                default_format=\"<<username>> <message>\"\n                vip=\"[VIP] <<username>> <message>\"\n                admin=\"<white>[</white>Prefix<white>]</white> <display_name><white>: <message></white>\"\n                discord=\"<message>\"\n            }\n        \"\"\")\n    public Map<String, String> defaults = Map.of(\n        \"default_format\", \"<display_name>: <message>\",\n        \"console\", \"[<channel>] <username>: <message>\",\n        \"discord\", \"<message>\"\n    );\n\n    @Comment(\"\"\"\n        Per-Language chat formats.\n        You can safely leave this section empty if you don't want to use this feature.\n        Each locale section can be configured in the same way as the above 'basic' section.\n        Will fall back to the 'basic' section if no format was found for the player's locale.\"\"\")\n    public Map<Locale, Map<String, String>> locales = Map.of(Locale.getDefault(), Map.of());\n\n    private static final String FALLBACK_FORMAT = \"<red><</red><username><red>></red> <message>\";\n\n    // TODO: Remove DiscordRecipient and use key instead (Couldn't figure out how to do it)\n    @Override\n    public String messageOf(final SourcedAudience sourcedAudience, final String ignored) {\n        if (sourcedAudience.recipient() instanceof CarbonPlayer && !(sourcedAudience.recipient() instanceof ConsoleCarbonPlayer)) {\n            return this.forPlayer(sourcedAudience);\n        } else if (sourcedAudience.recipient() instanceof DiscordRecipient) {\n            return this.defaults.getOrDefault(\"discord\", FALLBACK_FORMAT);\n        } else {\n            return this.defaults.getOrDefault(\"console\", FALLBACK_FORMAT);\n        }\n    }\n\n    private String forPlayer(final SourcedAudience sourcedAudience) {\n        final var sender = (CarbonPlayer) sourcedAudience.sender();\n        final var recipient = (CarbonPlayer) sourcedAudience.recipient();\n\n        if (recipient.locale() != null) {\n            final var formats = this.locales.get(recipient.locale());\n\n            if (formats != null) {\n                final @Nullable String format = formats.get(sender.primaryGroup());\n\n                if (format != null) {\n                    return format;\n                }\n\n                for (final var groupEntry : sender.groups()) {\n                    final @Nullable String groupFormat = formats.get(groupEntry);\n\n                    if (groupFormat != null) {\n                        return groupFormat;\n                    }\n                }\n            }\n        }\n\n        final @Nullable String format = this.defaults.get(sender.primaryGroup());\n\n        if (format != null) {\n            return format;\n        }\n\n        for (final var groupEntry : sender.groups()) {\n            final @Nullable String groupFormat = this.defaults.get(groupEntry);\n\n            if (groupFormat != null) {\n                return groupFormat;\n            }\n        }\n\n        return Objects.requireNonNullElse(this.defaults.get(\"default_format\"), FALLBACK_FORMAT);\n    }\n\n    private String forAudience(final Audience audience) {\n        return Objects.requireNonNullElse(this.defaults.get(\"console\"), FALLBACK_FORMAT);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/channels/messages/ConfigChannelMessages.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.channels.messages;\n\nimport java.util.UUID;\nimport net.draycia.carbon.common.messages.SourcedAudience;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.moonshine.annotation.Message;\nimport net.kyori.moonshine.annotation.Placeholder;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic interface ConfigChannelMessages {\n\n    // TODO: locale placeholders?\n    @Message(\"channel.format\")\n    Component chatFormat(\n        SourcedAudience audience,\n        @Placeholder UUID uuid,\n        @Placeholder Key channel,\n        @Placeholder(\"display_name\") Component displayName,\n        @Placeholder String username,\n        @Placeholder Component message,\n        @Placeholder(\"party_name\") Component partyName\n    );\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/CarbonCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command;\n\nimport java.util.Objects;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic abstract class CarbonCommand {\n\n    private @Nullable CommandSettings commandSettings = null;\n\n    public CommandSettings commandSettings() {\n        return Objects.requireNonNullElseGet(this.commandSettings, this::defaultCommandSettings);\n    }\n\n    public void commandSettings(final @NonNull CommandSettings commandSettings) {\n        this.commandSettings = commandSettings;\n    }\n\n    public abstract void init();\n\n    public abstract CommandSettings defaultCommandSettings();\n\n    public abstract Key key();\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/CommandSettings.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command;\n\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\n@ConfigSerializable\npublic class CommandSettings {\n\n    private boolean enabled = true;\n    private String name = \"\";\n    private String[] aliases = new String[0];\n\n    public CommandSettings() {\n\n    }\n\n    public CommandSettings(final boolean enabled, final String name, final String... aliases) {\n        this.enabled = enabled;\n        this.name = name;\n        this.aliases = aliases;\n    }\n\n    public CommandSettings(final String name, final String... aliases) {\n        this(true, name, aliases);\n    }\n\n    public boolean enabled() {\n        return this.enabled;\n    }\n\n    public String name() {\n        return this.name;\n    }\n\n    public String[] aliases() {\n        return this.aliases;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/Commander.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command;\n\nimport net.kyori.adventure.audience.Audience;\n\npublic interface Commander extends Audience {\n\n    boolean hasPermission(String permission);\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/ExecutionCoordinatorHolder.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command;\n\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport net.draycia.carbon.common.util.ConcurrentUtil;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.execution.ExecutionCoordinator;\n\n@DefaultQualifier(NonNull.class)\npublic record ExecutionCoordinatorHolder(\n    ExecutionCoordinator<Commander> executionCoordinator,\n    ExecutorService executorService\n) {\n\n    public void shutdown() {\n        ConcurrentUtil.shutdownExecutor(this.executorService, TimeUnit.MILLISECONDS, 50);\n    }\n\n    public static ExecutionCoordinatorHolder create(final Logger logger) {\n        final ExecutorService executorService = Executors.newFixedThreadPool(4, ConcurrentUtil.carbonThreadFactory(logger, \"Commands\"));\n        return new ExecutionCoordinatorHolder(\n            ExecutionCoordinator.coordinatorFor(executorService),\n            executorService\n        );\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/ParserFactory.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command;\n\nimport net.draycia.carbon.common.command.argument.CarbonPlayerParser;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic interface ParserFactory {\n\n    CarbonPlayerParser carbonPlayer();\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/PlayerCommander.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command;\n\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\n\npublic interface PlayerCommander extends Commander {\n\n    @NonNull CarbonPlayer carbonPlayer();\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/argument/CarbonPlayerParser.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.argument;\n\nimport com.google.inject.Inject;\nimport io.leangen.geantyref.TypeToken;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.exception.ComponentException;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.context.CommandContext;\nimport org.incendo.cloud.context.CommandInput;\nimport org.incendo.cloud.parser.ArgumentParseResult;\nimport org.incendo.cloud.parser.ArgumentParser;\nimport org.incendo.cloud.parser.ParserDescriptor;\nimport org.incendo.cloud.suggestion.SuggestionProvider;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonPlayerParser implements ArgumentParser.FutureArgumentParser<Commander, CarbonPlayer>, ParserDescriptor<Commander, CarbonPlayer> {\n\n    private final PlayerSuggestions suggestions;\n    private final UserManager<?> userManager;\n    private final ProfileResolver profileResolver;\n    private final CarbonMessages messages;\n\n    @Inject\n    private CarbonPlayerParser(\n        final PlayerSuggestions suggestions,\n        final UserManager<?> userManager,\n        final ProfileResolver profileResolver,\n        final CarbonMessages messages\n    ) {\n        this.suggestions = suggestions;\n        this.userManager = userManager;\n        this.profileResolver = profileResolver;\n        this.messages = messages;\n    }\n\n    @Override\n    public CompletableFuture<ArgumentParseResult<CarbonPlayer>> parseFuture(\n        final CommandContext<Commander> commandContext,\n        final CommandInput commandInput\n    ) {\n        final String input = commandInput.readString();\n        return this.profileResolver.resolveUUID(input, commandContext.isSuggestions()).thenCompose(uuid -> {\n            if (uuid == null) {\n                return ArgumentParseResult.failureFuture(new ParseException(input, this.messages));\n            }\n            return this.userManager.user(uuid).thenApply(ArgumentParseResult::success);\n        });\n    }\n\n    @Override\n    public @NonNull SuggestionProvider<Commander> suggestionProvider() {\n        return this.suggestions;\n    }\n\n    @Override\n    public @NonNull TypeToken<CarbonPlayer> valueType() {\n        return TypeToken.get(CarbonPlayer.class);\n    }\n\n    @Override\n    public @NonNull ArgumentParser<Commander, CarbonPlayer> parser() {\n        return this;\n    }\n\n    public static final class ParseException extends ComponentException {\n\n        private static final long serialVersionUID = -8331761537951077684L;\n        private final String input;\n\n        public ParseException(final String input, final CarbonMessages messages) {\n            super(messages.errorCommandInvalidPlayer(input));\n            this.input = input;\n        }\n\n        public String input() {\n            return this.input;\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/argument/PlayerSuggestions.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.argument;\n\nimport net.draycia.carbon.common.command.Commander;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.suggestion.SuggestionProvider;\n\n@DefaultQualifier(NonNull.class)\npublic interface PlayerSuggestions extends SuggestionProvider<Commander> {\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/ClearChatCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\n\n@DefaultQualifier(NonNull.class)\npublic final class ClearChatCommand extends CarbonCommand {\n\n    private final CarbonServer server;\n    private final CommandManager<Commander> commandManager;\n    private final ConfigManager configManager;\n    private final CarbonMessages carbonMessages;\n\n    @Inject\n    public ClearChatCommand(\n        final CarbonServer server,\n        final CommandManager<Commander> commandManager,\n        final ConfigManager configManager,\n        final CarbonMessages carbonMessages\n    ) {\n        this.server = server;\n        this.commandManager = commandManager;\n        this.configManager = configManager;\n        this.carbonMessages = carbonMessages;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"clearchat\", \"chatclear\", \"cc\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"clearchat\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .permission(\"carbon.clearchat.clear\")\n            .commandDescription(richDescription(this.carbonMessages.commandClearChatDescription()))\n            .handler(handler -> {\n                // Not fond of having to send 50 messages to each player\n                // Are we not able to just paste in 50 newlines and call it a day?\n                for (int i = 0; i < this.configManager.primaryConfig().clearChatSettings().iterations(); i++) {\n                    for (final var player : this.server.players()) {\n                        if (!player.hasPermission(\"carbon.clearchat.exempt\")) {\n                            player.sendMessage(this.configManager.primaryConfig().clearChatSettings().message());\n                        }\n                    }\n                }\n\n                final Component senderName;\n                final String username;\n\n                if (handler.sender() instanceof PlayerCommander player) {\n                    senderName = player.carbonPlayer().displayName();\n                    username = player.carbonPlayer().username();\n                } else {\n                    senderName = Component.text(\"Console\");\n                    username = \"Console\";\n                }\n\n                this.server.sendMessage(this.configManager.primaryConfig().clearChatSettings()\n                    .broadcast(senderName, username));\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/ContinueCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.util.UUID;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.minecraft.signed.SignedString;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.minecraft.signed.SignedGreedyStringParser.signedGreedyStringParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class ContinueCommand extends CarbonCommand {\n\n    private final UserManager<?> users;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages messages;\n    private final WhisperCommand.WhisperHandler whisper;\n\n    @Inject\n    public ContinueCommand(\n        final UserManager<?> userManager,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages messages,\n        final WhisperCommand.WhisperHandler whisper\n    ) {\n        this.users = userManager;\n        this.commandManager = commandManager;\n        this.messages = messages;\n        this.whisper = whisper;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"continue\", \"c\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"continue\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .required(\"message\", signedGreedyStringParser(), richDescription(this.messages.commandContinueArgumentMessage()))\n            .permission(\"carbon.whisper.continue\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.messages.commandContinueDescription()))\n            .handler(ctx -> {\n                final CarbonPlayer sender = ctx.sender().carbonPlayer();\n\n                if (sender.muted()) {\n                    this.messages.muteCannotSpeak(sender);\n                    return;\n                }\n\n                final SignedString message = ctx.get(\"message\");\n                final @Nullable UUID whisperTarget = sender.lastWhisperTarget();\n\n                if (whisperTarget == null) {\n                    this.messages.whisperTargetNotSet(sender, sender.displayName());\n                    return;\n                }\n\n                final @MonotonicNonNull CarbonPlayer recipient = this.users.user(whisperTarget).join();\n\n                this.whisper.whisper(sender, recipient, message);\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/DebugCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.util.ArrayList;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.JoinConfiguration;\nimport net.kyori.adventure.text.format.NamedTextColor;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\n\n@DefaultQualifier(NonNull.class)\npublic final class DebugCommand extends CarbonCommand {\n\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n    private final ParserFactory parserFactory;\n\n    @Inject\n    public DebugCommand(\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final ParserFactory parserFactory\n    ) {\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n        this.parserFactory = parserFactory;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"carbondebug\", \"cdebug\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"debug\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .optional(\"player\", this.parserFactory.carbonPlayer(),\n                richDescription(this.carbonMessages.commandDebugArgumentPlayer()))\n            .permission(\"carbon.debug\")\n            .senderType(Commander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandDebugDescription()))\n            .handler(handler -> {\n                final Commander sender = handler.sender();\n                final CarbonPlayer target;\n\n                if (handler.contains(\"player\")) {\n                    target = handler.get(\"player\");\n                } else if (sender instanceof PlayerCommander playerCommander) {\n                    target = playerCommander.carbonPlayer();\n                } else {\n                    return;\n                }\n\n                sender.sendMessage(\n                    Component.join(JoinConfiguration.noSeparators(),\n                        Component.text(\"Primary Group: \", NamedTextColor.GOLD),\n                        Component.text(target.primaryGroup(), NamedTextColor.GREEN))\n                );\n\n                final var groups = new ArrayList<Component>();\n\n                for (final var group : target.groups()) {\n                    groups.add(Component.text(group, NamedTextColor.GREEN));\n                }\n\n                final var formattedGroupsList =\n                    Component.join(JoinConfiguration.separator(\n                        Component.text(\", \", NamedTextColor.YELLOW)), groups\n                    );\n\n                sender.sendMessage(\n                    Component.join(JoinConfiguration.noSeparators(),\n                        Component.text(\"Groups: \", NamedTextColor.GOLD),\n                        formattedGroupsList\n                    )\n                );\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/FilterCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.BooleanParser.booleanParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class FilterCommand extends CarbonCommand {\n\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n\n    @Inject\n    public FilterCommand(\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages\n    ) {\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"filter\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"filter\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .optional(\"enabled\", booleanParser())\n            .permission(\"carbon.filter\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandOptionalFilterDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = handler.sender().carbonPlayer();\n\n                boolean enabled = !sender.applyOptionalChatFilters();\n\n                if (handler.contains(\"enabled\")) {\n                    enabled = handler.get(\"enabled\");\n                }\n\n                sender.applyOptionalChatFilters(enabled);\n                if (enabled) {\n                    this.carbonMessages.commandOptionalFilterEnabled(sender);\n                } else {\n                    this.carbonMessages.commandOptionalFilterDisabled(sender);\n                }\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/HelpCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.util.Map;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.messages.CarbonMessageSource;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nimport net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.context.CommandContext;\nimport org.incendo.cloud.context.CommandInput;\nimport org.incendo.cloud.help.result.CommandEntry;\nimport org.incendo.cloud.minecraft.extras.AudienceProvider;\nimport org.incendo.cloud.minecraft.extras.MinecraftHelp;\nimport org.incendo.cloud.suggestion.Suggestion;\nimport org.intellij.lang.annotations.Subst;\n\nimport static net.kyori.adventure.text.format.NamedTextColor.DARK_GRAY;\nimport static net.kyori.adventure.text.format.NamedTextColor.GRAY;\nimport static net.kyori.adventure.text.format.NamedTextColor.WHITE;\nimport static net.kyori.adventure.text.format.TextColor.color;\nimport static org.incendo.cloud.minecraft.extras.MinecraftHelp.helpColors;\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.StringParser.greedyStringParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class HelpCommand extends CarbonCommand {\n\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n    private final MinecraftHelp<Commander> minecraftHelp;\n\n    @Inject\n    public HelpCommand(\n        final CommandManager<Commander> commandManager,\n        final CarbonMessageSource messageSource,\n        final CarbonMessages carbonMessages\n    ) {\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n        this.minecraftHelp = createHelp(commandManager, messageSource);\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"carbon\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"help\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .literal(\"help\")\n            .optional(\"query\", greedyStringParser(), richDescription(this.carbonMessages.commandHelpArgumentQuery()), this::suggestQueries)\n            .permission(\"carbon.help\")\n            .commandDescription(richDescription(this.carbonMessages.commandHelpDescription()))\n            .handler(this::execute)\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n    private void execute(final CommandContext<Commander> ctx) {\n        this.minecraftHelp.queryCommands(ctx.getOrDefault(\"query\", \"\"), ctx.sender());\n    }\n\n    private CompletableFuture<Iterable<Suggestion>> suggestQueries(final CommandContext<Commander> ctx, final CommandInput input) {\n        final var result = this.commandManager.createHelpHandler().queryRootIndex(ctx.sender());\n        return CompletableFuture.completedFuture(result.entries().stream().map(CommandEntry::syntax).map(Suggestion::suggestion).toList());\n    }\n\n    private static MinecraftHelp<Commander> createHelp(\n        final CommandManager<Commander> manager,\n        final CarbonMessageSource messageSource\n    ) {\n        return MinecraftHelp.<Commander>builder()\n            .commandManager(manager)\n            .audienceProvider(AudienceProvider.nativeAudience())\n            .commandPrefix(\"/carbon help\")\n            .colors(helpColors(\n                color(0xE099FF),\n                WHITE,\n                color(0xDD1BC4),\n                GRAY,\n                DARK_GRAY\n            ))\n            .messageProvider((sender, key, args) -> {\n                final String messageKey = \"command.help.misc.\" + key;\n                final TagResolver.Builder tagResolver = TagResolver.builder();\n\n                for (final Map.Entry<String, String> entry : args.entrySet()) {\n                    @Subst(\"key\") final String k = entry.getKey();\n                    tagResolver.resolver(Placeholder.parsed(k, entry.getValue()));\n                }\n\n                return MiniMessage.miniMessage().deserialize(messageSource.messageOf(sender, messageKey), tagResolver.build());\n            })\n            .build();\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/IgnoreCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.UUIDParser.uuidParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class IgnoreCommand extends CarbonCommand {\n\n    private final UserManager<?> users;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n    private final ParserFactory parserFactory;\n\n    @Inject\n    public IgnoreCommand(\n        final UserManager<?> userManager,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final ParserFactory parserFactory\n    ) {\n        this.users = userManager;\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n        this.parserFactory = parserFactory;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"ignore\", \"block\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"ignore\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .optional(\"player\", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandIgnoreArgumentPlayer()))\n            .flag(this.commandManager.flagBuilder(\"uuid\")\n                .withAliases(\"u\")\n                .withDescription(richDescription(this.carbonMessages.commandIgnoreArgumentUUID()))\n                .withComponent(uuidParser())\n            )\n            .permission(\"carbon.ignore\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandIgnoreDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = handler.sender().carbonPlayer();\n                final CarbonPlayer target;\n\n                if (handler.contains(\"player\")) {\n                    target = handler.get(\"player\");\n                } else if (handler.flags().contains(\"uuid\")) {\n                    target = this.users.user(handler.flags().get(\"uuid\")).join();\n                } else {\n                    this.carbonMessages.ignoreTargetInvalid(sender);\n                    return;\n                }\n\n                if (target.hasPermission(\"carbon.ignore.exempt\")) {\n                    this.carbonMessages.ignoreExempt(sender, target.displayName());\n                    return;\n                }\n\n                if (sender.ignoring(target)) {\n                    this.carbonMessages.alreadyIgnored(sender, target.displayName());\n                    return;\n                }\n\n                sender.ignoring(target, true);\n                this.carbonMessages.nowIgnoring(sender, target.displayName());\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/IgnoreListCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.util.function.Supplier;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.util.Pagination;\nimport net.draycia.carbon.common.util.PaginationHelper;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.component.DefaultValue;\nimport org.incendo.cloud.context.CommandContext;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.IntegerParser.integerParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class IgnoreListCommand extends CarbonCommand {\n\n    private final UserManager<?> users;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages messages;\n    private final PaginationHelper pagination;\n\n    @Inject\n    public IgnoreListCommand(\n        final UserManager<?> userManager,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages messages,\n        final PaginationHelper pagination\n    ) {\n        this.users = userManager;\n        this.commandManager = commandManager;\n        this.messages = messages;\n        this.pagination = pagination;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"ignorelist\", \"listignores\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"ignorelist\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .permission(\"carbon.ignore\")\n            .senderType(PlayerCommander.class)\n            .optional(\"page\", integerParser(1), DefaultValue.constant(1))\n            .commandDescription(richDescription(this.messages.commandIgnoreListDescription()))\n            .handler(this::execute)\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n    private void execute(final CommandContext<PlayerCommander> ctx) {\n        final CarbonPlayer sender = ctx.sender().carbonPlayer();\n        final var elements = sender.ignoring().stream()\n            .sorted() // this way page numbers make sense\n            .map(id -> (Supplier<CarbonPlayer>) () -> this.users.user(id).join())\n            .toList();\n\n        if (elements.isEmpty()) {\n            this.messages.commandIgnoreListNoneIgnored(sender);\n            return;\n        }\n\n        final Pagination<Supplier<CarbonPlayer>> pagination = Pagination.<Supplier<CarbonPlayer>>builder()\n            .header(this.messages::commandIgnoreListPaginationHeader)\n            .item((e, lastOfPage) -> {\n                final CarbonPlayer p = e.get();\n                return this.messages.commandIgnoreListPaginationElement(p.displayName(), p.username());\n            })\n            .footer(this.pagination.footerRenderer(p -> \"/\" + this.commandSettings().name() + \" \" + p))\n            .pageOutOfRange(this.messages::paginationOutOfRange)\n            .build();\n\n        final int page = ctx.get(\"page\");\n\n        pagination.render(elements, page, 6).forEach(sender::sendMessage);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/JoinCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.util.Objects;\nimport net.draycia.carbon.api.channels.ChannelPermissionResult;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.suggestion.Suggestion;\nimport org.incendo.cloud.suggestion.SuggestionProvider;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.StringParser.greedyStringParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class JoinCommand extends CarbonCommand {\n\n    private final CarbonChannelRegistry channelRegistry;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n\n    @Inject\n    public JoinCommand(\n        final CarbonChannelRegistry channelRegistry,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages\n    ) {\n        this.channelRegistry = channelRegistry;\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"join\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"join\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .required(\"channel\", greedyStringParser(), SuggestionProvider.blocking((context, s) -> {\n                final CarbonPlayer sender = ((PlayerCommander) context.sender()).carbonPlayer();\n                return sender.leftChannels().stream()\n                    .map(this.channelRegistry::channel)\n                    .filter(Objects::nonNull)\n                    .filter(channel -> channel.permissions().joinPermitted(sender).permitted()\n                        || channel.permissions().hearingPermitted(sender).permitted()\n                        || channel.permissions().speechPermitted(sender).permitted())\n                    .map(channel -> channel.key().value())\n                    .map(Suggestion::suggestion)\n                    .toList();\n            }))\n            .permission(\"carbon.join\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandJoinDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = handler.sender().carbonPlayer();\n                final @Nullable ChatChannel channel = this.channelRegistry.channelByValue(handler.get(\"channel\"));\n                if (channel == null) {\n                    this.carbonMessages.channelNotFound(sender);\n                    return;\n                }\n                final ChannelPermissionResult permitted = channel.permissions().joinPermitted(sender);\n                if (!permitted.permitted()) {\n                    sender.sendMessage(permitted.reason());\n                    return;\n                }\n                if (!sender.leftChannels().contains(channel.key())) {\n                    this.carbonMessages.channelNotLeft(sender);\n                    return;\n                }\n                sender.joinChannel(channel);\n                this.carbonMessages.channelJoined(sender);\n\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/LeaveCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.util.Objects;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.suggestion.Suggestion;\nimport org.incendo.cloud.suggestion.SuggestionProvider;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.StringParser.greedyStringParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class LeaveCommand extends CarbonCommand {\n\n    private final CarbonChannelRegistry channelRegistry;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n\n    @Inject\n    public LeaveCommand(\n        final CarbonChannelRegistry channelRegistry,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages\n    ) {\n        this.channelRegistry = channelRegistry;\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"leave\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"leave\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .required(\"channel\", greedyStringParser(), SuggestionProvider.blocking((context, s) -> {\n                final CarbonPlayer sender = ((PlayerCommander) context.sender()).carbonPlayer();\n                return this.channelRegistry.keys().stream()\n                    .map(this.channelRegistry::channel)\n                    .filter(Objects::nonNull)\n                    .filter(x -> !sender.leftChannels().contains(x.key())\n                        && (x.permissions().joinPermitted(sender).permitted() || x.permissions().hearingPermitted(sender).permitted() || x.permissions().speechPermitted(sender).permitted()))\n                    .map(x -> x.key().value())\n                    .map(Suggestion::suggestion)\n                    .toList();\n            }))\n            .permission(\"carbon.leave\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandLeaveDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = handler.sender().carbonPlayer();\n                final @Nullable ChatChannel channel = this.channelRegistry.channelByValue(handler.get(\"channel\"));\n                if (channel == null) {\n                    this.carbonMessages.channelNotFound(sender);\n                    return;\n                }\n                if (sender.leftChannels().contains(channel.key())) {\n                    this.carbonMessages.channelAlreadyLeft(sender);\n                    return;\n                }\n                sender.leaveChannel(channel);\n                this.carbonMessages.channelLeft(sender);\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/MuteCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.time.Duration;\nimport java.time.Instant;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.parser.standard.DurationParser;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.UUIDParser.uuidParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class MuteCommand extends CarbonCommand {\n\n    private final CarbonServer server;\n    private final UserManager<?> users;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n    private final ParserFactory parserFactory;\n\n    @Inject\n    public MuteCommand(\n        final UserManager<?> userManager,\n        final CarbonServer server,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final ParserFactory parserFactory\n    ) {\n        this.server = server;\n        this.users = userManager;\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n        this.parserFactory = parserFactory;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"mute\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"mute\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .optional(\"player\", this.parserFactory.carbonPlayer(),\n                richDescription(this.carbonMessages.commandMuteArgumentPlayer()))\n            .flag(this.commandManager.flagBuilder(\"uuid\")\n                .withAliases(\"u\")\n                .withDescription(richDescription(this.carbonMessages.commandMuteArgumentUUID()))\n                .withComponent(uuidParser())\n            )\n            .flag(this.commandManager.flagBuilder(\"duration\")\n                .withAliases(\"d\")\n                .withDescription(richDescription(this.carbonMessages.commandMuteArgumentDuration()))\n                .withComponent(DurationParser.durationParser())\n            )\n            .permission(\"carbon.mute\")\n            .senderType(Commander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandMuteDescription()))\n            .handler(handler -> {\n                final Commander sender = handler.sender();\n                final CarbonPlayer target;\n\n                if (handler.contains(\"player\")) {\n                    target = handler.get(\"player\");\n                } else if (handler.flags().contains(\"uuid\")) {\n                    target = this.users.user(handler.flags().get(\"uuid\")).join();\n                } else {\n                    this.carbonMessages.muteNoTarget(sender);\n                    // TODO: send command syntax\n                    return;\n                }\n\n                if (target.hasPermission(\"carbon.mute.exempt\")) {\n                    this.carbonMessages.muteExempt(sender);\n                    return;\n                }\n\n                if (sender instanceof PlayerCommander playerCommander && playerCommander.carbonPlayer().equals(target)) {\n                    this.carbonMessages.muteExempt(playerCommander);\n                    return;\n                }\n\n                if (handler.flags().contains(\"duration\")) {\n                    this.handleTempMute(handler.flags().get(\"duration\"), target);\n                    return;\n                }\n\n                this.carbonMessages.muteAlertRecipient(target);\n                this.carbonMessages.muteAlertRecipient(this.server.console());\n\n                for (final var player : this.server.players()) {\n                    if (player.equals(target)) {\n                        continue;\n                    }\n\n                    if (player.hasPermission(\"carbon.mute.alert\")) {\n                        this.carbonMessages.muteAlertPlayers(player, target.displayName());\n                    }\n                }\n\n                target.muteExpiration(0);\n                target.muted(true);\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n    private void handleTempMute(final Duration duration, final CarbonPlayer target) {\n        final @Nullable Component formattedDuration;\n\n        if (duration.toDaysPart() > 0) {\n            formattedDuration = this.carbonMessages.durationDays(duration.toDaysPart(), duration.toHoursPart(),\n                duration.toMinutesPart(), duration.toSecondsPart());\n        } else {\n            formattedDuration = this.carbonMessages.durationHours(duration.toHoursPart(), duration.toMinutesPart(),\n                duration.toSecondsPart());\n        }\n\n        this.carbonMessages.tempMuteAlertRecipient(target, formattedDuration);\n        this.carbonMessages.tempMuteAlertRecipient(this.server.console(), formattedDuration);\n\n        for (final var player : this.server.players()) {\n            if (player.equals(target)) {\n                continue;\n            }\n\n            if (player.hasPermission(\"carbon.mute.alert\")) {\n                this.carbonMessages.tempMuteAlertPlayers(player, target.displayName(), formattedDuration);\n            }\n        }\n\n        target.muted(true);\n        target.muteExpiration(Instant.now().plus(duration).toEpochMilli());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/MuteInfoCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.time.Duration;\nimport java.time.Instant;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.UUIDParser.uuidParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class MuteInfoCommand extends CarbonCommand {\n\n    private final UserManager<?> users;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n    private final ParserFactory parserFactory;\n\n    @Inject\n    public MuteInfoCommand(\n        final UserManager<?> userManager,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final ParserFactory parserFactory\n    ) {\n        this.users = userManager;\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n        this.parserFactory = parserFactory;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"muteinfo\", \"muted\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"muteinfo\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .optional(\"player\", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandMuteInfoArgumentPlayer()))\n            .flag(this.commandManager.flagBuilder(\"uuid\")\n                .withAliases(\"u\")\n                .withDescription(richDescription(this.carbonMessages.commandMuteInfoArgumentUUID()))\n                .withComponent(uuidParser())\n            )\n            .permission(\"carbon.mute.info\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandMuteInfoDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = handler.sender().carbonPlayer();\n                final CarbonPlayer target;\n\n                if (handler.contains(\"player\")) {\n                    target = handler.get(\"player\");\n                } else if (handler.flags().contains(\"uuid\")) {\n                    target = this.users.user(handler.flags().get(\"uuid\")).join();\n                } else {\n                    target = sender;\n                }\n\n                if (!target.muted()) {\n                    if (sender.equals(target)) {\n                        this.carbonMessages.muteInfoSelfNotMuted(sender);\n                    } else {\n                        this.carbonMessages.muteInfoNotMuted(sender, target.displayName());\n                    }\n                } else {\n                    if (sender.equals(target)) {\n                        this.carbonMessages.muteInfoSelfMuted(sender);\n                    } else if (target.muteExpiration() > Instant.now().toEpochMilli()) {\n                        final Duration duration = Duration.ofMillis(target.muteExpiration() - System.currentTimeMillis());\n                        final @Nullable Component formattedDuration;\n\n                        if (duration.toDaysPart() > 0) {\n                            formattedDuration = this.carbonMessages.durationDays(duration.toDaysPart(), duration.toHoursPart(),\n                                duration.toMinutesPart(), duration.toSecondsPart());\n                        } else {\n                            formattedDuration = this.carbonMessages.durationHours(duration.toHoursPart(), duration.toMinutesPart(),\n                                duration.toSecondsPart());\n                        }\n\n                        this.carbonMessages.muteInfoMutedDuration(sender, target.displayName(), formattedDuration);\n                    } else {\n                        this.carbonMessages.muteInfoMuted(sender, target.displayName(), target.muted());\n                    }\n                }\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/NicknameCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messages.TagPermissions;\nimport net.draycia.carbon.common.util.CloudUtils;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.StringParser.greedyStringParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class NicknameCommand extends CarbonCommand {\n\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n    private final ParserFactory parserFactory;\n    private final ConfigManager config;\n\n    @Inject\n    public NicknameCommand(\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final ParserFactory parserFactory,\n        final ConfigManager config\n    ) {\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n        this.parserFactory = parserFactory;\n        this.config = config;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"nickname\", \"nick\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"nickname\");\n    }\n\n    @Override\n    public void init() {\n        if (!this.config.primaryConfig().nickname().useCarbonNicknames()) {\n            return;\n        }\n\n        // TODO: Allow UUID input for target player\n        final var selfRoot = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases());\n        final var othersRoot = selfRoot.literal(\"player\")\n            .required(\"player\", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandNicknameArgumentPlayer()));\n\n        // Check nickname\n        this.commandManager.command(selfRoot.permission(\"carbon.nickname\")\n            .commandDescription(richDescription(this.carbonMessages.commandNicknameDescription()))\n            .handler(ctx -> this.checkOwnNickname(CloudUtils.nonPlayerMustProvidePlayer(this.carbonMessages, ctx.sender()))));\n        this.commandManager.command(othersRoot.permission(\"carbon.nickname.others\")\n            .commandDescription(richDescription(this.carbonMessages.commandNicknameOthersDescription()))\n            .handler(ctx -> this.checkOthersNickname(ctx.sender(), ctx.get(\"player\"))));\n\n        // Set nickname\n        this.commandManager.command(selfRoot.permission(\"carbon.nickname.set\")\n            .commandDescription(richDescription(this.carbonMessages.commandNicknameSetDescription()))\n            .required(\"nickname\", greedyStringParser(), richDescription(this.carbonMessages.commandNicknameArgumentNickname()))\n            .handler(ctx -> this.applyNickname(ctx.sender(), CloudUtils.nonPlayerMustProvidePlayer(this.carbonMessages, ctx.sender()), ctx.get(\"nickname\"))));\n        this.commandManager.command(othersRoot.permission(\"carbon.nickname.others.set\")\n            .commandDescription(richDescription(this.carbonMessages.commandNicknameOthersSetDescription()))\n            .required(\"nickname\", greedyStringParser(), richDescription(this.carbonMessages.commandNicknameArgumentNickname()))\n            .handler(ctx -> this.applyNickname(ctx.sender(), ctx.get(\"player\"), ctx.get(\"nickname\"))));\n\n        // Reset/remove nickname\n        this.commandManager.command(selfRoot.permission(\"carbon.nickname.set\")\n            .commandDescription(richDescription(this.carbonMessages.commandNicknameResetDescription()))\n            .literal(\"reset\")\n            .handler(ctx -> this.resetNickname(ctx.sender(), CloudUtils.nonPlayerMustProvidePlayer(this.carbonMessages, ctx.sender()))));\n        this.commandManager.command(othersRoot.permission(\"carbon.nickname.others.set\")\n            .commandDescription(richDescription(this.carbonMessages.commandNicknameOthersResetDescription()))\n            .literal(\"reset\")\n            .handler(ctx -> this.resetNickname(ctx.sender(), ctx.get(\"player\"))));\n    }\n\n    private void resetNickname(final Commander sender, final CarbonPlayer target) {\n        target.nickname(null);\n\n        if (sender instanceof PlayerCommander playerCommander\n            && playerCommander.carbonPlayer().uuid().equals(target.uuid())) {\n            this.carbonMessages.nicknameReset(target);\n        } else {\n            this.carbonMessages.nicknameResetOthers(sender, target.username());\n        }\n    }\n\n    private void applyNickname(final Commander sender, final CarbonPlayer target, final String nick) {\n        final Component parsedNick = parseNickname(sender, nick);\n\n        final String plainNick = PlainTextComponentSerializer.plainText().serialize(parsedNick);\n\n        // If the nickname is caught in the character limit, return without setting a nickname.\n        final int minLength = this.config.primaryConfig().nickname().minLength();\n        final int maxLength = this.config.primaryConfig().nickname().maxLength();\n        if (plainNick.length() < minLength || maxLength < plainNick.length()) {\n            this.carbonMessages.nicknameErrorCharacterLimit(sender, parsedNick, minLength, maxLength);\n            return;\n        }\n\n        if (this.config.primaryConfig().nickname().blackList().stream().anyMatch(plainNick::equalsIgnoreCase)) {\n            this.carbonMessages.nicknameErrorBlackList(sender, parsedNick);\n            return;\n        }\n\n        if (!sender.hasPermission(\"carbon.nickname.filter\") && !plainNick.matches(this.config.primaryConfig().nickname().filter())) {\n            this.carbonMessages.nicknameErrorFilter(sender, parsedNick);\n            return;\n        }\n\n        target.nickname(parsedNick);\n\n        if (sender instanceof PlayerCommander playerCommander\n            && playerCommander.carbonPlayer().uuid().equals(target.uuid())) {\n            // Setting own nickname\n            this.carbonMessages.nicknameSet(sender, parsedNick);\n        } else {\n            // Setting other player's nickname\n            this.carbonMessages.nicknameSet(target, parsedNick);\n            this.carbonMessages.nicknameSetOthers(sender, target.username(), parsedNick);\n        }\n    }\n\n    private void checkOwnNickname(final CarbonPlayer sender) {\n        if (sender.nickname() != null) {\n            this.carbonMessages.nicknameShow(sender, sender.username(), sender.nickname());\n        } else {\n            this.carbonMessages.nicknameShowUnset(sender, sender.username());\n        }\n    }\n\n    private void checkOthersNickname(final Audience sender, final CarbonPlayer target) {\n        if (target.nickname() != null) {\n            this.carbonMessages.nicknameShowOthers(sender, target.username(), target.nickname());\n        } else {\n            this.carbonMessages.nicknameShowOthersUnset(sender, target.username());\n        }\n    }\n\n    private static Component parseNickname(final Commander sender, final String nick) {\n        // trim one level of quotes, to allow for nicknames which collide with command literals\n        return TagPermissions.parseTags(sender, TagPermissions.NICKNAME, trimQuotes(nick), sender::hasPermission);\n    }\n\n    private static String trimQuotes(final String string) {\n        if (string.length() < 3) {\n            return string;\n        }\n        final char first = string.charAt(0);\n        if ((first == '\\'' || first == '\"') && string.endsWith(String.valueOf(first))) {\n            return string.substring(1, string.length() - 1);\n        }\n        return string;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/PartyCommands.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.google.inject.Inject;\nimport java.util.Comparator;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.UUID;\nimport java.util.function.Supplier;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messages.Option;\nimport net.draycia.carbon.common.messages.TagPermissions;\nimport net.draycia.carbon.common.users.NetworkUsers;\nimport net.draycia.carbon.common.users.PartyInvites;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport net.draycia.carbon.common.util.Pagination;\nimport net.draycia.carbon.common.util.PaginationHelper;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.component.DefaultValue;\nimport org.incendo.cloud.context.CommandContext;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.IntegerParser.integerParser;\nimport static org.incendo.cloud.parser.standard.StringParser.greedyStringParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class PartyCommands extends CarbonCommand {\n\n    private final CommandManager<Commander> commandManager;\n    private final ParserFactory parserFactory;\n    private final UserManagerInternal<?> userManager;\n    private final PartyInvites partyInvites;\n    private final ConfigManager config;\n    private final CarbonMessages messages;\n    private final PaginationHelper pagination;\n    private final NetworkUsers network;\n\n    @Inject\n    public PartyCommands(\n        final CommandManager<Commander> commandManager,\n        final ParserFactory parserFactory,\n        final UserManagerInternal<?> userManager,\n        final PartyInvites partyInvites,\n        final ConfigManager config,\n        final CarbonMessages messages,\n        final PaginationHelper pagination,\n        final NetworkUsers network\n    ) {\n        this.commandManager = commandManager;\n        this.parserFactory = parserFactory;\n        this.userManager = userManager;\n        this.partyInvites = partyInvites;\n        this.config = config;\n        this.messages = messages;\n        this.pagination = pagination;\n        this.network = network;\n    }\n\n    @Override\n    public void init() {\n        if (!this.config.primaryConfig().partyChat().enabled) {\n            return;\n        }\n\n        final var root = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .senderType(PlayerCommander.class)\n            .permission(\"carbon.parties\");\n        final var info = root.commandDescription(richDescription(this.messages.partyDesc())).handler(this::info);\n\n        this.commandManager.command(info);\n        this.commandManager.command(info.literal(\"page\")\n            .optional(\"page\", integerParser(1), DefaultValue.constant(1)));\n        this.commandManager.command(\n            root.literal(\"create\")\n                .commandDescription(richDescription(this.messages.partyCreateDesc()))\n                .optional(\"name\", greedyStringParser())\n                .handler(this::createParty)\n        );\n        this.commandManager.command(\n            root.literal(\"invite\")\n                .commandDescription(richDescription(this.messages.partyInviteDesc()))\n                .required(\"player\", this.parserFactory.carbonPlayer())\n                .handler(this::invitePlayer)\n        );\n        this.commandManager.command(\n            root.literal(\"accept\")\n                .commandDescription(richDescription(this.messages.partyAcceptDesc()))\n                .optional(\"sender\", this.parserFactory.carbonPlayer())\n                .handler(this::acceptInvite)\n        );\n        this.commandManager.command(\n            root.literal(\"leave\")\n                .commandDescription(richDescription(this.messages.partyLeaveDesc()))\n                .handler(this::leaveParty)\n        );\n        this.commandManager.command(\n            root.literal(\"disband\")\n                .commandDescription(richDescription(this.messages.partyDisbandDesc()))\n                .handler(this::disbandParty)\n        );\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"party\", \"group\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"party\");\n    }\n\n    private void info(final CommandContext<PlayerCommander> ctx) {\n        final CarbonPlayer player = ctx.sender().carbonPlayer();\n        final @Nullable Party party = player.party().join();\n        if (party == null) {\n            this.messages.notInParty(player);\n            return;\n        }\n\n        this.messages.currentParty(player, party.name());\n\n        final var elements = party.members().stream()\n            .sorted(Comparator.<UUID, Boolean>comparing(this.network::online).reversed().thenComparing(UUID::compareTo))\n            .map(id -> (Supplier<CarbonPlayer>) () -> this.userManager.user(id).join())\n            .toList();\n\n        if (elements.isEmpty()) {\n            throw new IllegalStateException();\n        }\n\n        final Pagination<Supplier<CarbonPlayer>> pagination = Pagination.<Supplier<CarbonPlayer>>builder()\n            .header((page, pages) -> this.messages.commandPartyPaginationHeader(party.name()))\n            .item((e, lastOfPage) -> {\n                final CarbonPlayer p = e.get();\n                return this.messages.commandPartyPaginationElement(p.displayName(), p.username(), new Option(this.network.online(p)));\n            })\n            .footer(this.pagination.footerRenderer(p -> \"/\" + this.commandSettings().name() + \" page \" + p))\n            .pageOutOfRange(this.messages::paginationOutOfRange)\n            .build();\n\n        final int page = ctx.getOrDefault(\"page\", 1);\n\n        pagination.render(elements, page, 6).forEach(player::sendMessage);\n    }\n\n    private void createParty(final CommandContext<PlayerCommander> ctx) {\n        final CarbonPlayer player = ctx.sender().carbonPlayer();\n        final @Nullable Party oldParty = player.party().join();\n        if (oldParty != null) {\n            this.messages.mustLeavePartyFirst(player);\n            return;\n        }\n        final String name = ctx.getOrDefault(\"name\", player.username() + \"'s party\");\n        final Component component = TagPermissions.parseTags(player, TagPermissions.PARTY_NAME, name, player::hasPermission);\n        final Party party;\n        try {\n            party = this.userManager.createParty(component);\n        } catch (final IllegalArgumentException e) {\n            this.messages.partyNameTooLong(player);\n            return;\n        }\n        party.addMember(player.uuid());\n        this.messages.partyCreated(player, party.name());\n    }\n\n    private void invitePlayer(final CommandContext<PlayerCommander> ctx) {\n        final CarbonPlayer player = ctx.sender().carbonPlayer();\n        final CarbonPlayer recipient = ctx.get(\"player\");\n        if (recipient.uuid().equals(player.uuid())) {\n            this.messages.cannotInviteSelf(player);\n            return;\n        }\n        final @Nullable Party party = player.party().join();\n        if (party == null) {\n            this.messages.mustBeInParty(player);\n            return;\n        }\n        final @Nullable Party recipientParty = recipient.party().join();\n        if (recipientParty != null && recipientParty.id().equals(party.id())) {\n            this.messages.alreadyInParty(player, recipient.displayName());\n            return;\n        }\n        this.partyInvites.sendInvite(player.uuid(), recipient.uuid(), party.id());\n        this.messages.receivedPartyInvite(recipient, player.displayName(), player.username(), party.name());\n        this.messages.sentPartyInvite(player, recipient.displayName(), party.name());\n    }\n\n    private void acceptInvite(final CommandContext<PlayerCommander> ctx) {\n        final @Nullable CarbonPlayer sender = ctx.getOrDefault(\"sender\", null);\n        final CarbonPlayer player = ctx.sender().carbonPlayer();\n        final @Nullable Invite invite = this.findInvite(player, sender);\n        if (invite == null) {\n            return;\n        }\n        final @Nullable Party old = player.party().join();\n        if (old != null) {\n            this.messages.mustLeavePartyFirst(player);\n            return;\n        }\n        this.partyInvites.invalidateInvite(invite.sender(), player.uuid());\n        invite.party().addMember(player.uuid());\n        this.messages.joinedParty(player, invite.party().name());\n    }\n\n    private void leaveParty(final CommandContext<PlayerCommander> ctx) {\n        final CarbonPlayer player = ctx.sender().carbonPlayer();\n        final @Nullable Party old = player.party().join();\n        if (old == null) {\n            this.messages.mustBeInParty(player);\n            return;\n        }\n        if (old.members().size() == 1) {\n            this.disbandParty(ctx);\n            return;\n        }\n        old.removeMember(player.uuid());\n        this.messages.leftParty(player, old.name());\n    }\n\n    private void disbandParty(final CommandContext<PlayerCommander> ctx) {\n        final CarbonPlayer player = ctx.sender().carbonPlayer();\n        final @Nullable Party old = player.party().join();\n        if (old == null) {\n            this.messages.mustBeInParty(player);\n            return;\n        }\n        if (old.members().size() != 1) {\n            this.messages.cannotDisbandParty(player, old.name());\n            return;\n        }\n        old.disband();\n        this.messages.disbandedParty(player, old.name());\n    }\n\n    private @Nullable Invite findInvite(final CarbonPlayer player, final @Nullable CarbonPlayer sender) {\n        final @Nullable Cache<UUID, UUID> cache = this.partyInvites.invitesFor(player.uuid());\n        final @Nullable Map<UUID, UUID> map = cache != null ? Map.copyOf(cache.asMap()) : null;\n\n        if (map == null || map.isEmpty()) {\n            this.messages.noPendingPartyInvites(player);\n            return null;\n        } else if (sender != null) {\n            final @Nullable Party p = Optional.ofNullable(map.get(sender.uuid()))\n                .map(id -> this.userManager.party(id).join())\n                .orElse(null);\n            if (p == null) {\n                this.messages.noPartyInviteFrom(player, sender.displayName());\n                return null;\n            }\n            return new Invite(sender.uuid(), p);\n        }\n\n        if (map.size() == 1) {\n            final Map.Entry<UUID, UUID> e = map.entrySet().iterator().next();\n            final @Nullable Party p = this.userManager.party(e.getValue()).join();\n            if (p == null) {\n                this.messages.noPendingPartyInvites(player);\n                return null;\n            }\n            return new Invite(e.getKey(), p);\n        }\n\n        this.messages.mustSpecifyPartyInvite(player);\n        return null;\n    }\n\n    private record Invite(UUID sender, Party party) {}\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/RealNameCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.util.Locale;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.parser.standard.StringParser;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\n\n@DefaultQualifier(NonNull.class)\npublic final class RealNameCommand extends CarbonCommand {\n\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n    private final CarbonServer server;\n\n    @Inject\n    public RealNameCommand(\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final CarbonServer server\n    ) {\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n        this.server = server;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"realname\", \"rn\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"realname\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .required(\"player\", StringParser.greedyStringParser(),\n                richDescription(this.carbonMessages.commandRealNameArgumentPlayer()))\n            .permission(\"carbon.realname\")\n            .senderType(Commander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandRealNameDescription()))\n            .handler(handler -> {\n                final String input = handler.<String>get(\"player\").split(\" \")[0].toLowerCase(Locale.ENGLISH);\n                boolean found = false;\n\n                for (final CarbonPlayer player : this.server.players()) {\n                    if (player.vanished() && !handler.sender().hasPermission(\"carbon.realname.vanished\")) {\n                        continue;\n                    }\n\n                    final String plainName = PlainTextComponentSerializer.plainText().serialize(player.displayName()).toLowerCase(Locale.ENGLISH);\n\n                    if (plainName.contains(input)) {\n                        found = true;\n                        this.carbonMessages.realName(handler.sender(), player.displayName(), player.username());\n                    }\n                }\n\n                if (!found) {\n                    this.carbonMessages.realNameTargetInvalid(handler.sender(), input);\n                }\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/ReloadCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.event.events.CarbonReloadEvent;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\n\n@DefaultQualifier(NonNull.class)\npublic final class ReloadCommand extends CarbonCommand {\n\n    private final CarbonEventHandler events;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n\n    @Inject\n    public ReloadCommand(\n        final CarbonEventHandler eventHandler,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages\n    ) {\n        this.events = eventHandler;\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"carbon\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"reload\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .literal(\"reload\")\n            .permission(\"carbon.reload\")\n            .senderType(Commander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandReloadDescription()))\n            .handler(handler -> {\n                // TODO: Check if all listeners succeeded\n                this.events.emit(new CarbonReloadEvent());\n                this.carbonMessages.configReloaded(handler.sender());\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/ReplyCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.util.UUID;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.minecraft.signed.SignedString;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.minecraft.signed.SignedGreedyStringParser.signedGreedyStringParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class ReplyCommand extends CarbonCommand {\n\n    private final UserManager<?> users;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages messages;\n    private final WhisperCommand.WhisperHandler whisper;\n\n    @Inject\n    public ReplyCommand(\n        final UserManager<?> userManager,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages messages,\n        final WhisperCommand.WhisperHandler whisper\n    ) {\n        this.users = userManager;\n        this.commandManager = commandManager;\n        this.messages = messages;\n        this.whisper = whisper;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"reply\", \"r\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"reply\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .required(\"message\", signedGreedyStringParser(), richDescription(this.messages.commandReplyArgumentMessage()))\n            .permission(\"carbon.whisper.reply\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.messages.commandReplyDescription()))\n            .handler(ctx -> {\n                final CarbonPlayer sender = ctx.sender().carbonPlayer();\n\n                if (sender.muted()) {\n                    this.messages.muteCannotSpeak(sender);\n                    return;\n                }\n\n                final SignedString message = ctx.get(\"message\");\n                final @Nullable UUID replyTarget = sender.whisperReplyTarget();\n\n                if (replyTarget == null) {\n                    this.messages.replyTargetNotSet(sender, sender.displayName());\n                    return;\n                }\n\n                final @MonotonicNonNull CarbonPlayer recipient = this.users.user(replyTarget).join();\n\n                this.whisper.whisper(sender, recipient, message);\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/SpyCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.BooleanParser.booleanParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class SpyCommand extends CarbonCommand {\n\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n\n    @Inject\n    public SpyCommand(\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages\n    ) {\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"spy\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"spy\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .optional(\"enabled\", booleanParser())\n            .permission(\"carbon.spy\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandSpyDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = handler.sender().carbonPlayer();\n\n                boolean enabled = !sender.spying();\n\n                if (handler.contains(\"enabled\")) {\n                    enabled = handler.get(\"enabled\");\n                }\n\n                sender.spying(enabled);\n                if (enabled) {\n                    this.carbonMessages.commandSpyEnabled(sender);\n                } else {\n                    this.carbonMessages.commandSpyDisabled(sender);\n                }\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/ToggleMessagesCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\n\npublic class ToggleMessagesCommand extends CarbonCommand {\n\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n\n    @Inject\n    public ToggleMessagesCommand(\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages\n    ) {\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"togglemsg\", \"togglepm\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"togglemsg\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .permission(\"carbon.togglemsg\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandToggleMsgDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = handler.sender().carbonPlayer();\n                final boolean nowIgnoring = !sender.ignoringDirectMessages();\n                sender.ignoringDirectMessages(nowIgnoring);\n\n                if (nowIgnoring) {\n                    this.carbonMessages.whispersToggledOff(sender);\n                } else {\n                    this.carbonMessages.whispersToggledOn(sender);\n                }\n            })\n            .build();\n\n        this.commandManager.command(command);\n\n        final var toggleOn = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .permission(\"carbon.togglemsg\")\n            .literal(\"on\", \"allow\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandToggleMsgDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = handler.sender().carbonPlayer();\n                sender.ignoringDirectMessages(false);\n                this.carbonMessages.whispersToggledOn(sender);\n            })\n            .build();\n\n        this.commandManager.command(toggleOn);\n\n        final var toggleOff = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .permission(\"carbon.togglemsg\")\n            .literal(\"off\", \"ignore\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandToggleMsgDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = handler.sender().carbonPlayer();\n                sender.ignoringDirectMessages(true);\n                this.carbonMessages.whispersToggledOff(sender);\n            })\n            .build();\n\n        this.commandManager.command(toggleOff);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/UnignoreCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.UUIDParser.uuidParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class UnignoreCommand extends CarbonCommand {\n\n    private final UserManager<?> users;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n    private final ParserFactory parserFactory;\n\n    @Inject\n    public UnignoreCommand(\n        final UserManager<?> userManager,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final ParserFactory parserFactory\n    ) {\n        this.users = userManager;\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n        this.parserFactory = parserFactory;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"unignore\", \"unblock\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"unignore\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            // TODO: Filter, and only show muted players, but allow inputting any player name.\n            .optional(\"player\", this.parserFactory.carbonPlayer(),\n                richDescription(this.carbonMessages.commandUnignoreArgumentPlayer()))\n            .flag(this.commandManager.flagBuilder(\"uuid\")\n                .withAliases(\"u\")\n                .withDescription(richDescription(this.carbonMessages.commandUnignoreArgumentUUID()))\n                .withComponent(uuidParser())\n            )\n            .permission(\"carbon.ignore.unignore\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandUnignoreDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = handler.sender().carbonPlayer();\n                final CarbonPlayer target;\n\n                if (handler.contains(\"player\")) {\n                    target = handler.get(\"player\");\n                } else if (handler.flags().contains(\"uuid\")) {\n                    target = this.users.user(handler.flags().get(\"uuid\")).join();\n                } else {\n                    this.carbonMessages.ignoreTargetInvalid(sender);\n                    return;\n                }\n\n                if (!sender.ignoring(target)) {\n                    this.carbonMessages.notIgnored(sender, target.displayName());\n                    return;\n                }\n\n                sender.ignoring(target, false);\n                this.carbonMessages.noLongerIgnoring(sender, target.displayName());\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/UnmuteCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.UUIDParser.uuidParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class UnmuteCommand extends CarbonCommand {\n\n    private final UserManager<?> users;\n    private final CarbonServer server;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n    private final ParserFactory parserFactory;\n\n    @Inject\n    public UnmuteCommand(\n        final UserManager<?> userManager,\n        final CarbonServer server,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final ParserFactory parserFactory\n    ) {\n        this.users = userManager;\n        this.server = server;\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n        this.parserFactory = parserFactory;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"unmute\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"unmute\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .optional(\"player\", this.parserFactory.carbonPlayer(),\n                richDescription(this.carbonMessages.commandUnmuteArgumentPlayer()))\n            .flag(this.commandManager.flagBuilder(\"uuid\")\n                .withAliases(\"u\")\n                .withDescription(richDescription(this.carbonMessages.commandUnmuteArgumentUUID()))\n                .withComponent(uuidParser())\n            )\n            .permission(\"carbon.mute.unmute\")\n            .senderType(Commander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandUnmuteDescription()))\n            .handler(handler -> {\n                final Commander sender = handler.sender();\n                final CarbonPlayer target;\n\n                if (handler.contains(\"player\")) {\n                    target = handler.get(\"player\");\n                } else if (handler.flags().contains(\"uuid\")) {\n                    target = this.users.user(handler.flags().get(\"uuid\")).join();\n                } else {\n                    this.carbonMessages.unmuteNoTarget(sender);\n                    // TODO: send command syntax\n                    return;\n                }\n\n                this.carbonMessages.unmuteAlertRecipient(target);\n                this.carbonMessages.unmuteAlertPlayers(this.server.console(), target.displayName());\n\n                for (final var player : this.server.players()) {\n                    if (player.equals(target)) {\n                        continue;\n                    }\n\n                    if (!player.hasPermission(\"carbon.mute.notify\")) {\n                        continue;\n                    }\n\n                    this.carbonMessages.unmuteAlertPlayers(player, target.displayName());\n                }\n\n                target.muteExpiration(0);\n                target.muted(false);\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/UpdateUsernameCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport java.util.Objects;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.common.users.WrappedCarbonPlayer;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.parser.standard.UUIDParser.uuidParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class UpdateUsernameCommand extends CarbonCommand {\n\n    private final UserManager<?> userManager;\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages messageService;\n    private final ParserFactory parserFactory;\n    private final ProfileResolver profileResolver;\n\n    @Inject\n    public UpdateUsernameCommand(\n        final UserManager<?> userManager,\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages messageService,\n        final ParserFactory parserFactory,\n        final ProfileResolver profileResolver\n    ) {\n        this.userManager = userManager;\n        this.commandManager = commandManager;\n        this.messageService = messageService;\n        this.parserFactory = parserFactory;\n        this.profileResolver = profileResolver;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"updateusername\", \"updatename\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"updateusername\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .optional(\"player\", this.parserFactory.carbonPlayer(),\n                richDescription(this.messageService.commandUpdateUsernameArgumentPlayer()))\n            .flag(this.commandManager.flagBuilder(\"uuid\")\n                .withAliases(\"u\")\n                .withDescription(richDescription(this.messageService.commandUpdateUsernameArgumentUUID()))\n                .withComponent(uuidParser())\n            )\n            .permission(\"carbon.updateusername\")\n            .senderType(Commander.class)\n            .commandDescription(richDescription(this.messageService.commandUpdateUsernameDescription()))\n            .handler(handler -> {\n                final CarbonPlayer sender = ((PlayerCommander) handler.sender()).carbonPlayer();\n                CarbonPlayer target;\n\n                if (handler.contains(\"player\")) {\n                    target = handler.get(\"player\");\n                } else if (handler.flags().contains(\"uuid\")) {\n                    target = this.userManager.user(handler.flags().get(\"uuid\")).join();\n                } else {\n                    target = sender;\n                }\n\n                if (target instanceof WrappedCarbonPlayer wrappedPlayer) {\n                    target = wrappedPlayer.carbonPlayerCommon();\n                } else if (!(target instanceof CarbonPlayerCommon)) {\n                    this.messageService.usernameNotUpdated(sender);\n                    return;\n                }\n\n                this.messageService.usernameFetching(sender);\n                final CarbonPlayer finalTarget = target;\n                this.profileResolver.resolveName(target.uuid()).thenAccept(name -> {\n                    Objects.requireNonNull(name, \"Unable to fetch username for player.\");\n\n                    ((CarbonPlayerCommon) finalTarget).username(name);\n                    this.messageService.usernameUpdated(sender, name);\n                });\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/commands/WhisperCommand.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.commands;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Provider;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonPrivateChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.RawChat;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ParserFactory;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.command.argument.CarbonPlayerParser;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.event.events.CarbonPrivateChatEventImpl;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messages.SourcedAudience;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.messaging.packets.PacketFactory;\nimport net.draycia.carbon.common.messaging.packets.WhisperPacket;\nimport net.draycia.carbon.common.users.NetworkUsers;\nimport net.draycia.carbon.common.util.CloudUtils;\nimport net.kyori.adventure.chat.ChatType;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.sound.Sound;\nimport net.kyori.adventure.text.Component;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.minecraft.signed.SignedString;\n\nimport static org.incendo.cloud.minecraft.extras.RichDescription.richDescription;\nimport static org.incendo.cloud.minecraft.signed.SignedGreedyStringParser.signedGreedyStringParser;\n\n@DefaultQualifier(NonNull.class)\npublic final class WhisperCommand extends CarbonCommand {\n\n    private final CommandManager<Commander> commandManager;\n    private final CarbonMessages carbonMessages;\n    private final ParserFactory parserFactory;\n    private final WhisperHandler whisper;\n\n    @Inject\n    public WhisperCommand(\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final ParserFactory parserFactory,\n        final WhisperHandler whisper\n    ) {\n        this.commandManager = commandManager;\n        this.carbonMessages = carbonMessages;\n        this.parserFactory = parserFactory;\n        this.whisper = whisper;\n    }\n\n    @Override\n    public CommandSettings defaultCommandSettings() {\n        return new CommandSettings(\"whisper\", \"w\", \"message\", \"msg\", \"m\", \"tell\");\n    }\n\n    @Override\n    public Key key() {\n        return Key.key(\"carbon\", \"whisper\");\n    }\n\n    @Override\n    public void init() {\n        final var command = this.commandManager.commandBuilder(this.commandSettings().name(), this.commandSettings().aliases())\n            .required(\"player\", this.parserFactory.carbonPlayer(), richDescription(this.carbonMessages.commandWhisperArgumentPlayer()))\n            .required(\"message\", signedGreedyStringParser(), richDescription(this.carbonMessages.commandWhisperArgumentMessage()))\n            .permission(\"carbon.whisper.message\")\n            .senderType(PlayerCommander.class)\n            .commandDescription(richDescription(this.carbonMessages.commandWhisperDescription()))\n            .handler(ctx -> {\n                final CarbonPlayer sender = ctx.sender().carbonPlayer();\n\n                if (sender.muted()) {\n                    this.carbonMessages.muteCannotSpeak(sender);\n                    return;\n                }\n\n                final SignedString message = ctx.get(\"message\");\n                final CarbonPlayer recipient = ctx.get(\"player\");\n\n                this.whisper.whisper(sender, recipient, message, ctx.parsingContext(\"player\").consumedInput());\n            })\n            .build();\n\n        this.commandManager.command(command);\n    }\n\n    public static final class WhisperHandler {\n\n        private final Logger logger;\n        private final CarbonMessages messages;\n        private final ConfigManager configManager;\n        private final Provider<MessagingManager> messaging;\n        private final PacketFactory packetFactory;\n        private final UserManager<? extends CarbonPlayer> userManager;\n        private final CarbonServer server;\n        private final CarbonEventHandler events;\n        private final NetworkUsers network;\n        private final Key rawChatKey;\n\n        @Inject\n        private WhisperHandler(\n            final Logger logger,\n            final CarbonMessages messages,\n            final ConfigManager configManager,\n            final Provider<MessagingManager> messaging,\n            final PacketFactory packetFactory,\n            final UserManager<?> userManager,\n            final CarbonServer server,\n            final CarbonEventHandler events,\n            final NetworkUsers network,\n            @RawChat final Key rawChatKey\n        ) {\n            this.logger = logger;\n            this.messages = messages;\n            this.configManager = configManager;\n            this.messaging = messaging;\n            this.packetFactory = packetFactory;\n            this.userManager = userManager;\n            this.server = server;\n            this.events = events;\n            this.network = network;\n            this.rawChatKey = rawChatKey;\n        }\n\n        public void whisper(\n            final CarbonPlayer sender,\n            final CarbonPlayer recipient,\n            final SignedString message\n        ) {\n            this.whisper(sender, recipient, message, null);\n        }\n\n        public void whisper(\n            final CarbonPlayer sender,\n            final CarbonPlayer recipient,\n            final SignedString message,\n            final @Nullable String recipientInputString\n        ) {\n            if (sender.equals(recipient)) {\n                this.messages.whisperSelfError(sender, sender.displayName());\n                return;\n            }\n\n            if (sender.ignoringDirectMessages() && !sender.hasPermission(\"carbon.togglemsg.exempt\")) {\n                this.messages.whisperIgnoringAll(sender);\n                return;\n            }\n\n            if (!sender.hasPermission(\"carbon.whisper.send\")) {\n                this.messages.whisperNoPermissionSend(sender);\n                return;\n            }\n\n            final String recipientUsername = recipient.username();\n            if (!this.network.online(recipient) || !sender.awareOf(recipient) && !sender.hasPermission(\"carbon.whisper.vanished\")) {\n                final var exception = new CarbonPlayerParser.ParseException(\n                    recipientInputString == null ? recipientUsername : recipientInputString,\n                    this.messages\n                );\n                this.messages.errorCommandArgumentParsing(sender, CloudUtils.message(exception));\n                return;\n            }\n\n            final boolean localRecipient = recipient.online();\n\n            if (sender.ignoring(recipient)) {\n                this.messages.whisperIgnoringTarget(sender, recipient.displayName());\n                return;\n            }\n\n            if (recipient.ignoring(sender)) {\n                this.messages.whisperTargetIgnoring(sender, recipient.displayName());\n                return;\n            }\n\n            if (recipient.ignoringDirectMessages() && !sender.hasPermission(\"carbon.togglemsg.exempt\")) {\n                this.messages.whisperTargetIgnoringDMs(sender, recipient.displayName());\n                return;\n            }\n\n            final Component senderDisplayName = sender.displayName();\n            final Component recipientDisplayName = recipient.displayName();\n\n            final CarbonPrivateChatEvent privateChatEvent = new CarbonPrivateChatEventImpl(sender, recipient, Component.text(message.string()));\n            this.events.emit(privateChatEvent);\n\n            if (privateChatEvent.cancelled()) {\n                this.messages.whisperError(sender, sender.displayName(), recipient.displayName());\n                return;\n            }\n\n            final String senderUsername = sender.username();\n            message.sendMessage(\n                sender,\n                ChatType.chatType(this.rawChatKey),\n                this.messages.whisperSender(SourcedAudience.of(sender, sender), senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, recipient.uuid(), privateChatEvent.message())\n            );\n            if (localRecipient) {\n                if (!recipient.hasPermission(\"carbon.whisper.receive\")) {\n                    this.messages.whisperNoPermissionReceive(sender);\n                    return;\n                }\n\n                message.sendMessage(\n                    recipient,\n                    ChatType.chatType(this.rawChatKey),\n                    this.messages.whisperRecipient(SourcedAudience.of(sender, recipient), senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, recipient.uuid(), privateChatEvent.message())\n                );\n            }\n            WhisperCommand.broadcastWhisperSpy(this.server, this.messages, senderUsername, senderDisplayName,\n                recipientUsername, recipientDisplayName, privateChatEvent.message());\n            this.messages.whisperConsoleLog(this.server.console(), senderUsername, senderDisplayName,\n                recipientUsername, recipientDisplayName, privateChatEvent.message());\n\n            final @Nullable Sound messageSound = this.configManager.primaryConfig().messageSound();\n            if (localRecipient && messageSound != null && recipient.hasPermission(\"carbon.whisper.ping_sounds\")) {\n                recipient.playSound(messageSound, Sound.Emitter.self());\n            }\n\n            sender.lastWhisperTarget(recipient.uuid());\n            sender.whisperReplyTarget(recipient.uuid());\n            if (localRecipient) {\n                recipient.whisperReplyTarget(sender.uuid());\n            } else {\n                this.messaging.get().queuePacket(() -> this.packetFactory.whisperPacket(sender.uuid(), recipient.uuid(), privateChatEvent.message()));\n            }\n        }\n\n        public void handlePacket(final WhisperPacket packet) {\n            final @Nullable CarbonPlayer recipient = this.server.players().stream()\n                .filter(p -> p.uuid().equals(packet.to()))\n                .findFirst()\n                .orElse(null);\n            if (recipient == null) {\n                return;\n            }\n            this.userManager.user(packet.from()).thenAccept(sender -> {\n                final String senderUsername = sender.username();\n                final Component senderDisplayName = sender.displayName();\n                final String recipientUsername = recipient.username();\n                final Component recipientDisplayName = recipient.displayName();\n\n                if (!recipient.hasPermission(\"carbon.whisper.receive\")) {\n                    this.messages.whisperNoPermissionReceive(sender);\n                    return;\n                }\n\n                final CarbonPrivateChatEvent privateChatEvent = new CarbonPrivateChatEventImpl(sender, recipient, packet.message());\n                this.events.emit(privateChatEvent);\n\n                if (privateChatEvent.cancelled()) {\n                    this.messages.whisperError(sender, sender.displayName(), recipient.displayName());\n                    return;\n                }\n\n                recipient.whisperReplyTarget(sender.uuid());\n                SourcedAudience.of(sender, recipient).sendMessage(\n                    this.messages.whisperRecipient(SourcedAudience.of(sender, recipient), senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, recipient.uuid(), privateChatEvent.message())\n                );\n                WhisperCommand.broadcastWhisperSpy(this.server, this.messages, senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, privateChatEvent.message());\n                this.messages.whisperConsoleLog(this.server.console(), senderUsername, senderDisplayName, recipientUsername, recipientDisplayName, privateChatEvent.message());\n                final @Nullable Sound messageSound = this.configManager.primaryConfig().messageSound();\n                if (messageSound != null && recipient.hasPermission(\"carbon.whisper.ping_sounds\")) {\n                    recipient.playSound(messageSound, Sound.Emitter.self());\n                }\n            }).exceptionally(ex -> {\n                this.logger.warn(\"Failed to handle whisper packet {}\", packet, ex);\n                return null;\n            });\n        }\n    }\n\n    public static void broadcastWhisperSpy(\n        final CarbonServer server,\n        final CarbonMessages messages,\n        final String senderUsername,\n        final Component senderDisplayName,\n        final String recipientUsername,\n        final Component recipientDisplayName,\n        final Component message\n    ) {\n        for (final CarbonPlayer player : server.players()) {\n            if (player.spying() && !player.username().equals(senderUsername) && !player.username().equals(recipientUsername)) {\n                messages.whisperRecipientSpy(player, senderUsername,\n                    senderDisplayName, recipientUsername, recipientDisplayName, message);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/exception/CommandCompleted.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.exception;\n\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.ComponentLike;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class CommandCompleted extends ComponentException {\n\n    private static final long serialVersionUID = 1352215898395889299L;\n\n    private CommandCompleted(final @Nullable Component message) {\n        super(message);\n    }\n\n    public static CommandCompleted withoutMessage() {\n        return new CommandCompleted(null);\n    }\n\n    public static CommandCompleted withMessage(final ComponentLike message) {\n        return new CommandCompleted(message.asComponent());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/command/exception/ComponentException.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.command.exception;\n\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.ComponentLike;\nimport net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;\nimport net.kyori.adventure.util.ComponentMessageThrowable;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class ComponentException extends RuntimeException implements ComponentMessageThrowable {\n\n    private static final long serialVersionUID = 132203031250316968L;\n\n    private final @Nullable Component message;\n\n    protected ComponentException(final @Nullable Component message) {\n        this.message = message;\n    }\n\n    public static ComponentException withoutMessage() {\n        return new ComponentException(null);\n    }\n\n    public static ComponentException withMessage(final ComponentLike message) {\n        return new ComponentException(message.asComponent());\n    }\n\n    @Override\n    public @Nullable Component componentMessage() {\n        return this.message;\n    }\n\n    @Override\n    public String getMessage() {\n        return PlainTextComponentSerializer.plainText().serializeOr(this.message, \"No message.\");\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/config/ClearChatSettings.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.config;\n\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\n\n@ConfigSerializable\n@DefaultQualifier(NonNull.class)\n// TODO: Config versioning. This isn't automatically added to existing configs otherwise.\npublic class ClearChatSettings {\n\n    @Comment(\"The message that will be sent to each player.\")\n    private String message = \"\";\n\n    @Comment(\"The number of times the message will be sent to each player.\")\n    private int iterations = 50;\n\n    @Comment(\"The message to be sent after chat is cleared.\")\n    private String broadcast = \"<gold>Chat has been cleared by </gold><green><display_name><green><gold>.\";\n\n    private @MonotonicNonNull Component messageComponent = null;\n\n    public Component message() {\n        if (this.messageComponent == null) {\n            this.messageComponent = MiniMessage.miniMessage().deserialize(this.message);\n        }\n\n        return this.messageComponent;\n    }\n\n    public int iterations() {\n        return this.iterations;\n    }\n\n    public Component broadcast(final Component displayName, final String username) {\n        return MiniMessage.miniMessage().deserialize(this.broadcast,\n            TagResolver.builder()\n                .tag(\"display_name\", Tag.selfClosingInserting(displayName))\n                .tag(\"username\", Tag.selfClosingInserting(Component.text(username)))\n                .build());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/config/CommandConfig.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.config;\n\nimport java.util.Map;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.util.CloudUtils;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\n@ConfigSerializable\n@DefaultQualifier(MonotonicNonNull.class)\npublic class CommandConfig {\n\n    private Map<Key, CommandSettings> settings = CloudUtils.defaultCommandSettings();\n\n    public CommandConfig() {\n\n    }\n\n    public CommandConfig(final Map<Key, CommandSettings> settings) {\n        this.settings = settings;\n    }\n\n    public Map<Key, CommandSettings> settings() {\n        return this.settings;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/config/ConfigHeader.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.config;\n\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n/**\n * Tells Carbon's config loading logic to use {@link #value()}\n * as the header in {@link org.spongepowered.configurate.ConfigurationOptions}\n * when loading/saving this type as the root node of a document.\n */\n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface ConfigHeader {\n    String value();\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/config/ConfigManager.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.config;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport io.leangen.geantyref.GenericTypeReflector;\nimport java.io.IOException;\nimport java.lang.reflect.Type;\nimport java.nio.file.Path;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Set;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.common.DataDirectory;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.event.events.CarbonReloadEvent;\nimport net.draycia.carbon.common.integration.Integration;\nimport net.draycia.carbon.common.serialisation.gson.LocaleSerializerConfigurate;\nimport net.draycia.carbon.common.util.FileUtil;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.serializer.configurate4.ConfigurateComponentSerializer;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.CommentedConfigurationNode;\nimport org.spongepowered.configurate.CommentedConfigurationNodeIntermediary;\nimport org.spongepowered.configurate.ConfigurateException;\nimport org.spongepowered.configurate.ConfigurationNode;\nimport org.spongepowered.configurate.hocon.HoconConfigurationLoader;\nimport org.spongepowered.configurate.loader.ConfigurationLoader;\nimport org.spongepowered.configurate.objectmapping.ObjectMapper;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\nimport org.spongepowered.configurate.objectmapping.meta.Processor;\nimport org.spongepowered.configurate.transformation.ConfigurationTransformation;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic final class ConfigManager {\n\n    public static final String CONFIG_VERSION_KEY = \"config-version\";\n    private static final String PRIMARY_CONFIG_FILE_NAME = \"config.conf\";\n    private static final String COMMAND_SETTINGS_FILE_NAME = \"command-settings.conf\";\n\n    private final Path dataDirectory;\n    private final LocaleSerializerConfigurate locale;\n    private final Logger logger;\n    private final Set<Integration.ConfigMeta> integrations;\n\n    private volatile @MonotonicNonNull PrimaryConfig primaryConfig = null;\n\n    @Inject\n    private ConfigManager(\n        final CarbonEventHandler events,\n        @DataDirectory final Path dataDirectory,\n        final LocaleSerializerConfigurate locale,\n        final Logger logger,\n        final Set<Integration.ConfigMeta> integrations\n    ) {\n        this.dataDirectory = dataDirectory;\n        this.locale = locale;\n        this.logger = logger;\n        this.integrations = integrations;\n\n        events.subscribe(CarbonReloadEvent.class, -100, true, event -> this.reloadPrimaryConfig());\n    }\n\n    public static @Nullable String extractHeader(final Type type) {\n        if (type instanceof Class<?> cls) {\n            final @Nullable ConfigHeader h = cls.getAnnotation(ConfigHeader.class);\n            if (h == null) {\n                return null;\n            }\n            return h.value();\n        } else {\n            return extractHeader(GenericTypeReflector.erase(type));\n        }\n    }\n\n    public void reloadPrimaryConfig() {\n        this.logger.info(\"Reloading configuration....\");\n        final @Nullable PrimaryConfig load = this.load(PrimaryConfig.class, PRIMARY_CONFIG_FILE_NAME);\n        if (load != null) {\n            this.primaryConfig = load;\n        } else {\n            this.logger.error(\"Failed to reload primary config, see above for further details\");\n        }\n    }\n\n    public PrimaryConfig primaryConfig() {\n        if (this.primaryConfig == null) {\n            synchronized (this) {\n                if (this.primaryConfig == null) {\n                    this.logger.info(\"Loading configuration....\");\n                    final @Nullable PrimaryConfig load = this.load(PrimaryConfig.class, PRIMARY_CONFIG_FILE_NAME);\n                    if (load == null) {\n                        throw new RuntimeException(\"Failed to initialize primary config, see above for further details\");\n                    }\n                    this.primaryConfig = load;\n                }\n            }\n        }\n\n        return this.primaryConfig;\n    }\n\n    public Map<Key, CommandSettings> loadCommandSettings() {\n        final @Nullable CommandConfig load = this.load(CommandConfig.class, COMMAND_SETTINGS_FILE_NAME);\n        if (load == null) {\n            throw new RuntimeException(\"Failed to initialize command settings, see above for further details\");\n        }\n        return load.settings();\n    }\n\n    public ConfigurationLoader<?> configurationLoader(final Path file, final @Nullable String header) {\n        return HoconConfigurationLoader.builder()\n            .prettyPrinting(true)\n            .defaultOptions(opts -> {\n                final ConfigurateComponentSerializer serializer =\n                    ConfigurateComponentSerializer.configurate();\n\n                return opts.shouldCopyDefaults(true)\n                    .header(header)\n                    .serializers(serializerBuilder ->\n                        serializerBuilder.registerAll(serializer.serializers())\n                            .register(Locale.class, this.locale)\n                            .register(IntegrationConfigContainer.class, new IntegrationConfigContainer.Serializer(this.integrations))\n                            .registerAnnotatedObjects(ObjectMapper.factoryBuilder()\n                                .addProcessor(Comment.class, overrideComments())\n                                .build()));\n            })\n            .path(file)\n            .build();\n    }\n\n    private static Processor.Factory<Comment, Object> overrideComments() {\n        return (data, fieldType) -> (value, destination) -> {\n            if (destination instanceof final CommentedConfigurationNodeIntermediary<?> commented) {\n                commented.comment(data.value());\n            }\n        };\n    }\n\n    public <T> @Nullable T load(final Class<T> clazz, final String fileName) {\n        final Path file = this.dataDirectory.resolve(fileName);\n        try {\n            FileUtil.mkParentDirs(file);\n        } catch (final IOException ex) {\n            this.logger.error(\"Failed to create parent directories for '{}'\", file, ex);\n            return null;\n        }\n\n        final var loader = this.configurationLoader(file, extractHeader(clazz));\n\n        try {\n            final var node = loader.load();\n            try {\n                clazz.getDeclaredMethod(\"upgrade\", ConfigurationNode.class).invoke(null, node);\n            } catch (final NoSuchMethodException ignore) {\n            }\n            final @Nullable T config = node.get(clazz);\n            if (config == null) {\n                throw new ConfigurateException(node, \"Failed to deserialize \" + clazz.getName() + \" from node\");\n            }\n            node.set(clazz, config);\n            loader.save(node);\n            return config;\n        } catch (final ConfigurateException | ReflectiveOperationException exception) {\n            this.logger.error(\"Failed to load config '{}'\", file, exception);\n            return null;\n        }\n    }\n\n    public static <N extends ConfigurationNode> void configVersionComment(\n        final N rootNode,\n        final ConfigurationTransformation.Versioned versionedTransformation\n    ) {\n        final ConfigurationNode versionNode = rootNode.node(versionedTransformation.versionKey());\n        if (!versionNode.virtual() && versionNode instanceof CommentedConfigurationNode commented) {\n            commented.comment(\"Used internally to track changes to the config. Do not edit manually!\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/config/DatabaseSettings.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.config;\n\nimport java.util.concurrent.TimeUnit;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\n\n@DefaultQualifier(Nullable.class)\n@ConfigSerializable\npublic class DatabaseSettings {\n\n    public DatabaseSettings() {\n    }\n\n    public DatabaseSettings(final String url, final String username, final String password) {\n        this.url = url;\n        this.username = username;\n        this.password = password;\n    }\n\n    @Comment(\"\"\"\n        JDBC URL. Suggested defaults for each DB:\n        MySQL: jdbc:mysql://host:3306/DB\n        MariaDB: jdbc:mariadb://host:3306/DB\n        PostgreSQL: jdbc:postgresql://host:5432/database\"\"\")\n    private String url = \"jdbc:mysql://localhost:3306/carbon\";\n\n    @Comment(\"The connection username.\")\n    private String username = \"username\";\n\n    @Comment(\"The connection password.\")\n    private String password = \"password\";\n\n    @Comment(\"Settings for the connection pool. This is an advanced configuration that most users won't need to touch.\")\n    private ConnectionPool connectionPool = new ConnectionPool();\n\n    public String url() {\n        return this.url;\n    }\n\n    public String url(final String url) {\n        return this.url = url;\n    }\n\n    public String username() {\n        return this.username;\n    }\n\n    public String password() {\n        return this.password;\n    }\n\n    public ConnectionPool connectionPool() {\n        return this.connectionPool;\n    }\n\n    @ConfigSerializable\n    public static class ConnectionPool {\n        public int maximumPoolSize = 8;\n        public int minimumIdle = 8;\n        public long maximumLifetime = TimeUnit.MINUTES.toMillis(30);\n        public long keepaliveTime = 0L;\n        public long connectionTimeout = TimeUnit.SECONDS.toMillis(30);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/config/IntegrationConfigContainer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.config;\n\nimport java.lang.reflect.Type;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport net.draycia.carbon.common.integration.Integration;\nimport net.draycia.carbon.common.util.Exceptions;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.BasicConfigurationNode;\nimport org.spongepowered.configurate.ConfigurationNode;\nimport org.spongepowered.configurate.ConfigurationOptions;\nimport org.spongepowered.configurate.serialize.SerializationException;\nimport org.spongepowered.configurate.serialize.TypeSerializer;\n\n@DefaultQualifier(NonNull.class)\npublic final class IntegrationConfigContainer {\n\n    private final Map<String, Object> map = new HashMap<>();\n\n    @SuppressWarnings(\"unchecked\")\n    public <C> C config(final Integration.ConfigMeta meta) {\n        return (C) Objects.requireNonNull(this.map.get(meta.name()));\n    }\n\n    public static final class Serializer implements TypeSerializer<IntegrationConfigContainer> {\n        private final List<Integration.ConfigMeta> sections;\n\n        public Serializer(final Set<Integration.ConfigMeta> integrations) {\n            this.sections = integrations.stream()\n                .sorted(Comparator.comparing(Integration.ConfigMeta::name))\n                .toList();\n        }\n\n        @Override\n        public IntegrationConfigContainer deserialize(final Type type, final ConfigurationNode node) throws SerializationException {\n            final IntegrationConfigContainer container = new IntegrationConfigContainer();\n            for (final Integration.ConfigMeta section : this.sections) {\n                final @Nullable Object value = node.node(section.name()).get(section.type());\n                Objects.requireNonNull(value);\n                container.map.put(section.name(), value);\n            }\n            return container;\n        }\n\n        @Override\n        public void serialize(final Type type, final @Nullable IntegrationConfigContainer obj, final ConfigurationNode node) throws SerializationException {\n            Objects.requireNonNull(obj);\n\n            for (final Object key : node.childrenMap().keySet()) {\n                node.removeChild(key);\n            }\n\n            for (final Integration.ConfigMeta section : this.sections) {\n                node.node(section.name()).set(section.type(), obj.map.get(section.name()));\n            }\n        }\n\n        @Override\n        public IntegrationConfigContainer emptyValue(final Type specificType, final ConfigurationOptions options) {\n            final IntegrationConfigContainer container = new IntegrationConfigContainer();\n            for (final Integration.ConfigMeta section : this.sections) {\n                final @Nullable Object value;\n                try {\n                    value = options.serializers().get(section.type())\n                        .deserialize(section.type(), BasicConfigurationNode.root());\n                } catch (final Exception e) {\n                    throw Exceptions.rethrow(e);\n                }\n                Objects.requireNonNull(value);\n                container.map.put(section.name(), value);\n            }\n            return container;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/config/MessagingSettings.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.config;\n\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\n\n@DefaultQualifier(MonotonicNonNull.class)\n@ConfigSerializable\npublic class MessagingSettings {\n\n    @Comment(\"Whether cross-server messaging is enabled\")\n    private boolean enabled = false;\n\n    @Comment(\"One of: RABBITMQ, NATS, REDIS\")\n    private MessagingManager.@NonNull BrokerType brokerType = MessagingManager.BrokerType.NONE;\n\n    private String url = \"127.0.0.1\";\n\n    private int port = 5672; // RabbitMQ 5672, NATS 4222, Redis 6379\n\n    @Comment(\"RabbitMQ VHost\")\n    private String vhost = \"/\"; // RabbitMQ only\n\n    @Comment(\"NATS credentials file\")\n    private String credentialsFile = \"\"; // NATS only\n\n    @Comment(\"RabbitMQ username\")\n    private String username = \"username\"; // RabbitMQ only\n\n    @Comment(\"RabbitMQ and Redis password\")\n    private String password = \"password\"; // RabbitMQ and Redis only\n\n    public boolean enabled() {\n        return this.enabled;\n    }\n\n    public MessagingManager.@NonNull BrokerType brokerType() {\n        return this.brokerType;\n    }\n\n    public String url() {\n        return this.url;\n    }\n\n    public int port() {\n        return this.port;\n    }\n\n    public String vhost() {\n        return this.vhost;\n    }\n\n    public String credentialsFile() {\n        return this.credentialsFile;\n    }\n\n    public String username() {\n        return this.username;\n    }\n\n    public String password() {\n        return this.password;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/config/PingSettings.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.config;\n\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.sound.Sound;\nimport net.kyori.adventure.text.format.NamedTextColor;\nimport net.kyori.adventure.text.format.TextColor;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\n\n@ConfigSerializable\npublic class PingSettings {\n\n    @Comment(\"The color your name will be when another player mentions you.\")\n    private TextColor highlightTextColor = NamedTextColor.YELLOW;\n\n    private String prefix = \"@\";\n\n    private boolean playSound = false;\n    private Key name = Key.key(\"block.anvil.use\");\n    private Sound.Source source = Sound.Source.MASTER;\n    private float volume = 1.0f; // 0.0 -> infinity\n    private float pitch = 1.0f; // 0.0 -> 2.0\n\n    public TextColor highlightTextColor() {\n        return this.highlightTextColor;\n    }\n\n    public boolean playSound() {\n        return this.playSound;\n    }\n\n    public Key name() {\n        return this.name;\n    }\n\n    public String prefix() {\n        return this.prefix;\n    }\n\n    public Sound.Source source() {\n        return this.source;\n    }\n\n    public float volume() {\n        return this.volume;\n    }\n\n    public float pitch() {\n        return this.pitch;\n    }\n\n    public Sound sound() {\n        return Sound.sound(this.name, this.source, this.volume, this.pitch);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/config/PrimaryConfig.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.config;\n\nimport io.github.miniplaceholders.api.MiniPlaceholders;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil;\nimport net.draycia.carbon.common.util.Exceptions;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.sound.Sound;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.TextReplacementConfig;\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.ConfigurateException;\nimport org.spongepowered.configurate.ConfigurationNode;\nimport org.spongepowered.configurate.NodePath;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\nimport org.spongepowered.configurate.transformation.ConfigurationTransformation;\n\n@ConfigSerializable\n@DefaultQualifier(NonNull.class)\npublic class PrimaryConfig {\n\n    @Comment(\"The default locale for plugin messages.\")\n    private Locale defaultLocale = Locale.US;\n\n    @Comment(\"\"\"\n        The default channel that new players will be in when they join.\n        If the channel is not found or the player cannot use the channel, they will speak in basic non-channel chat.\"\"\")\n    private Key defaultChannel = Key.key(\"carbon\", \"global\");\n\n    @Comment(\"Returns you to the default channel when you use a channel's command while you have that channel active.\")\n    private boolean returnToDefaultChannel = false;\n\n    @Comment(\"\"\"\n        The service that will be used to store and load player information.\n        One of: JSON, H2, MYSQL, PSQL\n        Note: If you choose MYSQL or PSQL make sure you configure the \"database-settings\" section of this file!\"\"\")\n    private StorageType storageType = StorageType.JSON;\n\n    @Comment(\"\"\"\n        When \"storage-type\" is set to MYSQL or PSQL, this section configures the database connection.\n        If JSON or H2 storage is used, this section can be ignored.\"\"\")\n    private DatabaseSettings databaseSettings = new DatabaseSettings();\n\n    @Comment(\"Settings for cross-server messaging\")\n    private MessagingSettings messagingSettings = new MessagingSettings();\n\n    private NicknameSettings nicknameSettings = new NicknameSettings();\n\n    @Comment(\"\"\"\n        Plugin-wide custom placeholders.\n        These will be parsed in all messages rendered and sent by Carbon.\n        This includes chat, command feedback, and others.\n        Make sure to close your tags so they do not bleed into other formats.\n        Only a single pass is done so custom placeholders will not work within each other.\"\"\")\n    private Map<String, String> customPlaceholders = Map.of();\n\n    @Comment(\"The suggestions shown when using the TAB key in chat.\")\n    private List<String> customChatSuggestions = List.of();\n\n    @Comment(\"The placeholders replaced in chat messages, this WILL work with chat previews.\")\n    private Map<String, String> chatPlaceholders = Map.of();\n\n    @Comment(\"Basic regex based chat filter.\")\n    private Map<String, String> chatFilter = Map.of();\n\n    @Comment(\"Player toggled chat filter. Useful for more mild profanity.\")\n    private Map<String, String> optionalChatFilter = Map.of();\n\n    @Comment(\"Various settings related to pinging players in channels.\")\n    private PingSettings pingSettings = new PingSettings();\n\n    private PartySettings partyChat = new PartySettings();\n\n    @Comment(\"Sound for receiving a direct message\") // TODO migrate to a field name that makes more sense\n    private @Nullable Sound messageSound = Sound.sound(\n        Key.key(\"entity.experience_orb.pickup\"),\n        Sound.Source.MASTER,\n        1.0F,\n        1.0F\n    );\n\n    @Comment(\"Settings for the clear chat command\")\n    private ClearChatSettings clearChatSettings = new ClearChatSettings();\n\n    @Comment(\"Disables spying when the user doesn't have spy permissions\")\n    private boolean spyPermissionRequired = true;\n\n    @Comment(\"Alerts the user when they can no longer spy due to lacking permissions\")\n    private boolean spyDisabledMessage = false;\n\n    @Comment(\"Settings for integrations with other plugins/mods. Settings only apply when the relevant plugin/mod is present.\")\n    private IntegrationConfigContainer integrations;\n\n    @Comment(\"Whether Carbon should check for updates using the GitHub API on startup.\")\n    private boolean updateChecker = true;\n\n    public NicknameSettings nickname() {\n        return this.nicknameSettings;\n    }\n\n    public Locale defaultLocale() {\n        return this.defaultLocale;\n    }\n\n    public Key defaultChannel() {\n        return this.defaultChannel;\n    }\n\n    public boolean returnToDefaultChannel() {\n        return this.returnToDefaultChannel;\n    }\n\n    public StorageType storageType() {\n        return this.storageType;\n    }\n\n    public DatabaseSettings databaseSettings() {\n        return this.databaseSettings;\n    }\n\n    public MessagingSettings messagingSettings() {\n        return this.messagingSettings;\n    }\n\n    private String applyPlaceholders(String message, final Map<String, String> placeholders) {\n        for (final var entry : placeholders.entrySet()) {\n            message = message.replace(\"<\" + entry.getKey() + \">\",\n                entry.getValue());\n        }\n        return message;\n    }\n\n    public String applyCustomPlaceholders(final String message) {\n        return this.applyPlaceholders(message, this.customPlaceholders);\n    }\n\n    public @Nullable List<String> customChatSuggestions() {\n        return this.customChatSuggestions;\n    }\n\n    // Maybe we only need the two chat filters? Having 4 placeholder systems seems excessive.\n    public String applyChatPlaceholders(final String message) {\n        return this.applyPlaceholders(message, this.chatPlaceholders);\n    }\n\n    private Component applyFilters(Component message, final Map<String, String> filters) {\n        final TagResolver.Builder resolver = TagResolver.builder();\n\n        if (MiniPlaceholdersUtil.miniPlaceholdersLoaded()) {\n            resolver.resolver(MiniPlaceholders.globalPlaceholders());\n        }\n\n        for (final Map.Entry<String, String> entry : filters.entrySet()) {\n            message = message.replaceText(TextReplacementConfig.builder()\n                .match(entry.getKey()).replacement(MiniMessage.miniMessage().deserialize(entry.getValue(), resolver.build())).build());\n        }\n\n        return message;\n    }\n\n    public Component applyChatFilters(final Component message) {\n        return this.applyFilters(message, this.chatFilter);\n    }\n\n    public Component applyOptionalChatFilters(final Component message) {\n        return this.applyFilters(message, this.optionalChatFilter);\n    }\n\n    public PingSettings pings() {\n        return this.pingSettings;\n    }\n\n    public PartySettings partyChat() {\n        return this.partyChat;\n    }\n\n    public @Nullable Sound messageSound() {\n        return this.messageSound;\n    }\n\n    public ClearChatSettings clearChatSettings() {\n        return this.clearChatSettings;\n    }\n\n    public boolean spyPermissionRequired() {\n        return this.spyPermissionRequired;\n    }\n\n    public boolean spyDisabledMessage() {\n        return this.spyDisabledMessage;\n    }\n\n    public IntegrationConfigContainer integrations() {\n        return this.integrations;\n    }\n\n    public boolean updateChecker() {\n        return this.updateChecker;\n    }\n\n    @SuppressWarnings(\"unused\")\n    public static void upgrade(final ConfigurationNode node) {\n        final ConfigurationTransformation.VersionedBuilder builder = ConfigurationTransformation.versionedBuilder()\n            .versionKey(ConfigManager.CONFIG_VERSION_KEY);\n\n        final ConfigurationTransformation initial = ConfigurationTransformation.builder()\n            .addAction(NodePath.path(\"use-carbon-nicknames\"), (path, value) -> new Object[]{\"nickname-settings\", \"use-carbon-nicknames\"})\n            .build();\n        builder.addVersion(0, initial);\n\n        final ConfigurationTransformation one = ConfigurationTransformation.builder()\n            .addAction(NodePath.path(\"party-chat\"), (path, value) -> new Object[]{\"party-chat\", \"enabled\"})\n            .build();\n        builder.addVersion(1, one);\n\n        final ConfigurationTransformation.Versioned upgrader = builder.build();\n        final int from = upgrader.version(node);\n        try {\n            upgrader.apply(node);\n        } catch (final ConfigurateException e) {\n            Exceptions.rethrow(e);\n        }\n\n        ConfigManager.configVersionComment(node, upgrader);\n    }\n\n    @ConfigSerializable\n    public static final class NicknameSettings {\n\n        @Comment(\"Whether Carbon's nickname management should be used. Disable this if you wish to have another plugin manage nicknames.\")\n        private boolean useCarbonNicknames = true;\n\n        @Comment(\"Paper only. Updates the player's display name in the tab list to match their nickname.\")\n        private boolean updateTabList = true;\n\n        @Comment(\"Minimum number of characters in nickname (excluding formatting).\")\n        private int minLength = 3;\n\n        @Comment(\"Maximum number of characters in nickname (excluding formatting).\")\n        private int maxLength = 16;\n\n        private List<String> blackList = List.of(\"notch\", \"admin\");\n\n        @Comment(\"Regex pattern nicknames must match in order to be applied, can be bypassed with the permission 'carbon.nickname.filter'.\")\n        private String filter = \"^[a-zA-Z0-9_]*$\";\n\n        @Comment(\"Format used when displaying nicknames.\")\n        public String format = \"<hover:show_text:'<gray>@</gray><username>'><gray>~</gray><nickname></hover>\";\n\n        @Comment(\"Whether to skip applying 'format' when a nickname matches a players username, only differing in decoration.\")\n        public boolean skipFormatWhenNameMatches = true;\n\n        public boolean useCarbonNicknames() {\n            return this.useCarbonNicknames;\n        }\n\n        public boolean updateTabList() {\n            return this.updateTabList;\n        }\n\n        public List<String> blackList() {\n            return this.blackList;\n        }\n\n        public String filter() {\n            return this.filter;\n        }\n\n        public int minLength() {\n            return this.minLength;\n        }\n\n        public int maxLength() {\n            return this.maxLength;\n        }\n    }\n\n    @ConfigSerializable\n    public static final class PartySettings {\n\n        @Comment(\"Whether party chat is enabled\")\n        public boolean enabled = true;\n\n        public int expireInvitesAfterSeconds = 45;\n\n        public boolean playSound = false;\n\n        @Comment(\"Sound for receiving a party message\")\n        public @Nullable Sound messageSound = Sound.sound(\n            Key.key(\"entity.experience_orb.pickup\"),\n            Sound.Source.MASTER,\n            1.0F,\n            1.0F\n        );\n    }\n\n    public enum StorageType {\n        JSON,\n        MYSQL,\n        PSQL,\n        H2\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/event/CancellableImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.event;\n\nimport net.draycia.carbon.api.event.Cancellable;\n\npublic class CancellableImpl implements Cancellable, com.sasorio.event.Cancellable {\n\n    private boolean cancelled = false;\n\n    @Override\n    public boolean cancelled() {\n        return this.cancelled;\n    }\n\n    @Override\n    public void cancelled(final boolean cancelled) {\n        this.cancelled = cancelled;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/event/CarbonEventHandlerImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.event;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport com.sasorio.event.EventConfig;\nimport com.sasorio.event.EventSubscriber;\nimport com.sasorio.event.EventSubscription;\nimport com.sasorio.event.bus.EventBus;\nimport com.sasorio.event.bus.SimpleEventBus;\nimport com.sasorio.event.registry.EventRegistry;\nimport com.sasorio.event.registry.SimpleEventRegistry;\nimport net.draycia.carbon.api.event.Cancellable;\nimport net.draycia.carbon.api.event.CarbonEvent;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.CarbonEventSubscriber;\nimport net.draycia.carbon.api.event.CarbonEventSubscription;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Event handler for listening to and emitting carbon events.\n *\n * @since 1.0.0\n */\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic final class CarbonEventHandlerImpl implements CarbonEventHandler {\n\n    private final Logger logger;\n\n    @Inject\n    private CarbonEventHandlerImpl(final Logger logger) {\n        this.logger = logger;\n    }\n\n    private final EventRegistry<CarbonEvent> eventRegistry = new SimpleEventRegistry<>(CarbonEvent.class);\n    private final EventBus<CarbonEvent> eventBus = new SimpleEventBus<>(this.eventRegistry, this::onException);\n\n    private <E> void onException(final EventBus<? super E> eventBus, final EventSubscription<? super E> subscription, final E event, final Throwable throwable) {\n        final Object subscriber = subscription.subscriber() instanceof SubscriberWrapper<?> wrapped\n            ? wrapped.carbon\n            : subscription.subscriber();\n        this.logger.warn(\"Exception posting event '{}' to subscriber '{}'\", event, subscriber, throwable);\n    }\n\n    @Override\n    public <T extends CarbonEvent> CarbonEventSubscription<T> subscribe(\n        final Class<T> eventClass,\n        final CarbonEventSubscriber<T> subscriber\n    ) {\n        return new CarbonEventSubscriptionImpl<>(\n            eventClass,\n            subscriber,\n            this.eventRegistry.subscribe(eventClass, new SubscriberWrapper<>(subscriber, true))\n        );\n    }\n\n    // TODO: support EventConfig#exact()\n    @Override\n    public <T extends CarbonEvent> CarbonEventSubscription<T> subscribe(\n        final Class<T> eventClass,\n        final int order,\n        final boolean acceptsCancelled,\n        final CarbonEventSubscriber<T> subscriber\n    ) {\n        final EventConfig eventConfig = EventConfig.defaults().order(order).acceptsCancelled(acceptsCancelled);\n        return new CarbonEventSubscriptionImpl<>(\n            eventClass,\n            subscriber,\n            this.eventRegistry.subscribe(eventClass, eventConfig, new SubscriberWrapper<>(subscriber, acceptsCancelled))\n        );\n    }\n\n    @Override\n    public <T extends CarbonEvent> void emit(final T event) {\n        this.eventBus.post(event);\n    }\n\n    private record SubscriberWrapper<T extends CarbonEvent>(\n        CarbonEventSubscriber<T> carbon,\n        boolean acceptsCancelled\n    ) implements EventSubscriber<T> {\n\n        @Override\n        public void on(final T event) throws Throwable {\n            // Our events implement seiama Cancellable; but API consumers won't be able to do that\n            if (!this.acceptsCancelled && event instanceof Cancellable cancellable && cancellable.cancelled()) {\n                return;\n            }\n            this.carbon.on(event);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/event/CarbonEventSubscriptionImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.event;\n\nimport com.sasorio.event.EventSubscription;\nimport net.draycia.carbon.api.event.CarbonEvent;\nimport net.draycia.carbon.api.event.CarbonEventSubscriber;\nimport net.draycia.carbon.api.event.CarbonEventSubscription;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\nrecord CarbonEventSubscriptionImpl<T extends CarbonEvent>(\n    Class<T> event,\n    CarbonEventSubscriber<T> subscriber,\n    EventSubscription<T> backingSubscription\n) implements CarbonEventSubscription<T> {\n\n    @Override\n    public void dispose() {\n        this.backingSubscription.dispose();\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/event/events/CarbonChatEventImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.event.events;\n\nimport java.util.List;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.KeyedRenderer;\nimport net.draycia.carbon.common.event.CancellableImpl;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.chat.SignedMessage;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Event that's called when chat components are rendered for online players.\n *\n * @since 2.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic class CarbonChatEventImpl extends CancellableImpl implements CarbonChatEvent {\n\n    private final List<KeyedRenderer> renderers;\n    private final CarbonPlayer sender;\n    private final Component originalMessage;\n    private final List<? extends Audience> recipients;\n    private final @MonotonicNonNull ChatChannel chatChannel;\n    private final @MonotonicNonNull SignedMessage signedMessage;\n    public final boolean origin;\n    private Component message;\n\n    public CarbonChatEventImpl(\n        final CarbonPlayer sender,\n        final Component originalMessage,\n        final List<? extends Audience> recipients,\n        final List<KeyedRenderer> renderers,\n        final @Nullable ChatChannel chatChannel,\n        final @Nullable SignedMessage signedMessage\n    ) {\n        this(sender, originalMessage, recipients, renderers, chatChannel, signedMessage, true);\n    }\n\n    public CarbonChatEventImpl(\n        final CarbonPlayer sender,\n        final Component originalMessage,\n        final List<? extends Audience> recipients,\n        final List<KeyedRenderer> renderers,\n        final @Nullable ChatChannel chatChannel,\n        final @Nullable SignedMessage signedMessage,\n        final boolean origin\n    ) {\n        this.sender = sender;\n        this.originalMessage = originalMessage;\n        this.message = originalMessage;\n        this.recipients = recipients;\n        this.renderers = renderers;\n        this.chatChannel = chatChannel;\n        this.signedMessage = signedMessage;\n        this.origin = origin;\n    }\n\n    @Override\n    public List<KeyedRenderer> renderers() {\n        return this.renderers;\n    }\n\n    @Override\n    public @MonotonicNonNull SignedMessage signedMessage() {\n        return this.signedMessage;\n    }\n\n    @Override\n    public CarbonPlayer sender() {\n        return this.sender;\n    }\n\n    @Override\n    public Component originalMessage() {\n        return this.originalMessage;\n    }\n\n    @Override\n    public Component message() {\n        return this.message;\n    }\n\n    @Override\n    public void message(final Component message) {\n        this.message = message;\n    }\n\n    @Override\n    public @MonotonicNonNull ChatChannel chatChannel() {\n        return this.chatChannel;\n    }\n\n    @Override\n    public List<? extends Audience> recipients() {\n        return this.recipients;\n    }\n\n    public Component renderFor(final Audience viewer) {\n        Component renderedMessage = this.message();\n        for (final var renderer : this.renderers()) {\n            renderedMessage = renderer.render(this.sender, viewer, renderedMessage, this.message());\n        }\n        return renderedMessage;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/event/events/CarbonEarlyChatEvent.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.event.events;\n\nimport net.draycia.carbon.api.event.Cancellable;\nimport net.draycia.carbon.api.event.CarbonEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\n\npublic class CarbonEarlyChatEvent implements CarbonEvent, Cancellable {\n\n    private final CarbonPlayer sender;\n    private String message;\n    private boolean cancelled = false;\n\n    public CarbonEarlyChatEvent(final CarbonPlayer sender, final String message) {\n        this.sender = sender;\n        this.message = message;\n    }\n\n    public CarbonPlayer sender() {\n        return this.sender;\n    }\n\n    public String message() {\n        return this.message;\n    }\n\n    public void message(final String message) {\n        this.message = message;\n    }\n\n    @Override\n    public boolean cancelled() {\n        return this.cancelled;\n    }\n\n    @Override\n    public void cancelled(final boolean cancelled) {\n        this.cancelled = cancelled;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/event/events/CarbonPrivateChatEventImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.event.events;\n\nimport java.util.Objects;\nimport net.draycia.carbon.api.event.events.CarbonPrivateChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.event.CancellableImpl;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Called whenever a player privately messages another player.\n *\n * @since 3.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic class CarbonPrivateChatEventImpl extends CancellableImpl implements CarbonPrivateChatEvent {\n\n    private final CarbonPlayer sender;\n    private final CarbonPlayer recipient;\n\n    private Component message;\n\n    public CarbonPrivateChatEventImpl(final CarbonPlayer sender, final CarbonPlayer recipient, final Component message) {\n        this.sender = sender;\n        this.recipient = recipient;\n        this.message = message;\n    }\n\n    public void message(final Component message) {\n        this.message = Objects.requireNonNull(message);\n    }\n\n    public Component message() {\n        return this.message;\n    }\n\n    public CarbonPlayer sender() {\n        return this.sender;\n    }\n\n    public CarbonPlayer recipient() {\n        return this.recipient;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/event/events/CarbonReloadEvent.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.event.events;\n\nimport net.draycia.carbon.api.event.CarbonEvent;\n\npublic class CarbonReloadEvent implements CarbonEvent {\n\n\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/event/events/ChannelRegisterEventImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.event.events;\n\nimport java.util.Set;\nimport net.draycia.carbon.api.event.events.CarbonChannelRegisterEvent;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic record ChannelRegisterEventImpl(\n    CarbonChannelRegistry channelRegistry,\n    Set<Key> registered\n) implements CarbonChannelRegisterEvent {\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/event/events/ChannelSwitchEventImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.event.events;\n\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.event.events.ChannelSwitchEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class ChannelSwitchEventImpl implements ChannelSwitchEvent {\n\n    private final CarbonPlayer player;\n    private ChatChannel chatChannel;\n\n    public ChannelSwitchEventImpl(final CarbonPlayer player, final ChatChannel chatChannel) {\n        this.player = player;\n        this.chatChannel = chatChannel;\n    }\n\n    @Override\n    public CarbonPlayer player() {\n        return this.player;\n    }\n\n    @Override\n    public ChatChannel channel() {\n        return this.chatChannel;\n    }\n\n    @Override\n    public void channel(final ChatChannel chatChannel) {\n        this.chatChannel = chatChannel;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/integration/Integration.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.integration;\n\nimport java.lang.reflect.Type;\nimport net.draycia.carbon.common.config.ConfigManager;\n\npublic interface Integration {\n\n    boolean eligible();\n\n    void register();\n\n    interface ConfigMeta {\n        Type type();\n\n        String name();\n\n        record ConfigMetaRecord(Type type, String name) implements ConfigMeta {}\n    }\n\n    static ConfigMeta configMeta(final String name, final Type type) {\n        return new ConfigMeta.ConfigMetaRecord(type, name);\n    }\n\n    default <C> C config(final ConfigManager configManager, final ConfigMeta meta) {\n        return configManager.primaryConfig().integrations().config(meta);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/integration/miniplaceholders/MiniPlaceholdersExpansion.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.integration.miniplaceholders;\n\nimport com.google.inject.Inject;\nimport io.github.miniplaceholders.api.Expansion;\nimport java.util.UUID;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.identity.Identity;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class MiniPlaceholdersExpansion {\n\n    private final UserManager<?> userManager;\n    private final ChannelRegistry channels;\n\n    @Inject\n    private MiniPlaceholdersExpansion(\n        final UserManager<?> userManager,\n        final ChannelRegistry channels\n    ) {\n        this.userManager = userManager;\n        this.channels = channels;\n    }\n\n    public void registerExpansion() {\n        final Expansion expansion = Expansion.builder(\"carbonchat\")\n            .audiencePlaceholder(\"party\", (audience, queue, ctx) -> {\n                if (!hasId(audience)) {\n                    return null;\n                }\n                return Tag.selfClosingInserting(this.partyName(id(audience)));\n            })\n            .audiencePlaceholder(\"nickname\", (audience, queue, ctx) -> {\n                if (!hasId(audience)) {\n                    return null;\n                }\n                if (queue.hasNext() && queue.pop().lowerValue().equals(\"plain\")) {\n                    return Tag.selfClosingInserting(this.toPlain(this.nickname(id(audience))));\n                }\n                return Tag.selfClosingInserting(this.nickname(id(audience)));\n            })\n            .audiencePlaceholder(\"displayname\", (audience, queue, ctx) -> {\n                if (!hasId(audience)) {\n                    return null;\n                }\n                if (queue.hasNext() && queue.pop().lowerValue().equals(\"plain\")) {\n                    return Tag.selfClosingInserting(this.toPlain(this.displayName(id(audience))));\n                }\n                return Tag.selfClosingInserting(this.displayName(id(audience)));\n            })\n            .audiencePlaceholder(\"channel_key\", (audience, queue, ctx) -> {\n                if (!hasId(audience)) {\n                    return null;\n                }\n                return Tag.preProcessParsed(this.selectedChannelKey(id(audience)));\n            })\n            .build();\n        expansion.register();\n    }\n\n    private static boolean hasId(final Audience audience) {\n        return audience.get(Identity.UUID).isPresent();\n    }\n\n    private static UUID id(final Audience audience) {\n        return audience.get(Identity.UUID).orElseThrow();\n    }\n\n    private Component partyName(final UUID id) {\n        final @Nullable Party party = this.userManager.user(id).thenCompose(CarbonPlayer::party).join();\n        return party == null ? Component.empty() : party.name();\n    }\n\n    private Component displayName(final UUID id) {\n        final CarbonPlayer carbonPlayer = this.userManager.user(id).join();\n        return carbonPlayer.displayName();\n    }\n\n    private Component nickname(final UUID id) {\n        final CarbonPlayer carbonPlayer = this.userManager.user(id).join();\n        final @Nullable Component nickname = carbonPlayer.nickname();\n        return nickname == null ? Component.text(carbonPlayer.username()) : nickname;\n    }\n\n    private String selectedChannelKey(final UUID id) {\n        final CarbonPlayer carbonPlayer = this.userManager.user(id).join();\n        final @Nullable ChatChannel selected = carbonPlayer.selectedChannel();\n        if (selected != null) {\n            return selected.key().asString();\n        }\n        return this.channels.defaultKey().asString();\n    }\n\n    private Component toPlain(final Component input) {\n        return Component.text(PlainTextComponentSerializer.plainText().serialize(input));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/integration/miniplaceholders/MiniPlaceholdersIntegration.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.integration.miniplaceholders;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Provider;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.Integration;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\n\n@DefaultQualifier(NonNull.class)\npublic final class MiniPlaceholdersIntegration implements Integration {\n\n    private final Provider<MiniPlaceholdersExpansion> expansionProvider;\n    private final Config config;\n\n    @Inject\n    private MiniPlaceholdersIntegration(\n        final ConfigManager configManager,\n        final Provider<MiniPlaceholdersExpansion> expansionProvider\n    ) {\n        this.expansionProvider = expansionProvider;\n        this.config = this.config(configManager, configMeta());\n    }\n\n    @Override\n    public boolean eligible() {\n        return MiniPlaceholdersUtil.miniPlaceholdersLoaded();\n    }\n\n    @Override\n    public void register() {\n        this.expansionProvider.get().registerExpansion();\n    }\n\n    public static ConfigMeta configMeta() {\n        return Integration.configMeta(\"miniplaceholders\", MiniPlaceholdersIntegration.Config.class);\n    }\n\n    @ConfigSerializable\n    public static final class Config {\n        @Comment(\"Enabling relational placeholders may require reworking your format configs due to the way MiniPlaceholders v3 works.\")\n        public boolean relationalPlaceholders = false;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/integration/miniplaceholders/MiniPlaceholdersUtil.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.integration.miniplaceholders;\n\nimport io.github.miniplaceholders.api.MiniPlaceholders;\nimport io.github.miniplaceholders.api.types.RelationalAudience;\nimport java.util.Objects;\nimport net.kyori.adventure.audience.Audience;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\npublic final class MiniPlaceholdersUtil {\n\n    private static byte miniPlaceholdersLoaded = -1;\n\n    private MiniPlaceholdersUtil() {\n    }\n\n    public static boolean miniPlaceholdersLoaded() {\n        if (miniPlaceholdersLoaded == -1) {\n            try {\n                final String name = MiniPlaceholders.class.getName();\n                Objects.requireNonNull(name);\n                miniPlaceholdersLoaded = 1;\n            } catch (final NoClassDefFoundError error) {\n                miniPlaceholdersLoaded = 0;\n            }\n        }\n        return miniPlaceholdersLoaded == 1;\n    }\n\n    public static Audience wrapAudiences(final MiniPlaceholdersIntegration.@Nullable Config config, final @Nullable Audience recipient, final Audience sender) {\n        if (!miniPlaceholdersLoaded() || config == null || !config.relationalPlaceholders) {\n            return sender;\n        }\n        return wrapAudiences_(recipient, sender);\n    }\n\n    private static Audience wrapAudiences_(final @Nullable Audience recipient, final Audience sender) {\n        if (recipient == null) {\n            return sender;\n        }\n        return RelationalAudience.from(recipient, sender);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/ChatListenerInternal.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport java.util.ArrayList;\nimport java.util.Iterator;\nimport java.util.List;\nimport net.draycia.carbon.api.channels.ChannelPermissionResult;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.KeyedRenderer;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.event.events.CarbonChatEventImpl;\nimport net.draycia.carbon.common.event.events.CarbonEarlyChatEvent;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messages.TagPermissions;\nimport net.draycia.carbon.common.users.WrappedCarbonPlayer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.chat.SignedMessage;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.ComponentIteratorType;\nimport net.kyori.adventure.text.TextComponent;\nimport net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic abstract class ChatListenerInternal {\n\n    private final ConfigManager configManager;\n    private final CarbonMessages carbonMessages;\n    private final CarbonEventHandler carbonEventHandler;\n\n    protected ChatListenerInternal(\n        final CarbonEventHandler carbonEventHandler,\n        final CarbonMessages carbonMessages,\n        final ConfigManager configManager\n    ) {\n        this.configManager = configManager;\n        this.carbonMessages = carbonMessages;\n        this.carbonEventHandler = carbonEventHandler;\n    }\n\n    protected @Nullable CarbonChatEventImpl prepareAndEmitChatEvent(final CarbonPlayer sender, final Component originalMessage, final @Nullable SignedMessage signedMessage) {\n        final CarbonPlayer.ChannelMessage channelMessage = sender.channelForMessage(originalMessage);\n        final ChatChannel channel = channelMessage.channel();\n\n        return this.prepareAndEmitChatEvent(sender, channelMessage.message(), signedMessage, channel);\n    }\n\n    protected @Nullable CarbonChatEventImpl prepareAndEmitChatEvent(final CarbonPlayer sender, final Component message, final @Nullable SignedMessage signedMessage, final ChatChannel channel) {\n        if (!sender.hasPermission(\"carbon.cooldown.exempt\") && channel.cooldown() > 0) {\n            final long currentMillis = System.currentTimeMillis();\n            final long expiresAt = channel.playerCooldown(sender);\n\n            if (currentMillis < expiresAt) {\n                // Round up, or the player can be told they have 0 seconds remaining\n                final long remaining = (long) Math.ceil((double) (expiresAt - currentMillis) / 1000);\n                this.carbonMessages.channelCooldown(sender, remaining);\n                return null;\n            }\n\n            channel.startCooldown(sender);\n        }\n\n        if (sender.leftChannels().contains(channel.key())) {\n            sender.joinChannel(channel);\n            this.carbonMessages.channelJoined(sender);\n        }\n\n        final List<KeyedRenderer> renderers = new ArrayList<>();\n        renderers.add(KeyedRenderer.keyedRenderer(Key.key(\"carbon\", \"default\"), channel));\n\n        final List<Audience> recipients = channel.recipients(sender);\n\n        final var chatEvent = new CarbonChatEventImpl(sender, message, recipients, renderers, channel, signedMessage);\n\n        this.carbonEventHandler.emit(chatEvent);\n\n        return chatEvent;\n    }\n\n    protected @Nullable CarbonEarlyChatEvent prepareAndEmitPreChatEvent(final CarbonPlayer sender, final Component originalMessage) {\n        final CarbonPlayer.ChannelMessage channelMessage = sender.channelForMessage(originalMessage);\n        final ChatChannel channel = channelMessage.channel();\n\n        final ChannelPermissionResult permitted = channel.permissions().speechPermitted(sender);\n        if (!permitted.permitted()) {\n            sender.sendMessage(permitted.reason());\n            return null;\n        }\n\n        final String plainContent = PlainTextComponentSerializer.plainText().serialize(channelMessage.message());\n        final String content = this.configManager.primaryConfig().applyChatPlaceholders(plainContent);\n\n        final CarbonEarlyChatEvent earlyChatEvent = new CarbonEarlyChatEvent(sender, content);\n        this.carbonEventHandler.emit(earlyChatEvent);\n\n        return earlyChatEvent;\n    }\n\n    protected @Nullable Component parseTags(final CarbonPlayer sender, final String format) {\n        final Component message;\n\n        if (sender instanceof WrappedCarbonPlayer wrapped) {\n            message = wrapped.parseMessageTags(format);\n        } else {\n            message = TagPermissions.parseTags(sender, TagPermissions.MESSAGE, format, sender::hasPermission);\n        }\n\n        if (probablyBlank(message)) {\n            return null;\n        }\n\n        return message;\n    }\n\n    private static boolean probablyBlank(final Component component) {\n        final Iterator<Component> it = component.iterator(ComponentIteratorType.DEPTH_FIRST);\n        while (it.hasNext()) {\n            final Component c = it.next();\n            if (!(c instanceof TextComponent text)) {\n                // Assume non-text components probably aren't blank\n                return false;\n            } else if (!text.content().isBlank()) {\n                // Found some content, definitely not blank\n                return false;\n            }\n        }\n        // Likely blank\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/DeafenHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class DeafenHandler implements Listener {\n\n    @Inject\n    public DeafenHandler(final CarbonEventHandler events) {\n        events.subscribe(CarbonChatEvent.class, -10, false, event -> {\n            if (!event.sender().deafened()) {\n                return;\n            }\n\n            event.recipients().removeIf(entry -> entry instanceof CarbonPlayer carbonPlayer &&\n                carbonPlayer.deafened());\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/FilterHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.event.events.CarbonPrivateChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.KeyedRenderer;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\n\npublic class FilterHandler implements Listener {\n\n    private final ConfigManager configManager;\n\n    @Inject\n    public FilterHandler(\n        final CarbonEventHandler events,\n        final ConfigManager configManager\n    ) {\n        this.configManager = configManager;\n\n        events.subscribe(CarbonPrivateChatEvent.class, -9, false, event -> {\n            Component message = this.configManager.primaryConfig().applyChatFilters(event.message());\n\n            if (event.recipient().applyOptionalChatFilters()) {\n                message = this.configManager.primaryConfig().applyOptionalChatFilters(message);\n            }\n\n            event.message(message);\n        });\n\n        events.subscribe(CarbonChatEvent.class, -9, false, event -> {\n            event.message(this.configManager.primaryConfig().applyChatFilters(event.message()));\n\n            event.renderers().add(KeyedRenderer.keyedRenderer(Key.key(\"carbon\", \"filter\"), ($, recipient, message, $$$) -> {\n                if (recipient instanceof CarbonPlayer carbonPlayer) {\n                    if (carbonPlayer.applyOptionalChatFilters()) {\n                        return this.configManager.primaryConfig().applyOptionalChatFilters(message);\n                    }\n                }\n\n                return message;\n            }));\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/HyperlinkHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static net.draycia.carbon.common.util.Strings.URL_REPLACEMENT_CONFIG;\n\n@DefaultQualifier(NonNull.class)\npublic class HyperlinkHandler implements Listener {\n\n    @Inject\n    public HyperlinkHandler(final CarbonEventHandler events) {\n        events.subscribe(CarbonChatEvent.class, -7, false, event -> {\n            if (event.sender().hasPermission(\"carbon.chatlinks\")) {\n                event.message(event.message().replaceText(URL_REPLACEMENT_CONFIG.get()));\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/IgnoreHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class IgnoreHandler implements Listener {\n\n    @Inject\n    public IgnoreHandler(final CarbonEventHandler events) {\n        events.subscribe(CarbonChatEvent.class, -8, false, event -> {\n            event.recipients().removeIf(entry -> entry instanceof CarbonPlayer carbonPlayer &&\n                carbonPlayer.ignoring(event.sender()));\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/ItemLinkHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.event.events.CarbonPrivateChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.InventorySlot;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.TextReplacementConfig;\n\npublic class ItemLinkHandler implements Listener {\n\n    @Inject\n    public ItemLinkHandler(final CarbonEventHandler events) {\n        events.subscribe(CarbonChatEvent.class, -8, false, event -> {\n            event.message(this.handleChatEvent(event.sender(), event.message()));\n        });\n\n        events.subscribe(CarbonPrivateChatEvent.class, -8, false, event -> {\n            event.message(this.handleChatEvent(event.sender(), event.message()));\n        });\n    }\n\n    private Component handleChatEvent(final CarbonPlayer sender, Component message) {\n        if (!sender.hasPermission(\"carbon.itemlink\")) {\n            return message;\n        }\n\n        for (final var slot : InventorySlot.SLOTS) {\n            for (final var placeholder : slot.placeholders()) {\n                message = message\n                        .replaceText(TextReplacementConfig.builder()\n                        .matchLiteral(\"<\" + placeholder + \">\")\n                        .replacement(builder -> {\n                            final Component itemComponent = sender.createItemHoverComponent(slot);\n\n                            return itemComponent == null ? builder : itemComponent;\n                        })\n                        .build());\n            }\n        }\n\n        return message;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/Listener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\npublic interface Listener {\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/MessagePacketHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Provider;\nimport java.util.UUID;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.event.events.CarbonChatEventImpl;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.messaging.ServerId;\nimport net.draycia.carbon.common.messaging.packets.ChatMessagePacket;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class MessagePacketHandler implements Listener {\n\n    @Inject\n    public MessagePacketHandler(\n        final CarbonEventHandler events,\n        final @ServerId UUID serverId,\n        final Provider<MessagingManager> messaging\n    ) {\n        events.subscribe(CarbonChatEvent.class, 100, false, event -> {\n            if (!(event instanceof CarbonChatEventImpl e) || !e.origin) {\n                return;\n            }\n            if (event.sender() instanceof ConsoleCarbonPlayer) {\n                return;\n            }\n\n            if (!event.chatChannel().shouldCrossServer()) {\n                return;\n            }\n\n            messaging.get().queuePacket(() -> {\n                final CarbonPlayer sender = event.sender();\n                final Component networkMessage = e.renderFor(sender);\n\n                return new ChatMessagePacket(serverId, sender.uuid(),\n                    event.chatChannel().key(), sender.username(), networkMessage);\n            });\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/MuteHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.KeyedRenderer;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static net.draycia.carbon.api.util.KeyedRenderer.keyedRenderer;\nimport static net.kyori.adventure.key.Key.key;\n\n@DefaultQualifier(NonNull.class)\npublic class MuteHandler implements Listener {\n\n    private final Key muteKey = key(\"carbon\", \"mute\");\n    private CarbonMessages carbonMessages;\n\n    private final KeyedRenderer renderer =\n        keyedRenderer(this.muteKey, (sender, recipient, message, originalMessage) -> {\n            // This is an annoying side effect of the RenderedComponent change\n            final var prefix = this.carbonMessages.muteSpyPrefix(recipient);\n\n            return prefix.append(message);\n        });\n\n    @Inject\n    public MuteHandler(final CarbonEventHandler events, final CarbonMessages carbonMessages) {\n        this.carbonMessages = carbonMessages;\n\n        events.subscribe(CarbonChatEvent.class, -10, false, event -> {\n            if (!event.sender().muted()) {\n                return;\n            }\n\n            event.renderers().add(this.renderer);\n\n            event.recipients().removeIf(entry -> entry instanceof CarbonPlayer carbonPlayer &&\n                !carbonPlayer.spying() && !(entry instanceof ConsoleCarbonPlayer));\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/PartyChatSpyHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport java.util.Set;\nimport java.util.UUID;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.common.channels.PartyChatChannel;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class PartyChatSpyHandler implements Listener {\n\n    @Inject\n    public PartyChatSpyHandler(final CarbonEventHandler events, final CarbonMessages messages, final CarbonServer server) {\n        events.subscribe(CarbonChatEvent.class, -1, false, event -> {\n            if (!(event.chatChannel() instanceof PartyChatChannel)) {\n                return;\n            }\n\n            final @Nullable Party party = event.sender().party().get();\n            final Set<UUID> members = party == null ? Set.of() : party.members();\n            final Component partyName = party == null ? Component.empty() : party.name();\n\n            for (final CarbonPlayer player : server.players()) {\n                if (player.spying() && !members.contains(player.uuid())) {\n                    messages.partySpy(player, event.sender().uuid(), event.sender().displayName(), event.sender().username(), event.message(), partyName);\n                }\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/PartyPingHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.channels.PartyChatChannel;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.identity.Identity;\nimport net.kyori.adventure.sound.Sound;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class PartyPingHandler implements Listener {\n\n    @Inject\n    public PartyPingHandler(final CarbonEventHandler events, final ConfigManager configManager) {\n        events.subscribe(CarbonChatEvent.class, -6, false, event -> {\n            if (!(event.chatChannel() instanceof PartyChatChannel)) {\n                return;\n            }\n\n            final @Nullable Sound sound = configManager.primaryConfig().partyChat().messageSound;\n\n            if (configManager.primaryConfig().partyChat().playSound && sound != null) {\n                for (final Audience recipient : event.recipients()) {\n                    // Don't ping the message sender\n                    if (event.sender().uuid().equals(recipient.get(Identity.UUID).orElse(null))) {\n                        continue;\n                    }\n\n                    if (recipient instanceof CarbonPlayer player && !player.hasPermission(\"carbon.parties.ping_sound\")) {\n                        continue;\n                    }\n\n                    recipient.playSound(sound, Sound.Emitter.self());\n                }\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/PingHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.util.regex.Pattern;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.KeyedRenderer;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.sound.Sound;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.TextReplacementConfig;\nimport net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static net.draycia.carbon.api.util.KeyedRenderer.keyedRenderer;\nimport static net.kyori.adventure.key.Key.key;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic class PingHandler implements Listener {\n\n    private final Key pingKey = key(\"carbon\", \"pings\");\n    private final KeyedRenderer renderer;\n    private final ConfigManager configManager;\n\n    @Inject\n    public PingHandler(final CarbonEventHandler events, final ConfigManager configManager) {\n        this.configManager = configManager;\n        this.renderer = keyedRenderer(this.pingKey, (sender, recipient, message, originalMessage) -> {\n            if (!(recipient instanceof CarbonPlayer recipientPlayer)) {\n                return message;\n            }\n\n            return this.convertPings(recipientPlayer, message);\n        });\n\n        events.subscribe(CarbonChatEvent.class, -9, false, event -> {\n            event.renderers().add(0, this.renderer);\n        });\n    }\n\n    public Component convertPings(final CarbonPlayer recipient, final Component message) {\n        final String prefix = this.configManager.primaryConfig().pings().prefix();\n        final String plainDisplayName = PlainTextComponentSerializer.plainText().serialize(recipient.displayName());\n\n        return message.replaceText(TextReplacementConfig.builder()\n            // \\B(@Username|@Displayname)\\b\n            .match(Pattern.compile(\n                String.format(\n                    \"\\\\B%1$s(%2$s|%3$s)\\\\b\",\n                    Pattern.quote(prefix),\n                    Pattern.quote(recipient.username()),\n                    Pattern.quote(plainDisplayName)),\n                Pattern.CASE_INSENSITIVE))\n            .replacement(matchedText -> {\n                if (this.configManager.primaryConfig().pings().playSound() && recipient.hasPermission(\"carbon.ping_sounds\")) {\n                    recipient.playSound(this.configManager.primaryConfig().pings().sound(), Sound.Emitter.self());\n                }\n\n                return Component.text(matchedText.content()).color(this.configManager.primaryConfig().pings().highlightTextColor());\n            })\n            .build());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/listeners/RadiusListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.listeners;\n\nimport com.google.inject.Inject;\nimport java.util.ArrayList;\nimport java.util.List;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class RadiusListener implements Listener {\n\n    @Inject\n    public RadiusListener(\n        final CarbonEventHandler events,\n        final CarbonMessages carbonMessages\n    ) {\n        events.subscribe(CarbonChatEvent.class, -5, false, event -> {\n            if (event.chatChannel() == null) {\n                return;\n            }\n\n            final double radius = event.chatChannel().radius();\n\n            if (radius < 0) {\n                return;\n            }\n\n            final List<CarbonPlayer> spyingPlayers = new ArrayList<>();\n\n            if (radius == 0) {\n                event.recipients().removeIf(audience -> {\n                    if (audience.equals(event.sender()) || audience instanceof ConsoleCarbonPlayer) {\n                        return false;\n                    }\n\n                    if (audience instanceof CarbonPlayer carbonPlayer) {\n                        final boolean sameWorld = carbonPlayer.sameWorldAs(event.sender());\n\n                        if (!sameWorld && carbonPlayer.spying()) {\n                            spyingPlayers.add(carbonPlayer);\n                        }\n\n                        if (sameWorld && carbonPlayer.vanished()) {\n                            spyingPlayers.add(carbonPlayer);\n                            return true;\n                        }\n\n                        return !sameWorld;\n                    }\n\n                    return false;\n                });\n            } else {\n                event.recipients().removeIf(audience -> {\n                    if (audience.equals(event.sender()) || audience instanceof ConsoleCarbonPlayer) {\n                        return false;\n                    }\n\n                    if (audience instanceof CarbonPlayer carbonPlayer) {\n                        if (!event.sender().sameWorldAs(carbonPlayer)) {\n                            if (carbonPlayer.spying()) {\n                                spyingPlayers.add(carbonPlayer);\n                            }\n                            return true;\n                        }\n\n                        final double distance = carbonPlayer.distanceSquaredFrom(event.sender());\n                        final boolean outOfRange = distance > (radius * radius);\n\n                        if (outOfRange && carbonPlayer.spying()) {\n                            spyingPlayers.add(carbonPlayer);\n                        }\n\n                        if (!outOfRange && carbonPlayer.vanished()) {\n                            spyingPlayers.add(carbonPlayer);\n                            return true;\n                        }\n\n                        return outOfRange;\n                    }\n\n                    return false;\n                });\n            }\n            if (event.recipients().size() <= 2 && event.chatChannel().emptyRadiusRecipientsMessage()) { // the player and cosole\n                carbonMessages.emptyRecipients(event.sender());\n                return;\n            }\n\n            for (final CarbonPlayer player : spyingPlayers) {\n                carbonMessages.radiusSpy(player, event.sender().uuid(), event.chatChannel().key(), event.sender().displayName(),\n                    event.sender().username(), event.message());\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/CarbonMessageRenderer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport net.kyori.moonshine.message.IMessageRenderer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic abstract class CarbonMessageRenderer implements IMessageRenderer<Audience, String, Component, Object> {\n\n    private final RenderForTagResolver.Factory renderForTagResolver;\n\n    protected CarbonMessageRenderer(final RenderForTagResolver.Factory renderForTagResolver) {\n        this.renderForTagResolver = renderForTagResolver;\n    }\n\n    public final IMessageRenderer<SourcedAudience, String, Component, Object> asSourced() {\n        return this::render;\n    }\n\n    @Override\n    public final Component render(\n        final Audience receiver,\n        final String intermediateMessage,\n        final Map<String, ?> resolvedPlaceholders,\n        final @Nullable Method method,\n        final @Nullable Type owner\n    ) {\n        final TagResolver.Builder builder = TagResolver.builder();\n        addResolved(builder, resolvedPlaceholders);\n        builder.resolver(this.renderForTagResolver.create(resolvedPlaceholders));\n        return this.render(receiver, intermediateMessage, builder);\n    }\n\n    protected abstract Component render(\n        Audience receiver,\n        String intermediateMessage,\n        TagResolver.Builder resolverBuilder\n    );\n\n    @SuppressWarnings(\"PatternValidation\")\n    private static void addResolved(final TagResolver.Builder tagResolver, final Map<String, ?> resolvedPlaceholders) {\n        for (final var entry : resolvedPlaceholders.entrySet()) {\n            if (entry.getValue() instanceof Tag tag) {\n                tagResolver.tag(entry.getKey(), tag);\n            } else if (entry.getValue() instanceof TagResolver resolver) {\n                tagResolver.resolver(resolver);\n            } else {\n                throw new IllegalArgumentException(entry.getValue().toString());\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/CarbonMessageSender.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.moonshine.message.IMessageSender;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class CarbonMessageSender implements IMessageSender<Audience, Component> {\n\n    @Override\n    public void send(final Audience receiver, final Component renderedMessage) {\n        if (receiver instanceof SourcedAudience sourcedAudience) {\n            sourcedAudience.recipient().sendMessage(renderedMessage);\n        } else {\n            receiver.sendMessage(renderedMessage);\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/CarbonMessageSource.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.Reader;\nimport java.io.Writer;\nimport java.net.MalformedURLException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.FileSystem;\nimport java.nio.file.FileSystems;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.nio.file.Paths;\nimport java.util.Collections;\nimport java.util.Comparator;\nimport java.util.HashMap;\nimport java.util.LinkedHashSet;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.Set;\nimport java.util.function.Consumer;\nimport java.util.stream.Collectors;\nimport java.util.stream.Stream;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.DataDirectory;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.event.events.CarbonReloadEvent;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.translation.Translator;\nimport net.kyori.moonshine.message.IMessageSource;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic final class CarbonMessageSource implements IMessageSource<Audience, String> {\n\n    private final Locale defaultLocale;\n    private volatile Map<Locale, Properties> locales = Map.of();\n    private final Path pluginJar;\n    private final Logger logger;\n    private final Path dataDirectory;\n\n    @Inject\n    private CarbonMessageSource(\n        final CarbonEventHandler events,\n        final @DataDirectory Path dataDirectory,\n        final ConfigManager configManager,\n        final Logger logger\n    ) throws IOException {\n        this.dataDirectory = dataDirectory;\n        this.pluginJar = pluginJar();\n        this.logger = logger;\n\n        this.defaultLocale = configManager.primaryConfig().defaultLocale();\n\n        this.reloadTranslations();\n\n        events.subscribe(CarbonReloadEvent.class, -99, true, event -> {\n            this.reloadTranslations();\n        });\n    }\n\n    private static @NonNull Path pluginJar() {\n        try {\n            URL sourceUrl = CarbonMessageSource.class.getProtectionDomain().getCodeSource().getLocation();\n            // Some class loaders give the full url to the class, some give the URL to its jar.\n            // We want the containing jar, so we will unwrap jar-schema code sources.\n            if (sourceUrl.getProtocol().equals(\"jar\")) {\n                final int exclamationIdx = sourceUrl.getPath().lastIndexOf('!');\n                if (exclamationIdx != -1) {\n                    sourceUrl = URI.create(sourceUrl.getPath().substring(0, exclamationIdx)).toURL();\n                }\n            }\n            return Paths.get(sourceUrl.toURI());\n        } catch (final URISyntaxException | MalformedURLException ex) {\n            throw new RuntimeException(\"Could not locate plugin jar\", ex);\n        }\n    }\n\n    private void reloadTranslations() throws IOException {\n        final Map<Locale, Properties> map = new HashMap<>();\n\n        final Path localeDirectory = this.dataDirectory.resolve(\"locale\");\n\n        // Create locale directory\n        if (!Files.exists(localeDirectory)) {\n            Files.createDirectories(localeDirectory);\n        }\n\n        this.walkPluginJar(stream -> stream.filter(Files::isRegularFile)\n            .filter(it -> {\n                final String pathString = it.toString();\n                return pathString.startsWith(\"/locale/messages-\")\n                    && pathString.endsWith(\".properties\");\n            })\n            .forEach(localeFile -> {\n                final String localeString = localeString(localeFile);\n                final @Nullable Locale locale = parseLocale(localeString);\n\n                if (locale == null) {\n                    this.logger.warn(\"Unknown locale '{}'?\", localeString);\n                    return;\n                }\n\n                this.tryLoadLocale(map, localeDirectory, localeFile, locale);\n            }));\n\n        try (final Stream<Path> paths = Files.list(localeDirectory)) {\n            paths.filter(Files::isRegularFile).forEach(localeFile -> {\n                final String localeString = localeString(localeFile);\n                final @Nullable Locale locale = parseLocale(localeString);\n\n                if (locale == null) {\n                    this.logger.warn(\"Unknown locale '{}'?\", localeString);\n                    return;\n                }\n\n                if (map.containsKey(locale)) {\n                    return;\n                }\n\n                this.tryLoadLocale(map, localeDirectory, localeFile, locale);\n            });\n        }\n\n        this.logger.info(\"Loaded {} locales: [{}]\", map.size(), map.keySet().stream().map(Locale::toString).collect(Collectors.joining(\", \")));\n        this.locales = Map.copyOf(map);\n    }\n\n    private void tryLoadLocale(final Map<Locale, Properties> map, final Path localeDirectory, final Path localeFile, final Locale locale) {\n        final @Nullable Properties properties = this.readLocale(localeDirectory, localeFile, locale);\n        if (properties != null) {\n            map.put(locale, properties);\n        }\n    }\n\n    private @Nullable Properties readLocale(final Path localeDirectory, final Path localeFile, final Locale locale) {\n        this.logger.debug(\"Found locale {} ({}) in: {}\", locale.getDisplayName(), locale, localeFile);\n\n        final Properties properties = new Properties() {\n            @Override\n            public synchronized Set<Map.Entry<Object, Object>> entrySet() {\n                return Collections.unmodifiableSet(\n                    (Set<? extends Map.Entry<Object, Object>>) super.entrySet()\n                        .stream()\n                        .sorted(Comparator.comparing(entry -> entry.getKey().toString()))\n                        .collect(Collectors.toCollection(LinkedHashSet::new)));\n            }\n        };\n\n        try {\n            this.loadProperties(properties, localeDirectory, localeFile);\n\n            this.logger.debug(\"Successfully loaded locale {} ({})\", locale.getDisplayName(), locale);\n            return properties;\n        } catch (final IOException ex) {\n            this.logger.warn(\"Unable to load locale {} ({}) from source: {}\", locale.getDisplayName(), locale, localeFile, ex);\n            return null;\n        }\n    }\n\n    @Override\n    public String messageOf(final Audience receiver, final String messageKey) {\n        Audience audience = receiver;\n\n        if (audience instanceof SourcedAudience sourced) {\n            audience = sourced.recipient();\n        }\n\n        // Unwrap PlayerCommanders\n        if (audience instanceof PlayerCommander playerCommander) {\n            audience = playerCommander.carbonPlayer();\n        }\n\n        if (audience instanceof CarbonPlayer player) {\n            return this.forPlayer(messageKey, player);\n        } else {\n            return this.fromDefaultLocale(messageKey);\n        }\n    }\n\n    private String forPlayer(final String key, final CarbonPlayer player) {\n        final @Nullable Locale locale = player.locale();\n        if (locale != null) {\n            final var properties = this.locales.get(locale);\n\n            if (properties != null) {\n                final var message = properties.getProperty(key);\n\n                if (message != null) {\n                    return fixCrowdin(message);\n                }\n            }\n        }\n\n        return this.fromDefaultLocale(key);\n    }\n\n    private String fromDefaultLocale(final String key) {\n        final Properties defaultProperties = this.locales.get(this.defaultLocale);\n\n        if (defaultProperties != null) {\n            final String value = defaultProperties.getProperty(key);\n\n            if (value == null) {\n                this.logger.warn(\"No message mapping for key \" + key + \" in default locale \" + this.defaultLocale.getDisplayName());\n                return key;\n            }\n\n            return fixCrowdin(value);\n        }\n\n        return key;\n    }\n\n    private void walkPluginJar(final Consumer<Stream<Path>> user) throws IOException {\n        if (Files.isDirectory(this.pluginJar)) {\n            try (final var stream = Files.walk(this.pluginJar)) {\n                user.accept(stream.map(path -> path.relativize(this.pluginJar)));\n            }\n            return;\n        }\n        try (final FileSystem jar = FileSystems.newFileSystem(this.pluginJar, this.getClass().getClassLoader())) {\n            final Path root = jar.getRootDirectories()\n                .iterator()\n                .next();\n            try (final var stream = Files.walk(root)) {\n                user.accept(stream);\n            }\n        }\n    }\n\n    private void loadProperties(\n        final Properties properties,\n        final Path localeDirectory,\n        final Path sourceFile\n    ) throws IOException {\n        final Path userFile = localeDirectory.resolve(sourceFile.getFileName().toString());\n        final boolean samePath = sourceFile.normalize().toAbsolutePath().equals(userFile.normalize().toAbsolutePath());\n\n        if (Files.isRegularFile(userFile)) {\n            // If the file in the localeDirectory exists, read it to the properties\n            final InputStream inputStream = Files.newInputStream(userFile);\n            try (final Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {\n                properties.load(reader);\n            }\n        } else if (samePath && !Files.isRegularFile(userFile)) {\n            throw new IllegalStateException(\"sourceFile == userFile, and is not a regular file (%s)\".formatted(userFile));\n        }\n\n        boolean write = false;\n\n        // Read the file in the jar and add missing entries\n        if (Files.isRegularFile(sourceFile) && !samePath) {\n            try (final Reader reader = new InputStreamReader(Files.newInputStream(sourceFile), StandardCharsets.UTF_8)) {\n                final Properties packaged = new Properties();\n                packaged.load(reader);\n\n                for (final Map.Entry<Object, Object> entry : packaged.entrySet()) {\n                    write |= properties.putIfAbsent(entry.getKey(), entry.getValue()) == null;\n                }\n            }\n        }\n\n        // todo: copy missing entries from default english locale as well?\n\n        // Write properties back to file\n        if (write) {\n            try (final Writer outputStream = Files.newBufferedWriter(userFile)) {\n                properties.store(outputStream, null);\n            }\n        }\n    }\n\n    private static String localeString(final Path localeFile) {\n        return localeFile.getFileName().toString().substring(\"messages-\".length()).replace(\".properties\", \"\");\n    }\n\n    private static @Nullable Locale parseLocale(String localeString) {\n        // MC uses no_NO when the player selects nb_NO...\n        localeString = localeString.replace(\"nb_NO\", \"no_NO\");\n\n        return Translator.parseLocale(localeString);\n    }\n\n    // Crowdin exports single quotes as double quotes\n    private static String fixCrowdin(final String s) {\n        return s.replace(\"''\", \"'\");\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/CarbonMessages.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport java.util.UUID;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.moonshine.annotation.Message;\nimport net.kyori.moonshine.annotation.Placeholder;\n\npublic interface CarbonMessages {\n\n    /*\n     * =============================================================\n     * ======================== Basic Chat =========================\n     * =============================================================\n     */\n\n    @Message(\"channel.change\")\n    void changedChannels(final Audience audience, final String channel);\n\n    @Message(\"channel.radius.empty_recipients\")\n    void emptyRecipients(final Audience audience);\n\n    @Message(\"channel.radius.spy\")\n    void radiusSpy(\n        Audience audience,\n        @Placeholder UUID uuid,\n        @Placeholder Key channel,\n        @Placeholder(\"display_name\") Component displayName,\n        @Placeholder String username,\n        @Placeholder Component message\n    );\n\n    @Message(\"channel.not_found\")\n    void channelNotFound(final Audience audience);\n\n    @Message(\"channel.not_left\")\n    void channelNotLeft(final Audience audience);\n\n    @Message(\"channel.already_left\")\n    void channelAlreadyLeft(final Audience audience);\n\n    @Message(\"channel.no_permission\")\n    Component channelNoPermission(Audience audience);\n\n    @Message(\"channel.left\")\n    void channelLeft(final Audience audience);\n\n    @Message(\"channel.joined\")\n    void channelJoined(final Audience audience);\n\n    @Message(\"channel.cooldown\")\n    void channelCooldown(final Audience audience, long remaining);\n\n    /*\n     * =============================================================\n     * =========================== Mutes ===========================\n     * =============================================================\n     */\n\n    @Message(\"mute.info.self.muted\")\n    void muteInfoSelfMuted(final Audience audience);\n\n    @Message(\"mute.info.self.not_muted\")\n    void muteInfoSelfNotMuted(final Audience audience);\n\n    @Message(\"mute.info.not_muted\")\n    void muteInfoNotMuted(final Audience audience, final Component target);\n\n    @Message(\"mute.info.muted.duration\")\n    void muteInfoMutedDuration(final Audience audience, final Component target, final Component duration);\n\n    @Message(\"mute.info.muted\")\n    void muteInfoMuted(final Audience audience, final Component target, final boolean muted);\n\n    @Message(\"mute.unmute.alert.target\")\n    void unmuteAlertRecipient(final Audience audience);\n\n    @Message(\"mute.unmute.alert.players\")\n    void unmuteAlertPlayers(final Audience audience, final Component target);\n\n    @Message(\"mute.unmute.no_target\")\n    void unmuteNoTarget(final Audience audience);\n\n    @Message(\"mute.exempt\")\n    void muteExempt(final Audience audience);\n\n    @Message(\"mute.alert.target\")\n    void muteAlertRecipient(final Audience audience);\n\n    @Message(\"mute.alert.players\")\n    void muteAlertPlayers(final Audience audience, final Component target);\n\n    @Message(\"mute.cannot_speak\")\n    void muteCannotSpeak(final Audience audience);\n\n    @Message(\"mute.no_target\")\n    void muteNoTarget(final Audience audience);\n\n    @Message(\"mute.spy.prefix\")\n    Component muteSpyPrefix(final Audience audience);\n\n    @Message(\"mute.alert.target.temp\")\n    void tempMuteAlertRecipient(final Audience audience, final Component duration);\n\n    @Message(\"mute.alert.players.temp\")\n    void tempMuteAlertPlayers(final Audience audience, final Component target, final Component duration);\n\n    @Message(\"duration.hours\")\n    Component durationHours(final int hours, final int minutes, final int seconds);\n\n    @Message(\"duration.days\")\n    Component durationDays(final long days, final int hours, final int minutes, final int seconds);\n\n    /*\n     * =============================================================\n     * ====================== Direct Messages ======================\n     * =============================================================\n     */\n\n    @Message(\"whisper.to\")\n    Component whisperSender(\n        @NotPlaceholder SourcedAudience audience,\n        String senderUsername,\n        Component senderDisplayName,\n        String recipientUsername,\n        Component recipientDisplayName,\n        UUID recipientUuid,\n        Component message\n    );\n\n    @Message(\"whisper.from\")\n    Component whisperRecipient(\n        @NotPlaceholder SourcedAudience audience,\n        String senderUsername,\n        Component senderDisplayName,\n        String recipientUsername,\n        Component recipientDisplayName,\n        UUID recipientUuid,\n        Component message\n    );\n\n    @Message(\"whisper.from.spy\")\n    void whisperRecipientSpy(\n        Audience audience,\n        String senderUsername,\n        Component senderDisplayName,\n        String recipientUsername,\n        Component recipientDisplayName,\n        Component message\n    );\n\n    @Message(\"whisper.console\")\n    void whisperConsoleLog(\n        Audience audience,\n        String senderUsername,\n        Component senderDisplayName,\n        String recipientUsername,\n        Component recipientDisplayName,\n        Component message\n    );\n\n    @Message(\"whisper.error\")\n    void whisperError(\n        final Audience audience,\n        @Placeholder(\"sender_display_name\") final Component senderDisplayName,\n        @Placeholder(\"recipient_display_name\") final Component recipientDisplayName\n    );\n\n    @Message(\"reply.target.missing\")\n    void replyTargetNotSet(final Audience audience, @Placeholder(\"sender_display_name\") final Component senderDisplayName);\n\n    @Message(\"reply.target.self\")\n    void whisperSelfError(final Audience audience, @Placeholder(\"sender_display_name\") final Component senderDisplayName);\n\n    @Message(\"whisper.continue.target_missing\")\n    void whisperTargetNotSet(\n        final Audience audience,\n        @Placeholder(\"sender_display_name\") final Component senderDisplayName\n    );\n\n    @Message(\"whisper.ignoring_all\")\n    void whisperIgnoringAll(final Audience audience);\n\n    @Message(\"whisper.ignoring_target\")\n    void whisperIgnoringTarget(final Audience audience, final Component target);\n\n    @Message(\"whisper.ignored_by_target\")\n    void whisperTargetIgnoring(final Audience audience, final Component target);\n\n    @Message(\"whisper.ignored_dms\")\n    void whisperTargetIgnoringDMs(final Audience audience, final Component target);\n\n    @Message(\"whisper.toggled.on\")\n    void whispersToggledOn(final Audience audience);\n\n    @Message(\"whisper.toggled.off\")\n    void whispersToggledOff(final Audience audience);\n\n    @Message(\"whisper.no_permission.send\")\n    void whisperNoPermissionSend(final Audience audience);\n\n    @Message(\"whisper.no_permission.receive\")\n    void whisperNoPermissionReceive(final Audience audience);\n\n    /*\n     * =============================================================\n     * ========================= Nicknames =========================\n     * =============================================================\n     */\n\n    @Message(\"nickname.set\")\n    void nicknameSet(final Audience audience, final Component nickname);\n\n    @Message(\"nickname.set.others\")\n    void nicknameSetOthers(final Audience audience, final String target, final Component nickname);\n\n    @Message(\"nickname.error.character_limit\")\n    void nicknameErrorCharacterLimit(\n        final Audience audience,\n        final Component nickname,\n        final int minLength,\n        final int maxLength\n    );\n\n    @Message(\"nickname.error.blacklist\")\n    void nicknameErrorBlackList(final Audience audience, final Component nickname);\n\n    @Message(\"nickname.error.filter\")\n    void nicknameErrorFilter(final Audience audience, final Component nickname);\n\n    @Message(\"nickname.show.others\")\n    void nicknameShowOthers(final Audience audience, final String target, final Component nickname);\n\n    @Message(\"nickname.show.others.unset\")\n    void nicknameShowOthersUnset(final Audience audience, final String target);\n\n    @Message(\"nickname.show\")\n    void nicknameShow(final Audience audience, final String target, final Component nickname);\n\n    @Message(\"nickname.show.unset\")\n    void nicknameShowUnset(final Audience audience, final String target);\n\n    @Message(\"nickname.reset\")\n    void nicknameReset(final Audience audience);\n\n    @Message(\"nickname.reset.others\")\n    void nicknameResetOthers(final Audience audience, final String target);\n\n    @Message(\"nickname.realname\")\n    void realName(final Audience audience, final Component target, final String username);\n\n    @Message(\"nickname.realname.target_invalid\")\n    void realNameTargetInvalid(final Audience audience, final String input);\n\n    /*\n     * =============================================================\n     * ========================== Ignore ===========================\n     * =============================================================\n     */\n\n    @Message(\"ignore.already_ignored\")\n    void alreadyIgnored(final Audience audience, final Component target);\n\n    @Message(\"ignore.not_ignored\")\n    void notIgnored(Audience audience, Component target);\n\n    @Message(\"ignore.exempt\")\n    void ignoreExempt(final Audience audience, final Component target);\n\n    @Message(\"ignore.now_ignoring\")\n    void nowIgnoring(final Audience audience, final Component target);\n\n    @Message(\"ignore.no_longer_ignoring\")\n    void noLongerIgnoring(final Audience audience, final Component target);\n\n    @Message(\"ignore.invalid_target\")\n    void ignoreTargetInvalid(final Audience audience);\n\n    /*\n     * =============================================================\n     * ========================== Reload ===========================\n     * =============================================================\n     */\n\n    @Message(\"config.reload.success\")\n    void configReloaded(final Audience audience);\n\n    @Message(\"config.reload.failed\")\n    void configReloadFailed(final Audience audience);\n\n    /*\n     * =============================================================\n     * ========================== Spying ===========================\n     * =============================================================\n     */\n\n    @Message(\"command.spy.enabled\")\n    void commandSpyEnabled(final Audience audience);\n\n    @Message(\"command.spy.disabled\")\n    void commandSpyDisabled(final Audience audience);\n\n    @Message(\"command.spy.description\")\n    Component commandSpyDescription();\n\n    /*\n     * =============================================================\n     * ========================== Filters ==========================\n     * =============================================================\n     */\n\n    @Message(\"command.filter.optional.enabled\")\n    void commandOptionalFilterEnabled(final Audience audience);\n\n    @Message(\"command.filter.optional.disabled\")\n    void commandOptionalFilterDisabled(final Audience audience);\n\n    @Message(\"command.filter.optional.description\")\n    Component commandOptionalFilterDescription();\n\n    /*\n     * =============================================================\n     * ====================== Cloud Messages =======================\n     * =============================================================\n     */\n\n    @Message(\"error.command.no_permission\")\n    void errorCommandNoPermission(final Audience audience);\n\n    @Message(\"error.command.command_execution\")\n    void errorCommandCommandExecution(\n        final Audience audience,\n        @Placeholder(\"throwable_message\") final Component throwableMessage,\n        final String stacktrace\n    );\n\n    @Message(\"error.command.argument_parsing\")\n    void errorCommandArgumentParsing(final Audience audience, @Placeholder(\"throwable_message\") final Component throwableMessage);\n\n    @Message(\"error.command.invalid_player\")\n    Component errorCommandInvalidPlayer(String input);\n\n    @Message(\"error.command.invalid_sender\")\n    void errorCommandInvalidSender(final Audience audience, final String sender_type);\n\n    @Message(\"error.command.invalid_syntax\")\n    void errorCommandInvalidSyntax(final Audience audience, final Component syntax);\n\n    @Message(\"error.command.command_needs_player\")\n    Component commandNeedsPlayer();\n\n    /*\n     * =============================================================\n     * =================== Command Documentation ===================\n     * =============================================================\n     */\n\n    @Message(\"command.clearchat.description\")\n    Component commandClearChatDescription();\n\n    @Message(\"command.continue.argument.message\")\n    Component commandContinueArgumentMessage();\n\n    @Message(\"command.continue.description\")\n    Component commandContinueDescription();\n\n    @Message(\"command.debug.argument.player\")\n    Component commandDebugArgumentPlayer();\n\n    @Message(\"command.debug.description\")\n    Component commandDebugDescription();\n\n    @Message(\"command.help.argument.query\")\n    Component commandHelpArgumentQuery();\n\n    @Message(\"command.help.description\")\n    Component commandHelpDescription();\n\n    @Message(\"command.ignore.argument.player\")\n    Component commandIgnoreArgumentPlayer();\n\n    @Message(\"command.ignore.argument.uuid\")\n    Component commandIgnoreArgumentUUID();\n\n    @Message(\"command.ignore.description\")\n    Component commandIgnoreDescription();\n\n    @Message(\"command.ignorelist.description\")\n    Component commandIgnoreListDescription();\n\n    @Message(\"command.ignorelist.none_ignored\")\n    void commandIgnoreListNoneIgnored(Audience audience);\n\n    @Message(\"command.ignorelist.pagination_header\")\n    Component commandIgnoreListPaginationHeader(int page, int pages);\n\n    @Message(\"command.ignorelist.pagination_element\")\n    Component commandIgnoreListPaginationElement(Component displayName, String username);\n\n    @Message(\"command.join.description\")\n    Component commandJoinDescription();\n\n    @Message(\"command.leave.description\")\n    Component commandLeaveDescription();\n\n    @Message(\"command.mute.argument.player\")\n    Component commandMuteArgumentPlayer();\n\n    @Message(\"command.mute.argument.duration\")\n    Component commandMuteArgumentDuration();\n\n    @Message(\"command.mute.argument.uuid\")\n    Component commandMuteArgumentUUID();\n\n    @Message(\"command.mute.description\")\n    Component commandMuteDescription();\n\n    @Message(\"command.muteinfo.argument.player\")\n    Component commandMuteInfoArgumentPlayer();\n\n    @Message(\"command.muteinfo.argument.uuid\")\n    Component commandMuteInfoArgumentUUID();\n\n    @Message(\"command.muteinfo.description\")\n    Component commandMuteInfoDescription();\n\n    @Message(\"command.nickname.argument.player\")\n    Component commandNicknameArgumentPlayer();\n\n    @Message(\"command.nickname.argument.nickname\")\n    Component commandNicknameArgumentNickname();\n\n    @Message(\"command.nickname.reset.description\")\n    Component commandNicknameResetDescription();\n\n    @Message(\"command.nickname.set.description\")\n    Component commandNicknameSetDescription();\n\n    @Message(\"command.nickname.description\")\n    Component commandNicknameDescription();\n\n    @Message(\"command.nickname.others.reset.description\")\n    Component commandNicknameOthersResetDescription();\n\n    @Message(\"command.nickname.others.set.description\")\n    Component commandNicknameOthersSetDescription();\n\n    @Message(\"command.nickname.others.description\")\n    Component commandNicknameOthersDescription();\n\n    @Message(\"command.reload.description\")\n    Component commandReloadDescription();\n\n    @Message(\"command.reply.argument.message\")\n    Component commandReplyArgumentMessage();\n\n    @Message(\"command.reply.description\")\n    Component commandReplyDescription();\n\n    @Message(\"command.togglemsg.description\")\n    Component commandToggleMsgDescription();\n\n    @Message(\"command.unignore.argument.player\")\n    Component commandUnignoreArgumentPlayer();\n\n    @Message(\"command.unignore.argument.uuid\")\n    Component commandUnignoreArgumentUUID();\n\n    @Message(\"command.unignore.description\")\n    Component commandUnignoreDescription();\n\n    @Message(\"command.unmute.argument.player\")\n    Component commandUnmuteArgumentPlayer();\n\n    @Message(\"command.unmute.argument.uuid\")\n    Component commandUnmuteArgumentUUID();\n\n    @Message(\"command.unmute.description\")\n    Component commandUnmuteDescription();\n\n    @Message(\"command.whisper.argument.player\")\n    Component commandWhisperArgumentPlayer();\n\n    @Message(\"command.whisper.argument.message\")\n    Component commandWhisperArgumentMessage();\n\n    @Message(\"command.whisper.description\")\n    Component commandWhisperDescription();\n\n    @Message(\"command.realname.description\")\n    Component commandRealNameDescription();\n\n    @Message(\"command.realname.argument.player\")\n    Component commandRealNameArgumentPlayer();\n\n    @Message(\"command.updateusername.description\")\n    Component commandUpdateUsernameDescription();\n\n    @Message(\"command.updateusername.argument.player\")\n    Component commandUpdateUsernameArgumentPlayer();\n\n    @Message(\"command.updateusername.argument.uuid\")\n    Component commandUpdateUsernameArgumentUUID();\n\n    @Message(\"command.updateusername.notupdated\")\n    void usernameNotUpdated(final Audience recipient);\n\n    @Message(\"command.updateusername.fetching\")\n    void usernameFetching(final Audience audience);\n\n    @Message(\"command.updateusername.updated\")\n    void usernameUpdated(final Audience audience, @Placeholder(\"newname\") final String newName);\n\n    @Message(\"command.party.pagination_header\")\n    Component commandPartyPaginationHeader(Component partyName);\n\n    @Message(\"command.party.pagination_element\")\n    Component commandPartyPaginationElement(Component displayName, String username, Option online);\n\n    @Message(\"command.party.created\")\n    void partyCreated(Audience audience, Component partyName);\n\n    @Message(\"command.party.not_in_party\")\n    void notInParty(Audience audience);\n\n    @Message(\"command.party.current_party\")\n    void currentParty(Audience audience, Component partyName);\n\n    @Message(\"command.party.must_leave_current_first\")\n    void mustLeavePartyFirst(Audience audience);\n\n    @Message(\"command.party.name_too_long\")\n    void partyNameTooLong(Audience audience);\n\n    @Message(\"command.party.received_invite\")\n    void receivedPartyInvite(Audience audience, Component senderDisplayName, String senderUsername, Component partyName);\n\n    @Message(\"command.party.sent_invite\")\n    void sentPartyInvite(Audience audience, Component recipientDisplayName, Component partyName);\n\n    @Message(\"command.party.must_specify_invite\")\n    void mustSpecifyPartyInvite(Audience audience);\n\n    @Message(\"command.party.no_pending_invites\")\n    void noPendingPartyInvites(Audience audience);\n\n    @Message(\"command.party.no_invite_from\")\n    void noPartyInviteFrom(Audience audience, Component senderDisplayName);\n\n    @Message(\"command.party.joined_party\")\n    void joinedParty(Audience audience, Component partyName);\n\n    @Message(\"command.party.left_party\")\n    void leftParty(Audience audience, Component partyName);\n\n    @Message(\"command.party.disbanded\")\n    void disbandedParty(Audience audience, Component partyName);\n\n    @Message(\"command.party.cannot_disband_multiple_members\")\n    void cannotDisbandParty(Audience audience, Component partyName);\n\n    @Message(\"command.party.must_be_in_party\")\n    void mustBeInParty(Audience audience);\n\n    @Message(\"command.party.cannot_invite_self\")\n    void cannotInviteSelf(Audience audience);\n\n    @Message(\"command.party.already_in_party\")\n    void alreadyInParty(Audience audience, Component displayName);\n\n    @Message(\"command.party.description\")\n    Component partyDesc();\n\n    @Message(\"command.party.create.description\")\n    Component partyCreateDesc();\n\n    @Message(\"command.party.invite.description\")\n    Component partyInviteDesc();\n\n    @Message(\"command.party.accept.description\")\n    Component partyAcceptDesc();\n\n    @Message(\"command.party.leave.description\")\n    Component partyLeaveDesc();\n\n    @Message(\"command.party.disband.description\")\n    Component partyDisbandDesc();\n\n    @Message(\"party.player_joined\")\n    void playerJoinedParty(Audience audience, Component partyName, Component displayName);\n\n    @Message(\"party.player_left\")\n    void playerLeftParty(Audience audience, Component partyName, Component displayName);\n\n    @Message(\"party.cannot_use_channel\")\n    Component cannotUsePartyChannel(Audience audience);\n\n    @Message(\"party.spy\")\n    void partySpy(\n        Audience audience,\n        @Placeholder UUID uuid,\n        @Placeholder(\"display_name\") Component displayName,\n        @Placeholder String username,\n        @Placeholder Component message,\n        @Placeholder(\"party_name\") Component partyName\n    );\n\n    @Message(\"deletemessage.prefix\")\n    Component deleteMessagePrefix();\n\n    @Message(\"pagination.page_out_of_range\")\n    Component paginationOutOfRange(int page, int pages);\n\n    @Message(\"pagination.click_for_next_page\")\n    Component paginationClickForNextPage();\n\n    @Message(\"pagination.click_for_previous_page\")\n    Component paginationClickForPreviousPage();\n\n    @Message(\"pagination.footer\")\n    Component paginationFooter(int page, int pages, Component buttons);\n\n    /*\n     * =============================================================\n     * ======================= Integrations ========================\n     * =============================================================\n     */\n\n    @Message(\"integrations.towny.cannot_use_alliance_channel\")\n    Component cannotUseAllianceChannel(Audience audience);\n\n    @Message(\"integrations.towny.cannot_use_nation_channel\")\n    Component cannotUseNationChannel(Audience audience);\n\n    @Message(\"integrations.towny.cannot_use_town_channel\")\n    Component cannotUseTownChannel(Audience audience);\n\n    @Message(\"integrations.mcmmo.cannot_use_party_channel\")\n    Component cannotUseMcmmoPartyChannel(Audience audience);\n\n    @Message(\"integrations.adp_parties.cannot_use_party_channel\")\n    Component cannotUseADPPartiesPartyChannel(Audience audience);\n\n    @Message(\"integrations.fuuid.cannot_use_faction_channel\")\n    Component cannotUseFactionChannel(Audience audience);\n\n    @Message(\"integrations.fuuid.cannot_use_alliance_channel\")\n    Component cannotUseFactionAllianceChannel(Audience audience);\n\n    @Message(\"integrations.fuuid.cannot_use_truce_channel\")\n    Component cannotUseTruceChannel(Audience audience);\n\n    @Message(\"integrations.fuuid.cannot_use_mod_channel\")\n    Component cannotUseFactionModChannel(Audience audience);\n\n    @Message(\"integrations.plotsquared.cannot_use_plot_channel\")\n    Component cannotUsePlotChannel(Audience audience);\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/NotPlaceholder.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport java.lang.annotation.Documented;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\n\n@Documented\n@Target({ElementType.PARAMETER, ElementType.TYPE_USE})\n@Retention(RetentionPolicy.RUNTIME)\npublic @interface NotPlaceholder {\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/Option.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\npublic record Option(boolean value) {\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/OptionTagResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport net.kyori.adventure.text.minimessage.Context;\nimport net.kyori.adventure.text.minimessage.ParsingException;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.Nullable;\n\n@DefaultQualifier(NonNull.class)\npublic final class OptionTagResolver implements TagResolver {\n\n    private final String name;\n    private final boolean state;\n\n    public OptionTagResolver(final String name, final boolean state) {\n        this.name = name;\n        this.state = state;\n    }\n\n    @Override\n    public @Nullable Tag resolve(final String name, final ArgumentQueue arguments, final Context ctx) throws ParsingException {\n        if (!this.has(name)) {\n            return null;\n        }\n        final Tag.Argument t = arguments.popOr(\"Missing option 1\");\n        String f = \"\";\n        if (arguments.peek() != null) {\n            f = arguments.pop().value();\n        }\n        return Tag.preProcessParsed(this.state ? t.value() : f);\n    }\n\n    @Override\n    public boolean has(final String name) {\n        return name.equals(this.name);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/PrefixedDelegateIterator.java",
    "content": "/*\n * moonshine - A localisation library for Java.\n * Copyright (C) Mariell Hoversholm\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Lesser General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport java.util.Iterator;\n\npublic final class PrefixedDelegateIterator<T> implements Iterator<T> {\n\n    private final T prefix;\n    private final Iterator<T> delegate;\n    private boolean seenPrefix = false;\n\n    public PrefixedDelegateIterator(final T prefix, final Iterator<T> delegate) {\n        this.prefix = prefix;\n        this.delegate = delegate;\n    }\n\n    @Override\n    public boolean hasNext() {\n        return !this.seenPrefix || this.delegate.hasNext();\n    }\n\n    @Override\n    public T next() {\n        if (!this.seenPrefix) {\n            this.seenPrefix = true;\n            return this.prefix;\n        }\n\n        return this.delegate.next();\n    }\n\n    @Override\n    public void remove() {\n        if (!this.seenPrefix) {\n            throw new IllegalStateException(\"must see prefix before removing from iterator\");\n        }\n\n        this.delegate.remove();\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/RenderForTagResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport com.google.inject.Provider;\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.CancellationException;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport net.kyori.adventure.text.minimessage.Context;\nimport net.kyori.adventure.text.minimessage.ParsingException;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class RenderForTagResolver implements TagResolver {\n\n    private static final String TAG_NAME = \"render_for\";\n    private final UserManagerInternal<?> users;\n    private final ProfileResolver profileResolver;\n    private final Provider<CarbonMessageRenderer> messageRenderer;\n    private final Map<String, ?> resolvedPlaceholders;\n\n    public interface Factory {\n\n        RenderForTagResolver create(Map<String, ?> resolvedPlaceholders);\n    }\n\n    @AssistedInject\n    private RenderForTagResolver(\n        final UserManagerInternal<?> users,\n        final ProfileResolver profileResolver,\n        final Provider<CarbonMessageRenderer> messageRenderer,\n        final @Assisted Map<String, ?> resolvedPlaceholders\n    ) {\n        this.users = users;\n        this.profileResolver = profileResolver;\n        this.messageRenderer = messageRenderer;\n        this.resolvedPlaceholders = resolvedPlaceholders;\n    }\n\n    @SuppressWarnings(\"DataFlowIssue\")\n    @Override\n    public @Nullable Tag resolve(final String name, final ArgumentQueue arguments, final Context ctx) throws ParsingException {\n        if (!this.has(name)) {\n            return null;\n        }\n\n        final String renderFor = arguments.popOr(\"Missing username or UUID to render for\").value();\n        CompletableFuture<@Nullable ? extends CarbonPlayer> playerFuture;\n        try {\n            final UUID uuid = UUID.fromString(renderFor);\n            playerFuture = this.users.user(uuid);\n        } catch (final IllegalArgumentException ignore) {\n            playerFuture = this.profileResolver.resolveUUID(renderFor).thenCompose(uuid -> {\n                if (uuid != null) {\n                    return this.users.user(uuid);\n                }\n                return CompletableFuture.completedFuture(null);\n            });\n        }\n\n        final @Nullable CarbonPlayer player;\n        try {\n            player = playerFuture.join();\n            if (player == null) {\n                return null;\n            }\n        } catch (final CompletionException | CancellationException ignore) {\n            return null;\n        }\n\n        final String value = arguments.popOr(\"Missing message value\").value();\n        if (value.equalsIgnoreCase(\"inserting\")) {\n            return Tag.inserting(\n                this.messageRenderer.get().render(SourcedAudience.of(player, player), arguments.popOr(\"Missing message value\").value(), this.resolvedPlaceholders, null, null)\n            );\n        } else if (value.equalsIgnoreCase(\"self_closing_inserting\")) {\n            return Tag.selfClosingInserting(\n                this.messageRenderer.get().render(SourcedAudience.of(player, player), arguments.popOr(\"Missing message value\").value(), this.resolvedPlaceholders, null, null)\n            );\n        } else {\n            return Tag.selfClosingInserting(this.messageRenderer.get().render(SourcedAudience.of(player, player), value, this.resolvedPlaceholders, null, null));\n        }\n    }\n\n    @Override\n    public boolean has(final String name) {\n        return name.equalsIgnoreCase(TAG_NAME);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/SourcedAudience.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * An audience, where messages are sent from another Audience.\n */\n@DefaultQualifier(NonNull.class)\npublic interface SourcedAudience extends ForwardingAudience.Single {\n\n    /**\n     * The source audience.\n     *\n     * @return source\n     */\n    Audience sender();\n\n    /**\n     * The recipient audience. The audience that this sourced audience forwards to.\n     *\n     * @return recipient\n     */\n    Audience recipient();\n\n    @Override\n    default Audience audience() {\n        return this.recipient();\n    }\n\n    /**\n     * Create a new {@link SourcedAudience} instance.\n     *\n     * @param sender sender\n     * @param recipient recipient\n     * @return sourced audience\n     */\n    static SourcedAudience of(final Audience sender, final Audience recipient) {\n        return new SourcedAudienceImpl(sender, recipient);\n    }\n\n    /**\n     * The empty {@link SourcedAudience}, with an empty sender and recipient.\n     *\n     * @return the empty sourced audience\n     */\n    static SourcedAudience empty() {\n        return SourcedAudienceImpl.EMPTY;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/SourcedAudienceImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport net.kyori.adventure.audience.Audience;\n\nrecord SourcedAudienceImpl(Audience sender, Audience recipient) implements SourcedAudience {\n\n    static final SourcedAudience EMPTY = new SourcedAudienceImpl(Audience.empty(), Audience.empty());\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/SourcedMessageSender.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport net.kyori.adventure.text.Component;\nimport net.kyori.moonshine.message.IMessageSender;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class SourcedMessageSender implements IMessageSender<SourcedAudience, Component> {\n\n    @Override\n    public void send(final SourcedAudience receiver, final Component renderedMessage) {\n        receiver.recipient().sendMessage(renderedMessage);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/SourcedReceiverResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport com.google.inject.Singleton;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.moonshine.receiver.IReceiverLocator;\nimport net.kyori.moonshine.receiver.IReceiverLocatorResolver;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic final class SourcedReceiverResolver implements IReceiverLocatorResolver<SourcedAudience> {\n\n    @Override\n    public IReceiverLocator<SourcedAudience> resolve(final Method method, final Type proxy) {\n        return new Resolver();\n    }\n\n    private static final class Resolver implements IReceiverLocator<SourcedAudience> {\n        @Override\n        public SourcedAudience locate(final Method method, final Object proxy, final @Nullable Object[] parameters) {\n            if (parameters.length == 0) {\n                return SourcedAudience.empty();\n            }\n\n            final @Nullable Object parameter = parameters[0];\n\n            if (parameter instanceof SourcedAudience sourcedAudience) {\n                return sourcedAudience;\n            } else if (parameter instanceof Audience audience) {\n                return SourcedAudience.of(audience, audience);\n            }\n\n            return SourcedAudience.empty();\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/StandardPlaceholderResolverStrategyButDifferent.java",
    "content": "/*\n * moonshine - A localisation library for Java.\n * Copyright (C) Mariell Hoversholm\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU Lesser General Public License as published\n * by the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU Lesser General Public License for more details.\n *\n * You should have received a copy of the GNU Lesser General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport io.leangen.geantyref.GenericTypeReflector;\nimport java.lang.reflect.Parameter;\nimport java.lang.reflect.Type;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.moonshine.Moonshine;\nimport net.kyori.moonshine.annotation.Placeholder;\nimport net.kyori.moonshine.annotation.meta.ThreadSafe;\nimport net.kyori.moonshine.exception.PlaceholderResolvingException;\nimport net.kyori.moonshine.exception.UnfinishedPlaceholderException;\nimport net.kyori.moonshine.model.MoonshineMethod;\nimport net.kyori.moonshine.placeholder.ContinuanceValue;\nimport net.kyori.moonshine.placeholder.IPlaceholderResolver;\nimport net.kyori.moonshine.strategy.IPlaceholderResolverStrategy;\nimport net.kyori.moonshine.strategy.supertype.ISupertypeStrategy;\nimport net.kyori.moonshine.strategy.supertype.StandardSupertypeThenInterfaceSupertypeStrategy;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.spongepowered.configurate.util.NamingScheme;\n\nimport static java.util.Collections.emptyNavigableSet;\n\n@ThreadSafe\npublic final class StandardPlaceholderResolverStrategyButDifferent<R, I, F> implements IPlaceholderResolverStrategy<R, I, F> {\n\n    private final ISupertypeStrategy supertypeStrategy = new StandardSupertypeThenInterfaceSupertypeStrategy(false);\n    private final NamingScheme namingScheme;\n\n    /**\n     * Create a new {@link StandardPlaceholderResolverStrategyButDifferent}.\n     *\n     * @param placeholderParamaterDefaultNamingScheme the default naming scheme for placeholders that do not specify a name explicitly, applied to parameter names\n     */\n    public StandardPlaceholderResolverStrategyButDifferent(final NamingScheme placeholderParamaterDefaultNamingScheme) {\n        this.namingScheme = placeholderParamaterDefaultNamingScheme;\n    }\n\n    @Override\n    public Map<String, ? extends F> resolvePlaceholders(\n        final Moonshine<R, I, ?, F> moonshine,\n        final R receiver, final I intermediateText,\n        final MoonshineMethod<? extends R> moonshineMethod,\n        final @Nullable Object[] parameters\n    )\n        throws PlaceholderResolvingException {\n        if (parameters.length == 0) {\n            return Collections.emptyMap();\n        }\n\n        final Map<String, F> finalisedPlaceholders = new LinkedHashMap<>(parameters.length);\n        final Map<String, ContinuanceValue<?>> resolvingPlaceholders = new LinkedHashMap<>(16);\n        final Parameter[] methodParameters = moonshineMethod.reflectMethod().getParameters();\n        final Type[] exactParameterTypes = GenericTypeReflector.getParameterTypes(\n            moonshineMethod.reflectMethod(), moonshine.proxiedType());\n\n        // Don't resolve recipients\n        final int start = moonshineMethod.reflectMethod().getReturnType() != Void.TYPE ? 0 : 1;\n        for (int idx = start; idx < parameters.length; ++idx) {\n\n            final Parameter parameter = methodParameters[idx];\n            final @Nullable Object value = parameters[idx];\n            //  Don't resolve Audiences for now\n            if (value == null || parameter.getType() == Audience.class || parameter.getAnnotation(NotPlaceholder.class) != null) {\n                // Nothing to resolve with.\n                continue;\n            }\n\n            final Type parameterType = GenericTypeReflector.getExactSubType(\n                exactParameterTypes[idx], value.getClass());\n\n            final @Nullable Placeholder placeholder = parameter.getAnnotation(Placeholder.class);\n\n            final String placeholderName = (placeholder != null && !placeholder.value().isEmpty())\n                ? placeholder.value()\n                : this.namingScheme.coerce(parameter.getName());\n\n            resolvingPlaceholders\n                .put(placeholderName, ContinuanceValue.continuanceValue(value, parameterType));\n\n        }\n\n        this.resolvePlaceholder(moonshine, receiver, finalisedPlaceholders,\n            resolvingPlaceholders, moonshineMethod, parameters);\n\n        return finalisedPlaceholders;\n    }\n\n    /**\n     * Resolve a single placeholder.\n     *\n     * @param moonshine             the moonshine instance\n     * @param finalisedPlaceholders the finalised placeholders\n     * @param resolvingPlaceholders the placeholders to resolve\n     * @param moonshineMethod       the method we are resolving a placeholder for\n     */\n    private void resolvePlaceholder(\n        final Moonshine<R, I, ?, F> moonshine,\n        final R receiver,\n        final Map<String, F> finalisedPlaceholders,\n        final Map<String, ContinuanceValue<?>> resolvingPlaceholders,\n        final MoonshineMethod<? extends R> moonshineMethod,\n        final @Nullable Object[] parameters\n    )\n        throws UnfinishedPlaceholderException {\n        final var weightedPlaceholderResolvers = moonshine.weightedPlaceholderResolvers();\n\n        // Shamelessly stealing kashike's joke\n        dancing:\n        while (!resolvingPlaceholders.isEmpty()) {\n            final var resolvingPlaceholderIterator = resolvingPlaceholders.entrySet().iterator();\n            while (resolvingPlaceholderIterator.hasNext()) {\n                final var continuanceEntry = resolvingPlaceholderIterator.next();\n                final String continuancePlaceholderName = continuanceEntry.getKey();\n                final Type type = continuanceEntry.getValue().type();\n                final Object value = continuanceEntry.getValue().value();\n\n                final Iterator<Type> hierarchyIterator =\n                    new PrefixedDelegateIterator<>(type, this.supertypeStrategy.hierarchyIterator(type));\n                while (hierarchyIterator.hasNext()) {\n                    final Type supertype = hierarchyIterator.next();\n\n                    for (final var weighted : weightedPlaceholderResolvers.getOrDefault(supertype, emptyNavigableSet())) {\n                        @SuppressWarnings(\"unchecked\") // This should be equivalent.\n                        final var placeholderResolver =\n                            (IPlaceholderResolver<R, Object, ? extends F>) weighted.value();\n\n                        final var resolverResult =\n                            placeholderResolver.resolve(continuancePlaceholderName, value, receiver,\n                                moonshineMethod.owner().getType(),\n                                moonshineMethod.reflectMethod(), parameters);\n                        if (resolverResult == null) {\n                            // The resolver did not want to resolve this; pass it on.\n                            continue;\n                        }\n\n                        resolvingPlaceholderIterator.remove();\n\n                        resolverResult.forEach((resolvedName, resolvedValue) ->\n                            resolvedValue.map(conclusionValue -> finalisedPlaceholders\n                                    .put(resolvedName, conclusionValue.value()),\n                                continuanceValue -> resolvingPlaceholders.put(resolvedName, continuanceValue)));\n\n                        continue dancing;\n                    }\n                }\n\n                throw new UnfinishedPlaceholderException(moonshineMethod, continuancePlaceholderName, value);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/TagPermissions.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages;\n\nimport java.util.Map;\nimport java.util.function.Predicate;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.format.TextDecoration;\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport net.kyori.adventure.text.minimessage.tag.standard.StandardTags;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class TagPermissions {\n\n    public static final String NICKNAME = \"carbon.nickname.tags\";\n    public static final String MESSAGE = \"carbon.messagetags\";\n    public static final String PARTY_NAME = \"carbon.parties.name.tags\";\n    private static final Map<String, TagResolver> DEFAULT_TAGS = Map.ofEntries(\n        Map.entry(\"hover\", StandardTags.hoverEvent()),\n        Map.entry(\"click\", StandardTags.clickEvent()),\n        Map.entry(\"color\", StandardTags.color()),\n        Map.entry(\"keybind\", StandardTags.keybind()),\n        Map.entry(\"translatable\", StandardTags.translatable()),\n        Map.entry(\"insertion\", StandardTags.insertion()),\n        Map.entry(\"font\", StandardTags.font()),\n        Map.entry(\"decorations\", StandardTags.decorations()),\n        Map.entry(\"gradient\", StandardTags.gradient()),\n        Map.entry(\"rainbow\", StandardTags.rainbow()),\n        Map.entry(\"reset\", StandardTags.reset()),\n        Map.entry(\"newline\", StandardTags.newline()),\n        Map.entry(\"pride\", StandardTags.pride()),\n        Map.entry(\"shadow_color\", StandardTags.shadowColor()),\n        Map.entry(\"transition\", StandardTags.transition())\n    );\n\n    private TagPermissions() {\n    }\n\n    public static Component parseTags(\n        final @Nullable Audience audience,\n        final String basePermission,\n        final String message,\n        final Predicate<String> permission,\n        final TagResolver.Builder resolver\n    ) {\n        boolean hasAllDecorations = false;\n        for (final Map.Entry<String, TagResolver> entry : DEFAULT_TAGS.entrySet()) {\n            if (permission.test(basePermission + '.' + entry.getKey())) {\n                resolver.resolver(entry.getValue());\n                if (entry.getKey().equals(\"decorations\")) {\n                    hasAllDecorations = true;\n                }\n            }\n        }\n\n        if (!hasAllDecorations) {\n            for (final TextDecoration decoration : TextDecoration.values()) {\n                if (!permission.test(basePermission + '.' + decoration.name())) {\n                    continue;\n                }\n\n                resolver.resolver(StandardTags.decorations(decoration));\n            }\n        }\n\n        final MiniMessage miniMessage = MiniMessage.builder().tags(resolver.build()).build();\n\n        if (audience != null) {\n            return miniMessage.deserialize(message, audience);\n        }\n        return miniMessage.deserialize(message);\n    }\n\n    public static Component parseTags(\n        final @Nullable Audience audience,\n        final String basePermission,\n        final String message,\n        final Predicate<String> permission\n    ) {\n        return parseTags(audience, basePermission, message, permission, TagResolver.builder());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/placeholders/BooleanPlaceholderResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages.placeholders;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.moonshine.placeholder.ConclusionValue;\nimport net.kyori.moonshine.placeholder.ContinuanceValue;\nimport net.kyori.moonshine.placeholder.IPlaceholderResolver;\nimport net.kyori.moonshine.util.Either;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\npublic class BooleanPlaceholderResolver<R> implements IPlaceholderResolver<R, Boolean, Tag> {\n\n    @Override\n    public @Nullable Map<String, Either<ConclusionValue<? extends Tag>, ContinuanceValue<?>>> resolve(\n        final String placeholderName,\n        final Boolean value,\n        final R receiver,\n        final Type owner,\n        final Method method,\n        final @Nullable Object[] parameters\n    ) {\n        if (value == null) {\n            return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(\"false\"))));\n        }\n\n        return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(value.toString()))));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/placeholders/ComponentPlaceholderResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages.placeholders;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.moonshine.placeholder.ConclusionValue;\nimport net.kyori.moonshine.placeholder.ContinuanceValue;\nimport net.kyori.moonshine.placeholder.IPlaceholderResolver;\nimport net.kyori.moonshine.util.Either;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class ComponentPlaceholderResolver<R> implements IPlaceholderResolver<R, Component, Tag> {\n\n    @Override\n    public @Nullable Map<String, Either<ConclusionValue<? extends Tag>, ContinuanceValue<?>>> resolve(\n        final String placeholderName,\n        final Component value,\n        final R receiver,\n        final Type owner,\n        final Method method,\n        final @Nullable Object[] parameters\n    ) {\n        return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.selfClosingInserting(value))));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/placeholders/IntPlaceholderResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages.placeholders;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.moonshine.placeholder.ConclusionValue;\nimport net.kyori.moonshine.placeholder.ContinuanceValue;\nimport net.kyori.moonshine.placeholder.IPlaceholderResolver;\nimport net.kyori.moonshine.util.Either;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\npublic class IntPlaceholderResolver<R> implements IPlaceholderResolver<R, Integer, Tag> {\n\n    @Override\n    public @Nullable Map<String, Either<ConclusionValue<? extends Tag>, ContinuanceValue<?>>> resolve(\n        final String placeholderName,\n        final Integer value,\n        final R receiver,\n        final Type owner,\n        final Method method,\n        final @Nullable Object[] parameters\n    ) {\n        return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(String.valueOf(value)))));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/placeholders/KeyPlaceholderResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages.placeholders;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.moonshine.placeholder.ConclusionValue;\nimport net.kyori.moonshine.placeholder.ContinuanceValue;\nimport net.kyori.moonshine.placeholder.IPlaceholderResolver;\nimport net.kyori.moonshine.util.Either;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\npublic class KeyPlaceholderResolver<R> implements IPlaceholderResolver<R, Key, Tag> {\n\n    @Override\n    public @Nullable Map<String, Either<ConclusionValue<? extends Tag>, ContinuanceValue<?>>> resolve(\n        final String placeholderName,\n        final Key value,\n        final R receiver,\n        final Type owner,\n        final Method method,\n        final @Nullable Object[] parameters\n    ) {\n        return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(value.toString()))));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/placeholders/LongPlaceholderResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages.placeholders;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.moonshine.placeholder.ConclusionValue;\nimport net.kyori.moonshine.placeholder.ContinuanceValue;\nimport net.kyori.moonshine.placeholder.IPlaceholderResolver;\nimport net.kyori.moonshine.util.Either;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\npublic class LongPlaceholderResolver<R> implements IPlaceholderResolver<R, Long, Tag> {\n\n    @Override\n    public @Nullable Map<String, Either<ConclusionValue<? extends Tag>, ContinuanceValue<?>>> resolve(\n        final String placeholderName,\n        final Long value,\n        final R receiver,\n        final Type owner,\n        final Method method,\n        final @Nullable Object[] parameters\n    ) {\n        return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(String.valueOf(value)))));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/placeholders/OptionPlaceholderResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages.placeholders;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport net.draycia.carbon.common.messages.Option;\nimport net.draycia.carbon.common.messages.OptionTagResolver;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport net.kyori.moonshine.placeholder.ConclusionValue;\nimport net.kyori.moonshine.placeholder.ContinuanceValue;\nimport net.kyori.moonshine.placeholder.IPlaceholderResolver;\nimport net.kyori.moonshine.util.Either;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\npublic final class OptionPlaceholderResolver<R> implements IPlaceholderResolver<R, Option, TagResolver> {\n\n    @Override\n    public Map<String, Either<ConclusionValue<? extends TagResolver>, ContinuanceValue<?>>> resolve(\n        final String placeholderName,\n        final Option value,\n        final R receiver,\n        final Type owner,\n        final Method method,\n        final @Nullable Object[] parameters\n    ) {\n        if (value == null) {\n            throw new IllegalArgumentException();\n        }\n\n        return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(new OptionTagResolver(placeholderName, value.value()))));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/placeholders/StringPlaceholderResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages.placeholders;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.moonshine.placeholder.ConclusionValue;\nimport net.kyori.moonshine.placeholder.ContinuanceValue;\nimport net.kyori.moonshine.placeholder.IPlaceholderResolver;\nimport net.kyori.moonshine.util.Either;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\npublic class StringPlaceholderResolver<R> implements IPlaceholderResolver<R, String, Tag> {\n\n    @Override\n    public @Nullable Map<String, Either<ConclusionValue<? extends Tag>, ContinuanceValue<?>>> resolve(\n        final String placeholderName,\n        final String value,\n        final R receiver,\n        final Type owner,\n        final Method method,\n        final @Nullable Object[] parameters\n    ) {\n        return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(value))));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messages/placeholders/UUIDPlaceholderResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messages.placeholders;\n\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport java.util.UUID;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.moonshine.placeholder.ConclusionValue;\nimport net.kyori.moonshine.placeholder.ContinuanceValue;\nimport net.kyori.moonshine.placeholder.IPlaceholderResolver;\nimport net.kyori.moonshine.util.Either;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class UUIDPlaceholderResolver<R> implements IPlaceholderResolver<R, UUID, Tag> {\n\n    @Override\n    public @Nullable Map<String, Either<ConclusionValue<? extends Tag>, ContinuanceValue<?>>> resolve(\n        final String placeholderName,\n        final UUID value,\n        final R receiver,\n        final Type owner,\n        final Method method,\n        final @Nullable Object[] parameters\n    ) {\n        return Map.of(placeholderName, Either.left(ConclusionValue.conclusionValue(Tag.preProcessParsed(value.toString()))));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/CarbonChatPacketHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.KeyedRenderer;\nimport net.draycia.carbon.common.command.commands.WhisperCommand;\nimport net.draycia.carbon.common.event.events.CarbonChatEventImpl;\nimport net.draycia.carbon.common.messaging.packets.ChatMessagePacket;\nimport net.draycia.carbon.common.messaging.packets.DisbandPartyPacket;\nimport net.draycia.carbon.common.messaging.packets.InvalidatePartyInvitePacket;\nimport net.draycia.carbon.common.messaging.packets.LocalPlayerChangePacket;\nimport net.draycia.carbon.common.messaging.packets.LocalPlayersPacket;\nimport net.draycia.carbon.common.messaging.packets.PartyChangePacket;\nimport net.draycia.carbon.common.messaging.packets.PartyInvitePacket;\nimport net.draycia.carbon.common.messaging.packets.SaveCompletedPacket;\nimport net.draycia.carbon.common.messaging.packets.WhisperPacket;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.draycia.carbon.common.users.NetworkUsers;\nimport net.draycia.carbon.common.users.PartyInvites;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport ninja.egg82.messenger.handler.AbstractMessagingHandler;\nimport ninja.egg82.messenger.packets.Packet;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonChatPacketHandler extends AbstractMessagingHandler {\n\n    private final CarbonEventHandler events;\n    private final CarbonServer server;\n    private final ChannelRegistry channels;\n    private final UserManagerInternal<?> userManager;\n    private final NetworkUsers networkUsers;\n    private final WhisperCommand.WhisperHandler whisper;\n    private final PartyInvites partyInvites;\n\n    CarbonChatPacketHandler(\n        final CarbonChat carbonChat,\n        final MessagingManager messagingManager,\n        final UserManagerInternal<?> userManager,\n        final NetworkUsers networkUsers,\n        final WhisperCommand.WhisperHandler whisper,\n        final PartyInvites partyInvites\n    ) {\n        super(messagingManager.requirePacketService());\n        this.events = carbonChat.eventHandler();\n        this.server = carbonChat.server();\n        this.channels = carbonChat.channelRegistry();\n        this.userManager = userManager;\n        this.networkUsers = networkUsers;\n        this.whisper = whisper;\n        this.partyInvites = partyInvites;\n    }\n\n    @Override\n    protected boolean handlePacket(final Packet packet) {\n        if (packet instanceof SaveCompletedPacket statePacket) {\n            this.userManager.saveCompleteMessageReceived(statePacket.playerId());\n            return true;\n        } else if (packet instanceof PartyChangePacket pkt) {\n            this.userManager.partyChangeMessageReceived(pkt);\n            return true;\n        } else if (packet instanceof PartyInvitePacket pkt) {\n            this.partyInvites.handle(pkt);\n            return true;\n        } else if (packet instanceof InvalidatePartyInvitePacket pkt) {\n            this.partyInvites.handle(pkt);\n            return true;\n        } else if (packet instanceof DisbandPartyPacket pkt) {\n            this.userManager.disbandPartyMessageReceived(pkt);\n            return true;\n        } else if (packet instanceof ChatMessagePacket messagePacket) {\n            this.handleMessagePacket(messagePacket);\n            return true; // Don't log an error when the channel doesn't exist\n        } else if (packet instanceof LocalPlayersPacket playersPacket) {\n            this.networkUsers.handlePacket(playersPacket);\n            return true;\n        } else if (packet instanceof LocalPlayerChangePacket playerChangePacket) {\n            this.networkUsers.handlePacket(playerChangePacket);\n            return true;\n        } else if (packet instanceof WhisperPacket whisperPacket) {\n            this.whisper.handlePacket(whisperPacket);\n            return true;\n        }\n\n        return false;\n    }\n\n    private boolean handleMessagePacket(final ChatMessagePacket messagePacket) {\n        final CarbonPlayer sender = this.userManager.user(messagePacket.userId()).join();\n\n        final @Nullable ChatChannel channel = this.channels.channel(messagePacket.channelKey());\n\n        if (channel == null) {\n            return false;\n        }\n\n        if (!channel.shouldCrossServer()) {\n            return false;\n        }\n\n        final List<KeyedRenderer> renderers = new ArrayList<>();\n\n        final List<Audience> recipients = channel.recipients(sender);\n        final CarbonChatEventImpl chatEvent = new CarbonChatEventImpl(sender, messagePacket.message(), recipients, renderers, channel, null, false);\n        this.events.emit(chatEvent);\n\n        renderers.add(KeyedRenderer.keyedRenderer(Key.key(\"carbon\", \"console_cs\"), ($, recipient, message, original) -> {\n            if (recipient instanceof ConsoleCarbonPlayer) {\n                return Component.textOfChildren(Component.text(\"[Cross-Server] \"), message);\n            }\n\n            return message;\n        }));\n\n        for (final Audience recipient : recipients) {\n            if (recipient instanceof CarbonPlayer carbonRecipient\n                && !carbonRecipient.hasPermission(\"carbon.crossserver\")) {\n                continue;\n            }\n\n            recipient.sendMessage(chatEvent.renderFor(recipient));\n        }\n\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/MessagingManager.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.function.Consumer;\nimport java.util.function.Supplier;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.CarbonChatInternal;\nimport net.draycia.carbon.common.command.commands.WhisperCommand;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.config.MessagingSettings;\nimport net.draycia.carbon.common.messaging.packets.ChatMessagePacket;\nimport net.draycia.carbon.common.messaging.packets.DisbandPartyPacket;\nimport net.draycia.carbon.common.messaging.packets.InvalidatePartyInvitePacket;\nimport net.draycia.carbon.common.messaging.packets.LocalPlayerChangePacket;\nimport net.draycia.carbon.common.messaging.packets.LocalPlayersPacket;\nimport net.draycia.carbon.common.messaging.packets.PacketFactory;\nimport net.draycia.carbon.common.messaging.packets.PartyChangePacket;\nimport net.draycia.carbon.common.messaging.packets.PartyInvitePacket;\nimport net.draycia.carbon.common.messaging.packets.SaveCompletedPacket;\nimport net.draycia.carbon.common.messaging.packets.WhisperPacket;\nimport net.draycia.carbon.common.users.NetworkUsers;\nimport net.draycia.carbon.common.users.PartyInvites;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport net.draycia.carbon.common.util.ConcurrentUtil;\nimport net.draycia.carbon.common.util.ExceptionLoggingScheduledThreadPoolExecutor;\nimport net.draycia.carbon.common.util.Exceptions;\nimport ninja.egg82.messenger.MessagingService;\nimport ninja.egg82.messenger.NATSMessagingService;\nimport ninja.egg82.messenger.PacketManager;\nimport ninja.egg82.messenger.RabbitMQMessagingService;\nimport ninja.egg82.messenger.RedisMessagingService;\nimport ninja.egg82.messenger.handler.AbstractServerMessagingHandler;\nimport ninja.egg82.messenger.handler.MessagingHandler;\nimport ninja.egg82.messenger.handler.MessagingHandlerImpl;\nimport ninja.egg82.messenger.packets.AbstractPacket;\nimport ninja.egg82.messenger.packets.MultiPacket;\nimport ninja.egg82.messenger.packets.server.InitializationPacket;\nimport ninja.egg82.messenger.packets.server.KeepAlivePacket;\nimport ninja.egg82.messenger.packets.server.PacketVersionPacket;\nimport ninja.egg82.messenger.packets.server.PacketVersionRequestPacket;\nimport ninja.egg82.messenger.packets.server.ShutdownPacket;\nimport ninja.egg82.messenger.services.PacketService;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic class MessagingManager {\n\n    private static final byte protocolVersion = 1;\n\n    private final Logger logger;\n    private final UUID serverId;\n    private final @MonotonicNonNull ScheduledExecutorService scheduledExecutor;\n    private final @MonotonicNonNull MessagingService messagingService;\n    private volatile @MonotonicNonNull PacketService packetService;\n\n    @Inject\n    public MessagingManager(\n        final ConfigManager configManager,\n        final CarbonChat carbonChat,\n        final @ServerId UUID serverId,\n        final CarbonServer server,\n        final Logger logger,\n        final UserManagerInternal<?> userManager,\n        final NetworkUsers networkUsers,\n        final WhisperCommand.WhisperHandler whisper,\n        final PacketFactory packetFactory,\n        final PartyInvites partyInvites\n    ) {\n        this.serverId = serverId;\n        this.logger = logger;\n        final boolean proxy = ((CarbonChatInternal) carbonChat).isProxy();\n        if (proxy || !configManager.primaryConfig().messagingSettings().enabled()) {\n            if (!proxy) {\n                logger.info(\"Messaging services disabled in config. Cross-server will not work without this!\");\n            } else if (configManager.primaryConfig().messagingSettings().enabled()) {\n                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.\");\n            }\n            this.messagingService = null;\n            this.packetService = null;\n            this.scheduledExecutor = null;\n            return;\n        }\n\n        PacketManager.register(MultiPacket.class, MultiPacket::new);\n        PacketManager.register(KeepAlivePacket.class, KeepAlivePacket::new);\n        PacketManager.register(InitializationPacket.class, InitializationPacket::new);\n        PacketManager.register(PacketVersionPacket.class, PacketVersionPacket::new);\n        PacketManager.register(PacketVersionRequestPacket.class, PacketVersionRequestPacket::new);\n        PacketManager.register(ShutdownPacket.class, ShutdownPacket::new);\n        //PacketManager.register(HeartbeatPacket.class, HeartbeatPacket::new);\n        PacketManager.register(ChatMessagePacket.class, ChatMessagePacket::new);\n        PacketManager.register(SaveCompletedPacket.class, SaveCompletedPacket::new);\n        PacketManager.register(LocalPlayersPacket.class, LocalPlayersPacket::new);\n        PacketManager.register(LocalPlayerChangePacket.class, LocalPlayerChangePacket::new);\n        PacketManager.register(WhisperPacket.class, WhisperPacket::new);\n        PacketManager.register(PartyChangePacket.class, PartyChangePacket::new);\n        PacketManager.register(PartyInvitePacket.class, PartyInvitePacket::new);\n        PacketManager.register(InvalidatePartyInvitePacket.class, InvalidatePartyInvitePacket::new);\n        PacketManager.register(DisbandPartyPacket.class, DisbandPartyPacket::new);\n\n        this.packetService = new PacketService(4, false, protocolVersion);\n        this.scheduledExecutor = new ExceptionLoggingScheduledThreadPoolExecutor(4,\n            ConcurrentUtil.carbonThreadFactory(logger, \"MessagingManager\"), logger);\n\n        final MessagingHandlerImpl handlerImpl = new MessagingHandlerImpl(this.packetService);\n        handlerImpl.addHandler(new CarbonServerHandler(server, serverId, this.packetService, handlerImpl, packetFactory));\n        handlerImpl.addHandler(new CarbonChatPacketHandler(carbonChat, this, userManager, networkUsers, whisper, partyInvites));\n\n        try {\n            this.messagingService = this.initMessagingService(\n                this.packetService,\n                handlerImpl,\n                new File(\"/\"),\n                configManager.primaryConfig().messagingSettings()\n            );\n        } catch (final IOException | TimeoutException | InterruptedException e) {\n            throw Exceptions.rethrow(e);\n        }\n\n        this.packetService.addMessenger(this.messagingService);\n\n        this.packetService.queuePacket(new InitializationPacket(serverId, protocolVersion));\n        this.packetService.flushQueue();\n\n        // Broadcast keepalive packets\n        this.scheduledExecutor.scheduleAtFixedRate(() -> {\n            this.packetService.queuePacket(new KeepAlivePacket(serverId));\n            this.packetService.flushQueue();\n        }, 5, 5, TimeUnit.SECONDS);\n\n        this.scheduledExecutor.scheduleAtFixedRate(() -> {\n            try {\n                this.packetService.flushQueue();\n            } catch (final IndexOutOfBoundsException ignored) {\n\n            }\n        }, 0, 250, TimeUnit.MILLISECONDS);\n    }\n\n    public PacketService requirePacketService() {\n        return Objects.requireNonNull(this.packetService, \"packetService\");\n    }\n\n    private void withPacketService(final Consumer<PacketService> consumer) {\n        if (this.packetService != null) {\n            consumer.accept(this.packetService);\n        }\n    }\n\n    public void queuePacketAndFlush(final Supplier<? extends AbstractPacket> makePacket) {\n        this.withPacketService(service -> {\n            service.queuePacket(makePacket.get());\n            service.flushQueue();\n        });\n    }\n\n    public void queuePacket(final Supplier<? extends AbstractPacket> makePacket) {\n        this.withPacketService(service -> service.queuePacket(makePacket.get()));\n    }\n\n    public void onShutdown() {\n        if (this.scheduledExecutor != null) {\n            ConcurrentUtil.shutdownExecutor(this.scheduledExecutor, TimeUnit.MILLISECONDS, 500);\n        }\n        if (this.packetService != null) {\n            this.packetService.flushQueue();\n            this.packetService.shutdown();\n            this.packetService = null;\n        }\n        if (this.messagingService != null) {\n            this.messagingService.close();\n        }\n    }\n\n    private MessagingService initMessagingService(\n        final PacketService packetService,\n        final MessagingHandlerImpl handlerImpl,\n        final File packetDir,\n        final MessagingSettings messagingSettings\n    ) throws IOException, TimeoutException, InterruptedException {\n        final String name = \"engine1\";\n        final String channelName = \"carbon-data\";\n\n        return switch (messagingSettings.brokerType()) {\n            case RABBITMQ -> {\n                this.logger.info(\"Initializing RabbitMQ Messaging services...\");\n\n                final RabbitMQMessagingService.Builder builder = RabbitMQMessagingService.builder(packetService, name, channelName, this.serverId, handlerImpl, 0L, false, packetDir)\n                    .url(messagingSettings.url(), messagingSettings.port(), messagingSettings.vhost())\n                    .timeout(5000);\n\n                if (messagingSettings.username() != null && !messagingSettings.username().isBlank()) {\n                    builder.credentials(messagingSettings.username(), messagingSettings.password());\n                }\n\n                yield builder.build();\n            }\n            case NATS -> {\n                this.logger.info(\"Initializing NATS Messaging services...\");\n\n                final NATSMessagingService.Builder builder = NATSMessagingService.builder(packetService, name, channelName, this.serverId, handlerImpl, 0L, false, packetDir)\n                    .url(messagingSettings.url(), messagingSettings.port())\n                    .life(5000);\n\n                if (messagingSettings.credentialsFile() != null && !messagingSettings.credentialsFile().isBlank()) {\n                    builder.credentials(messagingSettings.credentialsFile());\n                }\n\n                yield builder.build();\n            }\n            case REDIS -> {\n                this.logger.info(\"Initializing Redis Messaging services...\");\n\n                final RedisMessagingService.Builder builder = RedisMessagingService.builder(packetService, name, channelName, this.serverId, handlerImpl, 0L, false, packetDir)\n                    .url(messagingSettings.url(), messagingSettings.port());\n\n                if (messagingSettings.password() != null && !messagingSettings.password().isBlank()) {\n                    builder.credentials(messagingSettings.password());\n                }\n\n                yield builder.build();\n            }\n            case NONE ->\n                throw new IllegalStateException(\"MessagingManager initialized with no messaging broker selected!\");\n        };\n    }\n\n    public enum BrokerType {\n        NONE,\n        RABBITMQ,\n        NATS,\n        REDIS,\n    }\n\n    private static final class CarbonServerHandler extends AbstractServerMessagingHandler {\n\n        private final CarbonServer server;\n        private final PacketFactory packetFactory;\n\n        private CarbonServerHandler(\n            final @NonNull CarbonServer server,\n            final @NonNull UUID serverId,\n            final @NonNull PacketService packetService,\n            final @NonNull MessagingHandler messagingHandler,\n            final @NonNull PacketFactory packetFactory\n        ) {\n            super(serverId, packetService, messagingHandler);\n            this.server = server;\n            this.packetFactory = packetFactory;\n        }\n\n        @Override\n        protected void handleInitialization(final @NonNull InitializationPacket packet) {\n            super.handleInitialization(packet);\n            final List<? extends CarbonPlayer> players = this.server.players();\n            final Map<UUID, String> map = new HashMap<>();\n            for (final CarbonPlayer player : players) {\n                map.put(player.uuid(), player.username());\n            }\n            this.packetService.queuePacket(this.packetFactory.localPlayersPacket(map));\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/ServerId.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging;\n\nimport com.google.inject.BindingAnnotation;\nimport com.google.inject.Key;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport java.util.UUID;\n\n/**\n * Injection binding annotation for the {@link UUID} identifier of\n * the currently running Carbon instance for cross-server packet\n * communications.\n */\n@BindingAnnotation\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})\npublic @interface ServerId {\n    Key<UUID> KEY = Key.get(UUID.class, ServerId.class);\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/CarbonPacket.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport io.netty.buffer.ByteBuf;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.function.BiConsumer;\nimport java.util.function.Function;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;\nimport ninja.egg82.messenger.packets.AbstractPacket;\nimport org.intellij.lang.annotations.Subst;\nimport org.jetbrains.annotations.NotNull;\n\npublic abstract class CarbonPacket extends AbstractPacket {\n\n    private final GsonComponentSerializer componentSerializer = GsonComponentSerializer.gson();\n\n    protected CarbonPacket(final @NotNull UUID sender) {\n        super(sender);\n    }\n\n    protected final void writeComponent(final Component component, final ByteBuf buffer) {\n        this.writeString(this.componentSerializer.serialize(component), buffer);\n    }\n\n    protected final Component readComponent(final ByteBuf buffer) {\n        return this.componentSerializer.deserialize(this.readString(buffer));\n    }\n\n    protected final void writeKey(final Key key, final ByteBuf buffer) {\n        this.writeString(key.asString(), buffer);\n    }\n\n    protected final Key readKey(final ByteBuf buffer) {\n        final @Subst(\"carbon:channel\") String value = this.readString(buffer);\n\n        return Key.key(value);\n    }\n\n    protected final <K, V> void writeMap(\n        final Map<K, V> map,\n        final BiConsumer<K, ByteBuf> keyWriter,\n        final BiConsumer<V, ByteBuf> valueWriter,\n        final ByteBuf buffer\n    ) {\n        this.writeVarInt(map.size(), buffer);\n\n        for (final Map.Entry<K, V> entry : map.entrySet()) {\n            keyWriter.accept(entry.getKey(), buffer);\n            valueWriter.accept(entry.getValue(), buffer);\n        }\n    }\n\n    protected final <K, V> Map<K, V> readMap(\n        final ByteBuf buffer,\n        final Function<ByteBuf, K> keyReader,\n        final Function<ByteBuf, V> valueReader\n    ) {\n        final int size = this.readVarInt(buffer);\n        final Map<K, V> map = new HashMap<>();\n\n        for (int i = 0; i < size; i++) {\n            map.put(keyReader.apply(buffer), valueReader.apply(buffer));\n        }\n\n        return map;\n    }\n\n    protected final <E extends Enum<E>> void writeEnum(final E value, final ByteBuf buf) {\n        this.writeVarInt(value.ordinal(), buf);\n    }\n\n    protected final <E extends Enum<E>> E readEnum(final ByteBuf buf, final Class<E> cls) {\n        return cls.getEnumConstants()[this.readVarInt(buf)];\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/ChatMessagePacket.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport io.netty.buffer.ByteBuf;\nimport java.util.UUID;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport ninja.egg82.messenger.utils.UUIDUtil;\nimport org.jetbrains.annotations.NotNull;\n\npublic final class ChatMessagePacket extends CarbonPacket {\n\n    // TODO: store item link placeholder components\n    private UUID userId;\n    private String channelPermission;\n    private Key channelKey;\n    private String username;\n    private Component message;\n\n    public UUID userId() {\n        return this.userId;\n    }\n\n    public String channelPermission() {\n        return this.channelPermission;\n    }\n\n    public Key channelKey() {\n        return this.channelKey;\n    }\n\n    public String username() {\n        return this.username;\n    }\n\n    public Component message() {\n        return this.message;\n    }\n\n    public ChatMessagePacket(final @NotNull UUID sender, final @NotNull ByteBuf data) {\n        super(sender);\n        this.read(data);\n    }\n\n    public ChatMessagePacket() {\n        super(UUIDUtil.EMPTY_UUID);\n    }\n\n    public ChatMessagePacket(\n        final @NotNull UUID serverId,\n        final UUID userId,\n        final Key channelKey,\n        final String username,\n        final Component message\n    ) {\n        super(serverId);\n        this.userId = userId;\n        this.channelKey = channelKey;\n        this.username = username;\n        this.message = message;\n    }\n\n    @Override\n    public void read(final io.netty.buffer.@NotNull ByteBuf buffer) {\n        this.userId = this.readUUID(buffer);\n        this.channelKey = this.readKey(buffer);\n        this.username = this.readString(buffer);\n        this.message = this.readComponent(buffer);\n    }\n\n    @Override\n    public void write(final io.netty.buffer.@NotNull ByteBuf buffer) {\n        this.writeUUID(this.userId, buffer);\n        this.writeKey(this.channelKey, buffer);\n        this.writeString(this.username, buffer);\n        this.writeComponent(this.message, buffer);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/DisbandPartyPacket.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport io.netty.buffer.ByteBuf;\nimport java.util.UUID;\nimport net.draycia.carbon.common.messaging.ServerId;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class DisbandPartyPacket extends CarbonPacket {\n\n    private @MonotonicNonNull UUID partyId;\n\n    @AssistedInject\n    public DisbandPartyPacket(\n        final @ServerId UUID serverId,\n        final @Assisted UUID partyId\n    ) {\n        super(serverId);\n        this.partyId = partyId;\n    }\n\n    public DisbandPartyPacket(final UUID sender, final ByteBuf data) {\n        super(sender);\n        this.read(data);\n    }\n\n    public UUID partyId() {\n        return this.partyId;\n    }\n\n    @Override\n    public void read(final ByteBuf buffer) {\n        this.partyId = this.readUUID(buffer);\n    }\n\n    @Override\n    public void write(final ByteBuf buffer) {\n        this.writeUUID(this.partyId, buffer);\n    }\n\n    @Override\n    public String toString() {\n        return \"DisbandPartyPacket{\" +\n            \"partyId=\" + this.partyId +\n            \", sender=\" + this.sender +\n            '}';\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/InvalidatePartyInvitePacket.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport io.netty.buffer.ByteBuf;\nimport java.util.UUID;\nimport net.draycia.carbon.common.messaging.ServerId;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class InvalidatePartyInvitePacket extends CarbonPacket {\n\n    private @MonotonicNonNull UUID from;\n    private @MonotonicNonNull UUID to;\n\n    @AssistedInject\n    public InvalidatePartyInvitePacket(\n        final @ServerId UUID serverId,\n        final @Assisted(\"from\") UUID from,\n        final @Assisted(\"to\") UUID to\n    ) {\n        super(serverId);\n        this.from = from;\n        this.to = to;\n    }\n\n    public InvalidatePartyInvitePacket(final UUID sender, final ByteBuf data) {\n        super(sender);\n        this.read(data);\n    }\n\n    public UUID from() {\n        return this.from;\n    }\n\n    public UUID to() {\n        return this.to;\n    }\n\n    @Override\n    public void read(final ByteBuf buffer) {\n        this.from = this.readUUID(buffer);\n        this.to = this.readUUID(buffer);\n    }\n\n    @Override\n    public void write(final ByteBuf buffer) {\n        this.writeUUID(this.from, buffer);\n        this.writeUUID(this.to, buffer);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/LocalPlayerChangePacket.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport io.netty.buffer.ByteBuf;\nimport java.util.UUID;\nimport net.draycia.carbon.common.messaging.ServerId;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class LocalPlayerChangePacket extends CarbonPacket {\n\n    private @MonotonicNonNull UUID playerId;\n    private @MonotonicNonNull String playerName;\n    private @MonotonicNonNull ChangeType changeType;\n\n    @AssistedInject\n    public LocalPlayerChangePacket(\n        final @ServerId UUID serverId,\n        final @Assisted UUID playerId,\n        final @Assisted @Nullable String playerName,\n        final @Assisted ChangeType changeType\n    ) {\n        super(serverId);\n        if (changeType == ChangeType.ADD && playerName == null) {\n            throw new IllegalArgumentException(\"playerName cannot be null for ChangeType.ADD\");\n        }\n        this.playerId = playerId;\n        this.playerName = playerName;\n        this.changeType = changeType;\n    }\n\n    @AssistedInject\n    public LocalPlayerChangePacket(final @ServerId UUID serverId, final @Assisted UUID playerId) {\n        super(serverId);\n        this.playerId = playerId;\n        this.playerName = null;\n        this.changeType = ChangeType.REMOVE;\n    }\n\n    public LocalPlayerChangePacket(final UUID sender, final ByteBuf data) {\n        super(sender);\n        this.read(data);\n    }\n\n    public UUID playerId() {\n        return this.playerId;\n    }\n\n    public String playerName() {\n        return this.playerName;\n    }\n\n    public ChangeType changeType() {\n        return this.changeType;\n    }\n\n    @Override\n    public void read(final ByteBuf buffer) {\n        this.playerId = this.readUUID(buffer);\n        final String type = this.readString(buffer);\n        this.changeType = ChangeType.valueOf(type);\n        if (this.changeType == ChangeType.ADD) {\n            this.playerName = this.readString(buffer);\n        }\n    }\n\n    @Override\n    public void write(final ByteBuf buffer) {\n        this.writeUUID(this.playerId, buffer);\n        this.writeString(this.changeType.name(), buffer);\n        if (this.changeType == ChangeType.ADD) {\n            this.writeString(this.playerName, buffer);\n        }\n    }\n\n    public enum ChangeType {\n        ADD, REMOVE\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/LocalPlayersPacket.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport io.netty.buffer.ByteBuf;\nimport java.util.Map;\nimport java.util.UUID;\nimport net.draycia.carbon.common.messaging.ServerId;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class LocalPlayersPacket extends CarbonPacket {\n\n    private @MonotonicNonNull Map<UUID, String> players;\n\n    @AssistedInject\n    public LocalPlayersPacket(\n        final @ServerId UUID serverId,\n        final @Assisted Map<UUID, String> players\n    ) {\n        super(serverId);\n        this.players = players;\n    }\n\n    public LocalPlayersPacket(final UUID sender, final ByteBuf data) {\n        super(sender);\n        this.read(data);\n    }\n\n    public Map<UUID, String> players() {\n        return this.players;\n    }\n\n    @Override\n    public void read(final ByteBuf buffer) {\n        this.players = this.readMap(buffer, this::readUUID, this::readString);\n    }\n\n    @Override\n    public void write(final ByteBuf buffer) {\n        this.writeMap(this.players, this::writeUUID, this::writeString, buffer);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/PacketFactory.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport com.google.inject.assistedinject.Assisted;\nimport java.util.Map;\nimport java.util.UUID;\nimport net.draycia.carbon.common.users.PartyImpl;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic interface PacketFactory {\n\n    SaveCompletedPacket saveCompletedPacket(UUID playerId);\n\n    LocalPlayersPacket localPlayersPacket(Map<UUID, String> players);\n\n    default LocalPlayersPacket clearLocalPlayersPacket() {\n        return this.localPlayersPacket(Map.of());\n    }\n\n    LocalPlayerChangePacket localPlayerChangePacket(UUID player, @Nullable String name, LocalPlayerChangePacket.ChangeType type);\n\n    default LocalPlayerChangePacket addLocalPlayerPacket(final UUID id, final String name) {\n        return this.localPlayerChangePacket(id, name, LocalPlayerChangePacket.ChangeType.ADD);\n    }\n\n    LocalPlayerChangePacket removeLocalPlayerPacket(final UUID id);\n\n    WhisperPacket whisperPacket(@Assisted(\"from\") UUID from, @Assisted(\"to\") UUID to, Component msg);\n\n    PartyChangePacket partyChange(UUID partyId, Map<UUID, PartyImpl.ChangeType> changes);\n\n    PartyInvitePacket partyInvite(@Assisted(\"from\") UUID from, @Assisted(\"to\") UUID to, @Assisted(\"party\") UUID party);\n\n    InvalidatePartyInvitePacket invalidatePartyInvite(@Assisted(\"from\") UUID from, @Assisted(\"to\") UUID to);\n\n    DisbandPartyPacket disbandParty(UUID party);\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyChangePacket.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport io.netty.buffer.ByteBuf;\nimport java.util.Map;\nimport java.util.UUID;\nimport net.draycia.carbon.common.messaging.ServerId;\nimport net.draycia.carbon.common.users.PartyImpl;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class PartyChangePacket extends CarbonPacket {\n\n    private @MonotonicNonNull UUID partyId;\n    private @MonotonicNonNull Map<UUID, PartyImpl.ChangeType> changes;\n\n    @AssistedInject\n    public PartyChangePacket(\n        final @ServerId UUID serverId,\n        final @Assisted UUID partyId,\n        final @Assisted Map<UUID, PartyImpl.ChangeType> changes\n    ) {\n        super(serverId);\n        this.partyId = partyId;\n        this.changes = changes;\n    }\n\n    public PartyChangePacket(final UUID sender, final ByteBuf data) {\n        super(sender);\n        this.read(data);\n    }\n\n    public UUID partyId() {\n        return this.partyId;\n    }\n\n    public Map<UUID, PartyImpl.ChangeType> changes() {\n        return this.changes;\n    }\n\n    @Override\n    public void read(final ByteBuf buffer) {\n        this.partyId = this.readUUID(buffer);\n        this.changes = this.readMap(buffer, this::readUUID, buf -> this.readEnum(buf, PartyImpl.ChangeType.class));\n    }\n\n    @Override\n    public void write(final ByteBuf buffer) {\n        this.writeUUID(this.partyId, buffer);\n        this.writeMap(this.changes, this::writeUUID, this::writeEnum, buffer);\n    }\n\n    @Override\n    public String toString() {\n        return \"PartyChangePacket{\" +\n            \"partyId=\" + this.partyId +\n            \", changes=\" + this.changes +\n            \", sender=\" + this.sender +\n            '}';\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/PartyInvitePacket.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport io.netty.buffer.ByteBuf;\nimport java.util.UUID;\nimport net.draycia.carbon.common.messaging.ServerId;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class PartyInvitePacket extends CarbonPacket {\n\n    private @MonotonicNonNull UUID from;\n    private @MonotonicNonNull UUID to;\n    private @MonotonicNonNull UUID party;\n\n    @AssistedInject\n    public PartyInvitePacket(\n        final @ServerId UUID serverId,\n        final @Assisted(\"from\") UUID from,\n        final @Assisted(\"to\") UUID to,\n        final @Assisted(\"party\") UUID party\n    ) {\n        super(serverId);\n        this.from = from;\n        this.to = to;\n        this.party = party;\n    }\n\n    public PartyInvitePacket(final UUID sender, final ByteBuf data) {\n        super(sender);\n        this.read(data);\n    }\n\n    public UUID from() {\n        return this.from;\n    }\n\n    public UUID to() {\n        return this.to;\n    }\n\n    public UUID party() {\n        return this.party;\n    }\n\n    @Override\n    public void read(final ByteBuf buffer) {\n        this.from = this.readUUID(buffer);\n        this.to = this.readUUID(buffer);\n        this.party = this.readUUID(buffer);\n    }\n\n    @Override\n    public void write(final ByteBuf buffer) {\n        this.writeUUID(this.from, buffer);\n        this.writeUUID(this.to, buffer);\n        this.writeUUID(this.party, buffer);\n    }\n\n    @Override\n    public String toString() {\n        return \"PartyInvitePacket{\" +\n            \"from=\" + this.from +\n            \", to=\" + this.to +\n            \", party=\" + this.party +\n            \", sender=\" + this.sender +\n            '}';\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/SaveCompletedPacket.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport io.netty.buffer.ByteBuf;\nimport java.util.UUID;\nimport net.draycia.carbon.common.messaging.ServerId;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class SaveCompletedPacket extends CarbonPacket {\n\n    private @MonotonicNonNull UUID player;\n\n    @AssistedInject\n    public SaveCompletedPacket(final @ServerId UUID serverId, final @Assisted UUID player) {\n        super(serverId);\n        this.player = player;\n    }\n\n    public SaveCompletedPacket(final UUID sender, final ByteBuf data) {\n        super(sender);\n        this.read(data);\n    }\n\n    public UUID playerId() {\n        return this.player;\n    }\n\n    @Override\n    public void read(final ByteBuf buffer) {\n        this.player = this.readUUID(buffer);\n    }\n\n    @Override\n    public void write(final ByteBuf buffer) {\n        this.writeUUID(this.player, buffer);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/messaging/packets/WhisperPacket.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.messaging.packets;\n\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport io.netty.buffer.ByteBuf;\nimport java.util.UUID;\nimport net.draycia.carbon.common.messaging.ServerId;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class WhisperPacket extends CarbonPacket {\n\n    private @MonotonicNonNull UUID from;\n    private @MonotonicNonNull UUID to;\n    private @MonotonicNonNull Component message;\n\n    @AssistedInject\n    public WhisperPacket(\n        final @ServerId UUID serverId,\n        final @Assisted(\"from\") UUID from,\n        final @Assisted(\"to\") UUID to,\n        final @Assisted Component message\n    ) {\n        super(serverId);\n        this.from = from;\n        this.to = to;\n        this.message = message;\n    }\n\n    public WhisperPacket(final UUID sender, final ByteBuf data) {\n        super(sender);\n        this.read(data);\n    }\n\n    public UUID from() {\n        return this.from;\n    }\n\n    public UUID to() {\n        return this.to;\n    }\n\n    public Component message() {\n        return this.message;\n    }\n\n    @Override\n    public void read(final ByteBuf buffer) {\n        this.from = this.readUUID(buffer);\n        this.to = this.readUUID(buffer);\n        this.message = this.readComponent(buffer);\n    }\n\n    @Override\n    public void write(final ByteBuf buffer) {\n        this.writeUUID(this.from, buffer);\n        this.writeUUID(this.to, buffer);\n        this.writeComponent(this.message, buffer);\n    }\n\n    @Override\n    public String toString() {\n        return \"WhisperPacket{\" +\n            \"from=\" + this.from +\n            \", to=\" + this.to +\n            \", message=\" + this.message +\n            \", sender=\" + this.sender +\n            '}';\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/serialisation/gson/ChatChannelSerializerGson.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.serialisation.gson;\n\nimport com.google.gson.TypeAdapter;\nimport com.google.gson.stream.JsonReader;\nimport com.google.gson.stream.JsonWriter;\nimport com.google.inject.Inject;\nimport java.io.IOException;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.intellij.lang.annotations.Subst;\n\nimport static net.kyori.adventure.key.Key.key;\n\n@DefaultQualifier(NonNull.class)\npublic class ChatChannelSerializerGson extends TypeAdapter<ChatChannel> {\n\n    private final ChannelRegistry registry;\n\n    @Inject\n    public ChatChannelSerializerGson(final ChannelRegistry registry) {\n        this.registry = registry;\n    }\n\n    @Override\n    public void write(final JsonWriter out, final @Nullable ChatChannel value) throws IOException {\n        if (value == null) {\n            out.value((String) null);\n        } else {\n            out.value(value.key().asString());\n        }\n    }\n\n    @Override\n    public @Nullable ChatChannel read(final JsonReader in) throws IOException {\n        @Subst(\"namespace:value\") final @Nullable String channelName = in.nextString();\n\n        if (channelName != null) {\n            return this.registry.channel(key(channelName));\n        }\n\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/serialisation/gson/LocaleSerializerConfigurate.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.serialisation.gson;\n\nimport com.google.inject.Inject;\nimport java.lang.reflect.Type;\nimport java.util.Locale;\nimport net.kyori.adventure.translation.Translator;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.ConfigurationNode;\nimport org.spongepowered.configurate.serialize.SerializationException;\nimport org.spongepowered.configurate.serialize.TypeSerializer;\n\nimport static java.util.Objects.requireNonNull;\n\n@DefaultQualifier(NonNull.class)\npublic class LocaleSerializerConfigurate implements TypeSerializer<Locale> {\n\n    private final Logger logger;\n\n    @Inject\n    public LocaleSerializerConfigurate(final Logger logger) {\n        this.logger = logger;\n    }\n\n    @Override\n    public Locale deserialize(final Type type, final ConfigurationNode node) {\n        final @Nullable String value = node.getString();\n\n        if (value == null) {\n            this.logger.warn(\"value null for locale! defaulting to en_US\");\n            return Locale.ENGLISH;\n        }\n\n        return requireNonNull(Translator.parseLocale(value), \"value locale cannot be null!\");\n    }\n\n    @Override\n    public void serialize(final Type type, final @Nullable Locale obj, final ConfigurationNode node) throws SerializationException {\n        if (obj == null) {\n            node.set(null);\n        } else {\n            node.set(obj.toString());\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/serialisation/gson/UUIDSerializerGson.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.serialisation.gson;\n\nimport com.google.gson.TypeAdapter;\nimport com.google.gson.stream.JsonReader;\nimport com.google.gson.stream.JsonWriter;\nimport java.io.IOException;\nimport java.util.UUID;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class UUIDSerializerGson extends TypeAdapter<UUID> {\n\n    @Override\n    public void write(final JsonWriter jsonWriter, final @Nullable UUID uuid) throws IOException {\n        if (uuid != null) {\n            jsonWriter.value(uuid.toString());\n        } else {\n            jsonWriter.value((String) null);\n        }\n    }\n\n    @Override\n    public UUID read(final JsonReader jsonReader) throws IOException {\n        return UUID.fromString(jsonReader.nextString());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/Backing.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport com.google.inject.BindingAnnotation;\nimport java.lang.annotation.ElementType;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.annotation.Target;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.users.db.DatabaseUserManager;\nimport net.draycia.carbon.common.users.json.JSONUserManager;\n\n/**\n * Injection binding annotation for the backing {@link UserManagerInternal}\n * (i.e. {@link JSONUserManager} or {@link DatabaseUserManager}),\n * with the generic type of {@link CarbonPlayerCommon}.\n *\n * <p>Injecting {@link UserManagerInternal} or {@link UserManager} with a generic type of {@literal ?}, without this annotation,\n * will inject the {@link PlatformUserManager}, which wraps the backing manager (this is generally what you want).</p>\n */\n@BindingAnnotation\n@Retention(RetentionPolicy.RUNTIME)\n@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})\npublic @interface Backing {\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/CachingUserManager.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport com.github.benmanes.caffeine.cache.AsyncCache;\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport com.google.inject.Injector;\nimport com.google.inject.Provider;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicBoolean;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.concurrent.locks.ReentrantLock;\nimport java.util.function.Function;\nimport java.util.stream.Collectors;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.messaging.packets.DisbandPartyPacket;\nimport net.draycia.carbon.common.messaging.packets.PacketFactory;\nimport net.draycia.carbon.common.messaging.packets.PartyChangePacket;\nimport net.draycia.carbon.common.users.db.DatabaseUserManager;\nimport net.draycia.carbon.common.util.ConcurrentUtil;\nimport net.kyori.adventure.text.Component;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static net.draycia.carbon.common.users.PlayerUtils.saveExceptionHandler;\n\n@DefaultQualifier(NonNull.class)\npublic abstract class CachingUserManager implements UserManagerInternal<CarbonPlayerCommon> {\n\n    private static final int DISBAND_DELAY = 10;\n\n    protected final Logger logger;\n    protected final ProfileResolver profileResolver;\n    private final ExecutorService executor;\n    private final Injector injector;\n    private final Provider<MessagingManager> messagingManager;\n    private final PacketFactory packetFactory;\n    private final CarbonServer server;\n    private final ReentrantLock cacheLock;\n    private final Map<UUID, CompletableFuture<CarbonPlayerCommon>> cache;\n    private final AsyncCache<UUID, Party> partyCache;\n    private final List<Runnable> queuedDisbands = new CopyOnWriteArrayList<>();\n    private final Cache<UUID, Object> recentDisbands = Caffeine.newBuilder()\n        .expireAfterWrite(DISBAND_DELAY + 10, TimeUnit.SECONDS)\n        .build();\n\n    protected CachingUserManager(\n        final Logger logger,\n        final ProfileResolver profileResolver,\n        final Injector injector,\n        final Provider<MessagingManager> messagingManager,\n        final PacketFactory packetFactory,\n        final CarbonServer server\n    ) {\n        this.logger = logger;\n        this.executor = Executors.newSingleThreadExecutor(ConcurrentUtil.carbonThreadFactory(logger, this.getClass().getSimpleName()));\n        this.partyCache = Caffeine.newBuilder()\n            .expireAfterAccess(Duration.ofMinutes(5))\n            .buildAsync();\n        this.profileResolver = profileResolver;\n        this.injector = injector;\n        this.messagingManager = messagingManager;\n        this.packetFactory = packetFactory;\n        this.server = server;\n        this.cacheLock = new ReentrantLock();\n        this.cache = new HashMap<>();\n    }\n\n    protected abstract CarbonPlayerCommon loadOrCreate(UUID uuid);\n\n    protected abstract void saveSync(CarbonPlayerCommon player);\n\n    protected abstract @Nullable PartyImpl loadParty(UUID uuid);\n\n    protected abstract void saveSync(PartyImpl info, Map<UUID, PartyImpl.ChangeType> polledChanges);\n\n    protected abstract void disbandSync(UUID id);\n\n    private CompletableFuture<Void> save(final CarbonPlayerCommon player) {\n        return CompletableFuture.runAsync(() -> {\n            this.saveSync(player);\n            player.saved();\n            this.messagingManager.get().queuePacketAndFlush(() -> this.packetFactory.saveCompletedPacket(player.uuid()));\n        }, this.executor);\n    }\n\n    @Override\n    public Party createParty(final Component name) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public void saveCompleteMessageReceived(final UUID playerId) {\n        this.cacheLock.lock();\n        try {\n            this.cache.remove(playerId);\n        } finally {\n            this.cacheLock.unlock();\n        }\n    }\n\n    @Override\n    public CompletableFuture<Void> saveIfNeeded(final CarbonPlayerCommon player) {\n        if (!player.needsSave()) {\n            return CompletableFuture.completedFuture(null);\n        }\n        return this.save(player);\n    }\n\n    @Override\n    public CompletableFuture<CarbonPlayerCommon> user(final UUID uuid) {\n        this.cacheLock.lock();\n        try {\n            return this.cache.computeIfAbsent(uuid, $ -> {\n                final CompletableFuture<CarbonPlayerCommon> future = CompletableFuture.supplyAsync(() -> {\n                    final CarbonPlayerCommon player = this.loadOrCreate(uuid);\n                    this.injector.injectMembers(player);\n                    if (this instanceof DatabaseUserManager) {\n                        player.registerPropertyUpdateListener(() ->\n                            this.save(player).exceptionally(saveExceptionHandler(this.logger, player.username, uuid)));\n                    }\n                    return player;\n                }, this.executor);\n                this.attachPostLoad(uuid, future);\n                return future;\n            });\n        } finally {\n            this.cacheLock.unlock();\n        }\n    }\n\n    @Override\n    public void shutdown() {\n        this.cacheLock.lock();\n        for (final Runnable task : this.queuedDisbands) {\n            task.run();\n        }\n        try {\n            final Map<UUID, CompletableFuture<Void>> collect = List.copyOf(this.cache.keySet()).stream()\n                .collect(Collectors.toMap(Function.identity(), this::loggedOut));\n            for (final Map.Entry<UUID, CompletableFuture<Void>> entry : collect.entrySet()) {\n                try {\n                    entry.getValue().join();\n                } catch (final Exception ex) {\n                    this.logger.warn(\"Exception saving data for player with uuid '{}'\", entry.getKey(), ex);\n                }\n            }\n            ConcurrentUtil.shutdownExecutor(this.executor, TimeUnit.MILLISECONDS, 500);\n        } finally {\n            this.cacheLock.unlock();\n        }\n    }\n\n    @Override\n    public CompletableFuture<Void> loggedOut(final UUID uuid) {\n        this.messagingManager.get().queuePacket(() -> this.packetFactory.removeLocalPlayerPacket(uuid));\n        this.cacheLock.lock();\n        try {\n            final @Nullable CompletableFuture<CarbonPlayerCommon> remove = this.cache.remove(uuid);\n            if (remove != null && remove.isDone()) { // don't need to save if it never finished loading\n                final @Nullable CarbonPlayerCommon join = remove.join();\n                if (join != null) {\n                    return this.saveIfNeeded(join);\n                }\n            }\n            return CompletableFuture.completedFuture(null);\n        } finally {\n            this.cacheLock.unlock();\n        }\n    }\n\n    @Override\n    public void cleanup() {\n        this.cacheLock.lock();\n        try {\n            for (final Map.Entry<UUID, CompletableFuture<CarbonPlayerCommon>> entry : Map.copyOf(this.cache).entrySet()) {\n                final @Nullable CarbonPlayerCommon getNow = entry.getValue().getNow(null);\n                if (getNow == null || !getNow.transientLoadedNeedsUnload()) {\n                    continue;\n                }\n                this.cache.remove(entry.getKey());\n                this.saveIfNeeded(getNow).exceptionally(saveExceptionHandler(this.logger, getNow.username, getNow.uuid()));\n            }\n        } finally {\n            this.cacheLock.unlock();\n        }\n    }\n\n    // Don't keep failed requests, so they can be retried on the next request\n    // The caller is expected to handle the error\n    private void attachPostLoad(final UUID uuid, final CompletableFuture<CarbonPlayerCommon> future) {\n        future.whenComplete((result, thr) -> {\n            if (result == null || thr != null) {\n                this.cacheLock.lock();\n                try {\n                    this.cache.remove(uuid);\n                } finally {\n                    this.cacheLock.unlock();\n                }\n            }\n        });\n    }\n\n    @Override\n    public CompletableFuture<@Nullable Party> party(final UUID id) {\n        // we delay party deletion for cross-server purposes, so ignore present data when we know it was recently disbanded\n        if (this.recentDisbands.getIfPresent(id) != null) {\n            return CompletableFuture.completedFuture(null);\n        }\n        return this.partyCache.get(id, (uuid, cacheExecutor) -> CompletableFuture.supplyAsync(() -> {\n            final @Nullable PartyImpl party = this.loadParty(uuid);\n            if (party != null) {\n                this.injector.injectMembers(party);\n            }\n            return party;\n        }, this.executor));\n    }\n\n    @Override\n    public CompletableFuture<Void> saveParty(final PartyImpl info) {\n        return CompletableFuture.runAsync(() -> {\n            final Map<UUID, PartyImpl.ChangeType> changes = info.pollChanges();\n            if (changes.isEmpty()) {\n                return;\n            }\n            this.saveSync(info, changes);\n            this.messagingManager.get().queuePacketAndFlush(() -> this.packetFactory.partyChange(info.id(), changes));\n        }, this.executor);\n    }\n\n    @Override\n    public final void disbandParty(final UUID id) {\n        this.partyCache.synchronous().invalidate(id);\n        final AtomicBoolean ran = new AtomicBoolean(false);\n        final AtomicReference<Runnable> taskRef = new AtomicReference<>();\n        final Runnable task = () -> {\n            if (ran.compareAndSet(false, true)) {\n                this.disbandSync(id);\n                this.queuedDisbands.remove(taskRef.get());\n            }\n        };\n        taskRef.set(task);\n        this.queuedDisbands.add(task);\n        this.recentDisbands.put(id, new Object());\n        // delay deletion so other servers can post leave events\n        CompletableFuture.delayedExecutor(DISBAND_DELAY, TimeUnit.SECONDS, this.executor).execute(task);\n        this.messagingManager.get().queuePacketAndFlush(() -> this.packetFactory.disbandParty(id));\n    }\n\n    @Override\n    public void partyChangeMessageReceived(final PartyChangePacket pkt) {\n        final @Nullable CompletableFuture<@Nullable Party> future = this.partyIfMemberOnline(pkt.partyId());\n        if (future == null) {\n            return;\n        }\n        future.thenAccept(party -> {\n            if (party == null) {\n                return;\n            }\n            final PartyImpl impl = (PartyImpl) party;\n            pkt.changes().forEach((id, type) -> {\n                switch (type) {\n                    case ADD -> impl.addMemberRaw(id);\n                    case REMOVE -> impl.removeMemberRaw(id);\n                }\n            });\n        }).whenComplete(($, thr) -> {\n            if (thr != null) {\n                this.logger.warn(\"Exception handling party change packet {}\", pkt, thr);\n            }\n        });\n    }\n\n    private @Nullable CompletableFuture<@Nullable Party> partyIfMemberOnline(final UUID partyId) {\n        @Nullable CompletableFuture<@Nullable Party> future = this.partyCache.getIfPresent(partyId);\n        if (future == null) {\n            // we want to notify any online members even if the party isn't loaded locally yet\n            for (final CarbonPlayer player : this.server.players()) {\n                if (partyId.equals(((WrappedCarbonPlayer) player).partyId())) {\n                    future = this.party(partyId);\n                }\n            }\n        }\n        return future;\n    }\n\n    @Override\n    public void disbandPartyMessageReceived(final DisbandPartyPacket pkt) {\n        final @Nullable CompletableFuture<@Nullable Party> future = this.partyIfMemberOnline(pkt.partyId());\n        this.recentDisbands.put(pkt.partyId(), new Object());\n        if (future == null) {\n            return;\n        }\n        future.thenAccept(party -> {\n            if (party == null) {\n                return;\n            }\n            ((PartyImpl) party).disbandRaw();\n            this.partyCache.synchronous().invalidate(pkt.partyId());\n        }).whenComplete(($, thr) -> {\n            if (thr != null) {\n                this.logger.warn(\"Exception handling party disband packet {}\", pkt, thr);\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/CarbonPlayerCommon.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport com.google.inject.Inject;\nimport java.time.Duration;\nimport java.time.Instant;\nimport java.util.Collections;\nimport java.util.HashSet;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.stream.Stream;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.api.util.InventorySlot;\nimport net.draycia.carbon.common.PlatformScheduler;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.messages.CarbonMessageRenderer;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport net.kyori.adventure.identity.Identity;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class CarbonPlayerCommon implements CarbonPlayer, ForwardingAudience.Single {\n\n    private static final long KEEP_TRANSIENT_LOADS_FOR = Duration.ofMinutes(2).toMillis();\n\n    private transient @MonotonicNonNull @Inject ChannelRegistry channelRegistry;\n    private transient @MonotonicNonNull @Inject ProfileResolver profileResolver;\n    private transient @MonotonicNonNull @Inject PlatformScheduler scheduler;\n    private transient @MonotonicNonNull @Inject ConfigManager config;\n    private transient @MonotonicNonNull @Inject CarbonMessageRenderer messageRenderer;\n    private transient @MonotonicNonNull @Inject UserManagerInternal<?> users;\n    private transient @MonotonicNonNull @Inject CarbonMessages messages;\n    private volatile transient long transientLoadedSince = -1;\n\n    protected final PersistentUserProperty<Boolean> muted;\n    protected final PersistentUserProperty<Long> muteExpiration;\n    protected final PersistentUserProperty<Boolean> deafened;\n    protected final PersistentUserProperty<Key> selectedChannel;\n\n    // All players have these\n    protected transient @MonotonicNonNull String username = null;\n    protected @MonotonicNonNull UUID uuid;\n\n    // Display information\n    protected final PersistentUserProperty<Component> displayName;\n\n    // Whispers\n    protected final PersistentUserProperty<UUID> lastWhisperTarget;\n    protected final PersistentUserProperty<UUID> whisperReplyTarget;\n    protected final PersistentUserProperty<Boolean> ignoringDirectMessages;\n\n    // Administrative\n    protected final PersistentUserProperty<Boolean> spying;\n    protected final PersistentUserProperty<Boolean> applyOptionalChatFilters;\n\n    // Punishments\n    protected final PersistentUserProperty<Set<UUID>> ignoredPlayers;\n\n    protected final PersistentUserProperty<Set<Key>> leftChannels;\n\n    protected final PersistentUserProperty<UUID> party;\n\n    public CarbonPlayerCommon(\n        final boolean muted,\n        final long muteExpiration,\n        final boolean deafened,\n        final @Nullable Key selectedChannel,\n        final @Nullable String username, // will be resolved when requested\n        final UUID uuid,\n        final @Nullable Component displayName,\n        final @Nullable UUID lastWhisperTarget,\n        final @Nullable UUID whisperReplyTarget,\n        final boolean spying,\n        final boolean ignoreDirectMessages,\n        final @Nullable UUID party,\n        final boolean applyOptionalChatFilters\n    ) {\n        this.muted = PersistentUserProperty.of(muted);\n        this.muteExpiration = PersistentUserProperty.of(muteExpiration);\n        this.deafened = PersistentUserProperty.of(deafened);\n        this.selectedChannel = PersistentUserProperty.of(selectedChannel);\n        this.username = username;\n        this.uuid = uuid;\n        this.displayName = PersistentUserProperty.of(displayName);\n        this.lastWhisperTarget = PersistentUserProperty.of(lastWhisperTarget);\n        this.whisperReplyTarget = PersistentUserProperty.of(whisperReplyTarget);\n        this.spying = PersistentUserProperty.of(spying);\n        this.ignoredPlayers = PersistentUserProperty.of(Collections.emptySet());\n        this.leftChannels = PersistentUserProperty.of(Collections.emptySet());\n        this.ignoringDirectMessages = PersistentUserProperty.of(ignoreDirectMessages);\n        this.party = PersistentUserProperty.of(party);\n        this.applyOptionalChatFilters = PersistentUserProperty.of(applyOptionalChatFilters);\n    }\n\n    public CarbonPlayerCommon(\n        final @Nullable String username, // will be resolved when requested\n        final UUID uuid\n    ) {\n        this.muted = PersistentUserProperty.of(false);\n        this.muteExpiration = PersistentUserProperty.of(0L);\n        this.deafened = PersistentUserProperty.of(false);\n        this.selectedChannel = PersistentUserProperty.empty();\n        this.displayName = PersistentUserProperty.empty();\n        this.lastWhisperTarget = PersistentUserProperty.empty();\n        this.whisperReplyTarget = PersistentUserProperty.empty();\n        this.spying = PersistentUserProperty.of(false);\n        this.ignoredPlayers = PersistentUserProperty.of(Collections.emptySet());\n        this.leftChannels = PersistentUserProperty.of(Collections.emptySet());\n        this.username = username;\n        this.uuid = uuid;\n        this.ignoringDirectMessages = PersistentUserProperty.of(false);\n        this.party = PersistentUserProperty.empty();\n        this.applyOptionalChatFilters = PersistentUserProperty.of(true);\n    }\n\n    public CarbonPlayerCommon() {\n        this.muted = PersistentUserProperty.of(false);\n        this.muteExpiration = PersistentUserProperty.of(0L);\n        this.deafened = PersistentUserProperty.of(false);\n        this.selectedChannel = PersistentUserProperty.empty();\n        this.displayName = PersistentUserProperty.empty();\n        this.lastWhisperTarget = PersistentUserProperty.empty();\n        this.whisperReplyTarget = PersistentUserProperty.empty();\n        this.spying = PersistentUserProperty.of(false);\n        this.applyOptionalChatFilters = PersistentUserProperty.of(true);\n        this.ignoredPlayers = PersistentUserProperty.of(Collections.emptySet());\n        this.leftChannels = PersistentUserProperty.of(Collections.emptySet());\n        this.ignoringDirectMessages = PersistentUserProperty.of(false);\n        this.party = PersistentUserProperty.empty();\n    }\n\n    public boolean needsSave() {\n        return this.properties().anyMatch(PersistentUserProperty::changed);\n    }\n\n    private Stream<PersistentUserProperty<?>> properties() {\n        return Stream.of(\n            this.muted,\n            this.muteExpiration,\n            this.deafened,\n            this.selectedChannel,\n            this.displayName,\n            this.lastWhisperTarget,\n            this.whisperReplyTarget,\n            this.spying,\n            this.applyOptionalChatFilters,\n            this.ignoredPlayers,\n            this.leftChannels,\n            this.ignoringDirectMessages,\n            this.party\n        );\n    }\n\n    public void schedule(final Runnable task) {\n        this.scheduler.scheduleForPlayer(this, task);\n    }\n\n    public void registerPropertyUpdateListener(final Runnable task) {\n        this.properties().forEach(prop -> prop.registerUpdateListener(task));\n    }\n\n    @Override\n    public Audience audience() {\n        return Audience.empty();\n    }\n\n    @Override\n    public @Nullable Component createItemHoverComponent(final InventorySlot slot) {\n        return null;\n    }\n\n    @Override\n    public @Nullable Component nickname() {\n        if (!this.config.primaryConfig().nickname().useCarbonNicknames()) {\n            return null;\n        }\n        return this.displayName.orNull();\n    }\n\n    public @Nullable Component nicknameRaw() {\n        return this.displayName.orNull();\n    }\n\n    @Override\n    public void nickname(final @Nullable Component nickname) {\n        this.displayName.set(nickname);\n    }\n\n    @Override\n    public boolean hasPermission(final String permission) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public String primaryGroup() {\n        return \"default\";\n    }\n\n    @Override\n    public List<String> groups() {\n        return List.of(\"default\");\n    }\n\n    @Override\n    public boolean muted() {\n        if (this.muted.get()) {\n            if (this.muteExpiration() > 0) {\n                return Instant.now().toEpochMilli() < this.muteExpiration();\n            }\n\n            return true;\n        }\n\n        return false;\n    }\n\n    @Override\n    public void muted(final boolean muted) {\n        this.muted.set(muted);\n    }\n\n    @Override\n    public long muteExpiration() {\n        return this.muteExpiration.get();\n    }\n\n    @Override\n    public void muteExpiration(final long epochMillis) {\n        this.muteExpiration.set(epochMillis);\n    }\n\n    @Override\n    public Set<UUID> ignoring() {\n        return this.ignoredPlayers.get();\n    }\n\n    @Override\n    public boolean ignoring(final UUID player) {\n        return this.ignoredPlayers.get().contains(player);\n    }\n\n    @Override\n    public boolean ignoring(final CarbonPlayer player) {\n        return this.ignoring(player.uuid());\n    }\n\n    public void ignoring(final UUID player, final boolean nowIgnoring, final boolean internal) {\n        final Set<UUID> newIgnored = new HashSet<>(this.ignoredPlayers.get());\n        if (nowIgnoring) {\n            newIgnored.add(player);\n        } else {\n            newIgnored.remove(player);\n        }\n        if (internal) {\n            this.ignoredPlayers.internalSet(Collections.unmodifiableSet(newIgnored));\n        } else {\n            this.ignoredPlayers.set(Collections.unmodifiableSet(newIgnored));\n        }\n    }\n\n    @Override\n    public void ignoring(final UUID player, final boolean nowIgnoring) {\n        this.ignoring(player, nowIgnoring, false);\n    }\n\n    @Override\n    public void ignoring(final CarbonPlayer player, final boolean nowIgnoring) {\n        this.ignoring(player.uuid(), nowIgnoring);\n    }\n\n    @Override\n    public boolean deafened() {\n        return this.deafened.get();\n    }\n\n    @Override\n    public void deafened(final boolean deafened) {\n        this.deafened.set(deafened);\n    }\n\n    @Override\n    public boolean spying() {\n        return this.spying.get();\n    }\n\n    @Override\n    public void spying(final boolean spying) {\n        this.spying.set(spying);\n    }\n\n    @Override\n    public boolean ignoringDirectMessages() {\n        return this.ignoringDirectMessages.get();\n    }\n\n    @Override\n    public void ignoringDirectMessages(final boolean ignoring) {\n        this.ignoringDirectMessages.set(ignoring);\n    }\n\n    @Override\n    public void sendMessageAsPlayer(final String message) {\n\n    }\n\n    @Override\n    public boolean online() {\n        return false;\n    }\n\n    @Override\n    public @Nullable UUID whisperReplyTarget() {\n        return this.whisperReplyTarget.orNull();\n    }\n\n    @Override\n    public void whisperReplyTarget(final @Nullable UUID whisperReplyTarget) {\n        this.whisperReplyTarget.set(whisperReplyTarget);\n    }\n\n    @Override\n    public @Nullable UUID lastWhisperTarget() {\n        return this.lastWhisperTarget.orNull();\n    }\n\n    @Override\n    public void lastWhisperTarget(final @Nullable UUID lastWhisperTarget) {\n        this.lastWhisperTarget.set(lastWhisperTarget);\n    }\n\n    @Override\n    public boolean vanished() {\n        return false;\n    }\n\n    @Override\n    public boolean awareOf(final CarbonPlayer other) {\n        return true;\n    }\n\n    @Override\n    public List<Key> leftChannels() {\n        return List.copyOf(this.leftChannels.get());\n    }\n\n    public void joinChannel(final Key key, final boolean internal) {\n        final Set<Key> newKeys = new HashSet<>(this.leftChannels.get());\n        newKeys.remove(key);\n        if (internal) {\n            this.leftChannels.internalSet(Collections.unmodifiableSet(newKeys));\n        } else {\n            this.leftChannels.set(Collections.unmodifiableSet(newKeys));\n        }\n    }\n\n    @Override\n    public void joinChannel(final ChatChannel channel) {\n        this.joinChannel(channel.key(), false);\n    }\n\n    public void leaveChannel(final ChatChannel channel, final boolean internal) {\n        final Set<Key> newKeys = new HashSet<>(this.leftChannels.get());\n        newKeys.add(channel.key());\n        if (internal) {\n            this.leftChannels.internalSet(Collections.unmodifiableSet(newKeys));\n        } else {\n            this.leftChannels.set(Collections.unmodifiableSet(newKeys));\n        }\n    }\n\n    @Override\n    public void leaveChannel(final ChatChannel channel) {\n        this.leaveChannel(channel, false);\n    }\n\n    @Override\n    public Identity identity() {\n        return Identity.identity(this.uuid);\n    }\n\n    @Override\n    public @Nullable Locale locale() {\n        return Locale.getDefault();\n    }\n\n    @Override\n    public @Nullable ChatChannel selectedChannel() {\n        final @Nullable Key selected = this.selectedChannelKey();\n        return selected == null ? null : this.channelRegistry.channel(selected);\n    }\n\n    public ChannelRegistry channelRegistry() {\n        return this.channelRegistry;\n    }\n\n    public @Nullable Key selectedChannelKey() {\n        return this.selectedChannel.orNull();\n    }\n\n    @Override\n    public void selectedChannel(final @Nullable ChatChannel chatChannel) {\n        if (chatChannel == null) {\n            this.selectedChannel.set(null);\n        } else {\n            this.selectedChannel.set(chatChannel.key());\n        }\n    }\n\n    @Override\n    public ChannelMessage channelForMessage(final Component message) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public double distanceSquaredFrom(final CarbonPlayer other) {\n        return -1;\n    }\n\n    @Override\n    public boolean sameWorldAs(final CarbonPlayer other) {\n        return false;\n    }\n\n    @Override\n    public String username() {\n        if (this.username == null) {\n            this.username = Objects.requireNonNull(\n                this.profileResolver.resolveName(this.uuid).join(),\n                () -> \"Failed to resolve username for player with UUID \" + this.uuid + \" (null result)\"\n            );\n        }\n\n        return this.username;\n    }\n\n    @Override\n    public Component displayName() {\n        throw new UnsupportedOperationException();\n    }\n\n    public void username(final String username) {\n        this.username = username;\n    }\n\n    public void markTransientLoaded(final boolean value) {\n        if (value) {\n            this.transientLoadedSince = System.currentTimeMillis();\n        } else {\n            this.transientLoadedSince = -1;\n        }\n    }\n\n    public boolean transientLoadedNeedsUnload() {\n        return this.transientLoadedSince != -1 && System.currentTimeMillis() - this.transientLoadedSince > KEEP_TRANSIENT_LOADS_FOR;\n    }\n\n    @Override\n    public boolean hasNickname() {\n        if (!this.config.primaryConfig().nickname().useCarbonNicknames()) {\n            return false;\n        }\n        return this.displayName.hasValue();\n    }\n\n    public ConfigManager configManager() {\n        return this.config;\n    }\n\n    public CarbonMessageRenderer messageRenderer() {\n        return this.messageRenderer;\n    }\n\n    public CarbonMessages carbonMessages() {\n        return this.messages;\n    }\n\n    @Override\n    public UUID uuid() {\n        return this.uuid;\n    }\n\n    @Override\n    public boolean equals(final @Nullable Object other) {\n        if (other == null || this.getClass() != other.getClass()) {\n            return false;\n        }\n\n        return this.uuid.equals(((CarbonPlayerCommon) other).uuid);\n    }\n\n    @Override\n    public int hashCode() {\n        return this.uuid.hashCode();\n    }\n\n    public void saved() {\n        this.properties().forEach(PersistentUserProperty::saved);\n    }\n\n    public @Nullable UUID partyId() {\n        return this.party.orNull();\n    }\n\n    @Override\n    public CompletableFuture<@Nullable Party> party() {\n        final @Nullable UUID id = this.party.orNull();\n        if (id == null) {\n            return CompletableFuture.completedFuture(null);\n        }\n        return this.users.party(id);\n    }\n\n    public void party(final @Nullable Party party) {\n        this.party.set(party == null ? null : party.id());\n    }\n\n    @Override\n    public boolean applyOptionalChatFilters() {\n        return this.applyOptionalChatFilters.get();\n    }\n\n    @Override\n    public void applyOptionalChatFilters(final boolean applyOptionalChatFilters) {\n        this.applyOptionalChatFilters.set(applyOptionalChatFilters);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/ConsoleCarbonPlayer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.api.util.InventorySlot;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport net.kyori.adventure.identity.Identity;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\n\n@DefaultQualifier(NonNull.class)\npublic class ConsoleCarbonPlayer implements CarbonPlayer, ForwardingAudience.Single {\n\n    private final Audience audience;\n\n    public ConsoleCarbonPlayer(final Audience audience) {\n        this.audience = audience;\n    }\n\n    @Override\n    public @NotNull Audience audience() {\n        return this.audience;\n    }\n\n    @Override\n    public double distanceSquaredFrom(final CarbonPlayer other) {\n        return 0;\n    }\n\n    @Override\n    public boolean sameWorldAs(final CarbonPlayer other) {\n        return true;\n    }\n\n    @Override\n    public String username() {\n        return \"Console\";\n    }\n\n    @Override\n    public Component displayName() {\n        return Component.text(this.username());\n    }\n\n    @Override\n    public boolean hasNickname() {\n        return false;\n    }\n\n    @Override\n    public @Nullable Component nickname() {\n        return null;\n    }\n\n    @Override\n    public void nickname(final @Nullable Component nickname) {\n\n    }\n\n    @Override\n    public UUID uuid() {\n        return new UUID(0, 0);\n    }\n\n    @Override\n    public @Nullable Component createItemHoverComponent(final InventorySlot slot) {\n        return null;\n    }\n\n    @Override\n    public @Nullable Locale locale() {\n        return null;\n    }\n\n    @Override\n    public @Nullable ChatChannel selectedChannel() {\n        return null;\n    }\n\n    @Override\n    public void selectedChannel(final @Nullable ChatChannel chatChannel) {\n\n    }\n\n    @Override\n    public boolean hasPermission(final String permission) {\n        return true;\n    }\n\n    @Override\n    public String primaryGroup() {\n        return \"console_sender\";\n    }\n\n    @Override\n    public List<String> groups() {\n        return List.of(\"console_sender\");\n    }\n\n    @Override\n    public boolean muted() {\n        return false;\n    }\n\n    @Override\n    public void muted(final boolean muted) {\n\n    }\n\n    @Override\n    public long muteExpiration() {\n        return 0;\n    }\n\n    @Override\n    public void muteExpiration(final long epochMillis) {\n\n    }\n\n    @Override\n    public Set<UUID> ignoring() {\n        return Collections.emptySet();\n    }\n\n    @Override\n    public boolean ignoring(final UUID player) {\n        return false;\n    }\n\n    @Override\n    public boolean ignoring(final CarbonPlayer player) {\n        return false;\n    }\n\n    @Override\n    public void ignoring(final UUID player, final boolean nowIgnoring) {\n\n    }\n\n    @Override\n    public void ignoring(final CarbonPlayer player, final boolean nowIgnoring) {\n\n    }\n\n    @Override\n    public boolean deafened() {\n        return false;\n    }\n\n    @Override\n    public void deafened(final boolean deafened) {\n\n    }\n\n    @Override\n    public boolean spying() {\n        return false;\n    }\n\n    @Override\n    public void spying(final boolean spying) {\n\n    }\n\n    @Override\n    public boolean ignoringDirectMessages() {\n        return false;\n    }\n\n    @Override\n    public void ignoringDirectMessages(final boolean ignoring) {\n\n    }\n\n    @Override\n    public void sendMessageAsPlayer(final String message) {\n\n    }\n\n    @Override\n    public boolean online() {\n        return true;\n    }\n\n    @Override\n    public @Nullable UUID whisperReplyTarget() {\n        return null;\n    }\n\n    @Override\n    public void whisperReplyTarget(final @Nullable UUID uuid) {\n\n    }\n\n    @Override\n    public @Nullable UUID lastWhisperTarget() {\n        return null;\n    }\n\n    @Override\n    public void lastWhisperTarget(final @Nullable UUID uuid) {\n\n    }\n\n    @Override\n    public boolean vanished() {\n        return false;\n    }\n\n    @Override\n    public boolean awareOf(final CarbonPlayer other) {\n        return true;\n    }\n\n    @Override\n    public List<Key> leftChannels() {\n        return List.of();\n    }\n\n    @Override\n    public void joinChannel(final ChatChannel channel) {\n\n    }\n\n    @Override\n    public void leaveChannel(final ChatChannel channel) {\n\n    }\n\n    @Override\n    public CompletableFuture<@Nullable Party> party() {\n        return CompletableFuture.completedFuture(null);\n    }\n\n    @Override\n    public boolean applyOptionalChatFilters() {\n        return false;\n    }\n\n    @Override\n    public void applyOptionalChatFilters(final boolean applyOptionalChatFilters) {\n\n    }\n\n    @Override\n    public @NotNull Identity identity() {\n        return Identity.nil();\n    }\n\n    @Override\n    public ChannelMessage channelForMessage(final Component message) {\n        return new ChannelMessage(message, null);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/MojangProfileResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.TypeAdapter;\nimport com.google.gson.reflect.TypeToken;\nimport com.google.gson.stream.JsonReader;\nimport com.google.gson.stream.JsonWriter;\nimport com.google.inject.Inject;\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Timer;\nimport java.util.TimerTask;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport net.draycia.carbon.common.util.ConcurrentUtil;\nimport net.draycia.carbon.common.util.FastUuidSansHyphens;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class MojangProfileResolver implements ProfileResolver {\n\n    private final HttpClient client;\n    private final Gson gson;\n    private final ExecutorService executorService;\n    private final Map<String, CompletableFuture<@Nullable BasicLookupResponse>> pendingUuidLookups = new HashMap<>();\n    private final Map<UUID, CompletableFuture<@Nullable BasicLookupResponse>> pendingUsernameLookups = new HashMap<>();\n    private final ProfileCache cache;\n    private final RateLimiter globalRateLimit;\n    private final RateLimiter uuidToProfileRateLimit;\n\n    @Inject\n    private MojangProfileResolver(final Logger logger, final ProfileCache cache) {\n        this.client = HttpClient.newHttpClient();\n        this.gson = new GsonBuilder()\n            .registerTypeAdapter(UUID.class, new UUIDTypeAdapter())\n            .create();\n        this.executorService = Executors.newFixedThreadPool(2, ConcurrentUtil.carbonThreadFactory(logger, \"MojangProfileResolver\"));\n        this.cache = cache;\n        this.globalRateLimit = new RateLimiter(600);\n        this.uuidToProfileRateLimit = new RateLimiter(200);\n    }\n\n    @Override\n    public synchronized CompletableFuture<@Nullable UUID> resolveUUID(final String username, final boolean cacheOnly) {\n        if (username.length() > 25 || username.length() < 1) { // Invalid names\n            return CompletableFuture.completedFuture(null);\n        }\n        if (cacheOnly || this.cache.hasCachedEntry(username)) {\n            return CompletableFuture.completedFuture(this.cache.cachedId(username));\n        }\n        return this.pendingUuidLookups.computeIfAbsent(username, $ -> {\n            if (!this.globalRateLimit.canSubmit()) {\n                return CompletableFuture.completedFuture(null);\n            }\n            final CompletableFuture<@Nullable BasicLookupResponse> mojangLookup = CompletableFuture.supplyAsync(() -> {\n                try {\n                    final HttpRequest request = createRequest(\n                        \"https://api.mojang.com/users/profiles/minecraft/\" + username);\n\n                    return this.sendRequest(request);\n                } catch (final Exception e) {\n                    throw new RuntimeException(\"Exception resolving UUID for name \" + username, e);\n                }\n            }, this.executorService);\n\n            mojangLookup.whenComplete((result, $$$) -> {\n                synchronized (this) {\n                    this.cache.cache(result == null ? null : result.id(), username);\n                    this.pendingUuidLookups.remove(username);\n                }\n            });\n\n            return mojangLookup;\n        }).thenApply(response -> {\n            if (response == null) {\n                return null;\n            }\n            return response.id();\n        });\n    }\n\n    @Override\n    public synchronized CompletableFuture<@Nullable String> resolveName(final UUID uuid, final boolean cacheOnly) {\n        if (cacheOnly || this.cache.hasCachedEntry(uuid)) {\n            return CompletableFuture.completedFuture(this.cache.cachedName(uuid));\n        }\n        return this.pendingUsernameLookups.computeIfAbsent(uuid, $ -> {\n            final boolean globalLimited = !this.globalRateLimit.canSubmit();\n            final boolean nameLimited = !this.uuidToProfileRateLimit.canSubmit();\n            if (globalLimited || nameLimited) {\n                if (nameLimited && !globalLimited) {\n                    // Add back to the global limit if we didn't actually make a request due to uuidToProfileRateLimit\n                    this.globalRateLimit.available.getAndIncrement();\n                }\n                return CompletableFuture.completedFuture(null);\n            }\n            final CompletableFuture<@Nullable BasicLookupResponse> mojangLookup = CompletableFuture.supplyAsync(() -> {\n                try {\n                    final HttpRequest request = createRequest(\n                        \"https://api.mojang.com/user/profile/\" + uuid.toString().replace(\"-\", \"\"));\n\n                    return this.sendRequest(request);\n                } catch (final Exception e) {\n                    throw new RuntimeException(\"Exception resolving name for UUID \" + uuid, e);\n                }\n            }, this.executorService);\n\n            mojangLookup.whenComplete((result, $$$) -> {\n                synchronized (this) {\n                    this.cache.cache(uuid, result == null ? null : result.name());\n                    this.pendingUsernameLookups.remove(uuid);\n                }\n            });\n\n            return mojangLookup;\n        }).thenApply(response -> {\n            if (response == null) {\n                return null;\n            }\n            return response.name();\n        });\n    }\n\n    private static HttpRequest createRequest(final String uri) throws URISyntaxException {\n        return HttpRequest.newBuilder()\n            .uri(new URI(uri))\n            .GET()\n            .build();\n    }\n\n    private @Nullable BasicLookupResponse sendRequest(final HttpRequest request) throws IOException, InterruptedException {\n        final HttpResponse<String> response = this.client.send(request, HttpResponse.BodyHandlers.ofString());\n\n        if (response == null) {\n            throw new RuntimeException(\"Null response for request \" + request);\n        } else if (response.statusCode() == 429) {\n            throw new RuntimeException(\"Got rate-limited by Mojang, could not fulfill request: \" + request);\n        } else if (response.statusCode() == 404) {\n            // No such profile\n            return null;\n        } else if (response.statusCode() == 400) {\n            // Invalid name/UUID\n            return null;\n        } else if (response.statusCode() != 200) {\n            throw new RuntimeException(\"Received non-200 response code (\" + response.statusCode() + \") for request \" + request + \": \" + response.body());\n        }\n\n        final BasicLookupResponse basicLookupResponse = this.gson.fromJson(response.body(), new TypeToken<BasicLookupResponse>() {}.getType());\n        if (basicLookupResponse == null) {\n            throw new RuntimeException(\"Malformed response body for request \" + request + \": '\" + response.body() + \"'\");\n        }\n        return basicLookupResponse;\n    }\n\n    @Override\n    public void shutdown() {\n        ConcurrentUtil.shutdownExecutor(this.executorService, TimeUnit.MILLISECONDS, 500);\n        this.globalRateLimit.shutdown();\n        this.uuidToProfileRateLimit.shutdown();\n    }\n\n    private record BasicLookupResponse(UUID id, String name) {\n\n    }\n\n    private static final class UUIDTypeAdapter extends TypeAdapter<UUID> {\n\n        private UUIDTypeAdapter() {\n        }\n\n        @Override\n        public void write(final JsonWriter out, final UUID value) throws IOException {\n            out.value(FastUuidSansHyphens.toString(value));\n        }\n\n        @Override\n        public UUID read(final JsonReader in) throws IOException {\n            final String input = in.nextString();\n            return FastUuidSansHyphens.parseUuid(input);\n        }\n\n    }\n\n    private static final class RateLimiter {\n\n        private final AtomicInteger available;\n        private final Timer timer;\n\n        private RateLimiter(final int perTenMinutes) {\n            this.timer = new Timer(\"CarbonChat \" + this);\n            this.available = new AtomicInteger(perTenMinutes);\n            this.timer.scheduleAtFixedRate(new TimerTask() {\n                @Override\n                public void run() {\n                    RateLimiter.this.available.set(perTenMinutes);\n                }\n            }, 0L, Duration.ofMinutes(10).toMillis());\n        }\n\n        boolean canSubmit() {\n            return this.available.getAndDecrement() >= 0;\n        }\n\n        void shutdown() {\n            this.timer.cancel();\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/NetworkUsers.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.stream.Stream;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.command.argument.PlayerSuggestions;\nimport net.draycia.carbon.common.messaging.packets.LocalPlayerChangePacket;\nimport net.draycia.carbon.common.messaging.packets.LocalPlayersPacket;\nimport net.draycia.carbon.common.util.Exceptions;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.context.CommandContext;\nimport org.incendo.cloud.context.CommandInput;\nimport org.incendo.cloud.suggestion.Suggestion;\n\n/**\n * Eventually consistent store of who is on each server in the network (besides self).\n *\n * <p>Currently used for username suggestions and whispers.</p>\n */\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic final class NetworkUsers implements PlayerSuggestions {\n\n    private final CarbonServer server;\n    private final Map<UUID, Map<UUID, String>> map = new ConcurrentHashMap<>();\n    private final UserManager<? extends CarbonPlayer> userManager;\n    private final ProfileCache profileCache;\n\n    @Inject\n    private NetworkUsers(\n        final CarbonServer server,\n        final UserManager<?> userManager,\n        final ProfileCache profileCache\n    ) {\n        this.server = server;\n        this.userManager = userManager;\n        this.profileCache = profileCache;\n    }\n\n    public void handlePacket(final LocalPlayerChangePacket packet) {\n        final Map<UUID, String> serverMap = this.map.computeIfAbsent(packet.getSender(), $ -> new ConcurrentHashMap<>());\n\n        switch (packet.changeType()) {\n            case ADD -> {\n                serverMap.put(packet.playerId(), packet.playerName());\n                this.profileCache.cache(packet.playerId(), packet.playerName());\n            }\n            case REMOVE -> serverMap.remove(packet.playerId());\n        }\n\n        this.map.values().removeIf(Map::isEmpty);\n    }\n\n    public void handlePacket(final LocalPlayersPacket packet) {\n        if (packet.players().isEmpty()) {\n            this.map.remove(packet.getSender());\n        } else {\n            final Map<UUID, String> serverMap = this.map.computeIfAbsent(packet.getSender(), $ -> new ConcurrentHashMap<>());\n            serverMap.clear();\n            serverMap.putAll(packet.players());\n\n            packet.players().forEach(this.profileCache::cache);\n        }\n    }\n\n    // PlayerSuggestions impl\n    @Override\n    public CompletableFuture<Iterable<Suggestion>> suggestionsFuture(final CommandContext<Commander> ctx, final CommandInput input) {\n        final Commander commander = ctx.sender();\n\n        final List<? extends CarbonPlayer> local = this.server.players();\n\n        if (!(commander instanceof PlayerCommander player)) {\n            return CompletableFuture.completedFuture(\n                Stream.concat(local.stream().map(CarbonPlayer::username), this.map.values().stream().flatMap(m -> m.values().stream()))\n                    .distinct()\n                    .map(Suggestion::suggestion)\n                    .toList()\n            );\n        }\n        final CarbonPlayer carbonPlayer = player.carbonPlayer();\n\n        final List<? extends CompletableFuture<? extends CarbonPlayer>> remotePlayerFutures =\n            this.map.values().stream()\n                .flatMap(m -> m.keySet().stream())\n                .map(this.userManager::user)\n                .toList(); // collect to ensure we request all futures before waiting\n        final CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(remotePlayerFutures.toArray(CompletableFuture[]::new));\n        try {\n            combinedFuture.get(50, TimeUnit.MILLISECONDS);\n        } catch (final TimeoutException ignore) {\n        } catch (final Exception e) {\n            throw Exceptions.rethrow(e);\n        }\n        final Stream<? extends CarbonPlayer> remote = remotePlayerFutures.stream()\n            .map(future -> future.getNow(null))\n            .filter(Objects::nonNull);\n\n        return CompletableFuture.completedFuture(\n            Stream.concat(local.stream(), remote)\n                .filter(carbonPlayer::awareOf)\n                .map(CarbonPlayer::username)\n                .distinct()\n                .map(Suggestion::suggestion)\n                .toList()\n        );\n    }\n\n    public boolean online(final CarbonPlayer player) {\n        if (player.online()) {\n            return true;\n        }\n        return this.map.values().stream().anyMatch(server -> server.containsKey(player.uuid()));\n    }\n\n    public boolean online(final UUID uuid) {\n        final @Nullable CarbonPlayer player = this.server.players().stream()\n            .filter(it -> it.uuid().equals(uuid))\n            .findFirst()\n            .orElse(null);\n        return player != null || this.map.values().stream().anyMatch(server -> server.containsKey(uuid));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/PartyImpl.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport com.google.common.base.Suppliers;\nimport com.google.inject.Inject;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.function.BiConsumer;\nimport java.util.function.Supplier;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.PartyJoinEvent;\nimport net.draycia.carbon.api.event.events.PartyLeaveEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class PartyImpl implements Party {\n\n    private final Component name;\n    private final UUID id;\n    private final Set<UUID> members;\n    private transient final @Nullable String serializedName;\n    private transient volatile @MonotonicNonNull Map<UUID, ChangeType> changes;\n    private transient @MonotonicNonNull @Inject UserManagerInternal<?> userManager;\n    private transient @MonotonicNonNull @Inject CarbonServer server;\n    private transient @MonotonicNonNull @Inject Logger logger;\n    private transient @MonotonicNonNull @Inject CarbonEventHandler events;\n    private transient @MonotonicNonNull @Inject CarbonMessages messages;\n    private transient volatile boolean disbanded = false;\n\n    private PartyImpl(\n        final Component name,\n        final UUID id\n    ) {\n        this.serializedName = GsonComponentSerializer.gson().serialize(name);\n        if (this.serializedName.toCharArray().length > 8192) {\n            throw new IllegalArgumentException(\"Serialized party name is too long: '%s', %s > 8192\".formatted(name, this.serializedName.toCharArray().length));\n        }\n        this.name = name;\n        this.id = id;\n        this.members = ConcurrentHashMap.newKeySet();\n        this.changes = new ConcurrentHashMap<>();\n    }\n\n    public static PartyImpl create(final Component name) {\n        return create(name, UUID.randomUUID());\n    }\n\n    public static PartyImpl create(final Component name, final UUID id) {\n        return new PartyImpl(name, id);\n    }\n\n    private Map<UUID, ChangeType> changes() {\n        if (this.changes == null) {\n            synchronized (this) {\n                if (this.changes == null) {\n                    this.changes = new ConcurrentHashMap<>();\n                }\n            }\n        }\n        return this.changes;\n    }\n\n    @Override\n    public void addMember(final UUID id) {\n        if (this.disbanded) {\n            throw new IllegalStateException(\"This party was disbanded.\");\n        }\n        this.changes().put(id, ChangeType.ADD);\n        this.addMemberRaw(id);\n        final BiConsumer<Void, @Nullable Throwable> exceptionHandler = ($, thr) -> {\n            if (thr != null) {\n                this.logger.warn(\"Exception adding member {} to group {}\", id, this.id(), thr);\n            }\n        };\n        this.userManager.saveParty(this).whenComplete(exceptionHandler);\n        this.userManager.user(id).thenCompose(user -> {\n            final WrappedCarbonPlayer wrapped = (WrappedCarbonPlayer) user;\n            final @Nullable UUID oldPartyId = wrapped.partyId();\n            wrapped.party(this);\n            if (oldPartyId != null) {\n                return this.userManager.party(oldPartyId).thenAccept(old -> {\n                    if (old != null) {\n                        old.removeMember(user.uuid());\n                    }\n                });\n            }\n            return CompletableFuture.completedFuture(null);\n        }).whenComplete(exceptionHandler);\n    }\n\n    @Override\n    public void removeMember(final UUID id) {\n        if (this.disbanded) {\n            throw new IllegalStateException(\"This party was disbanded.\");\n        }\n        this.changes().put(id, ChangeType.REMOVE);\n        this.removeMemberRaw(id);\n        final BiConsumer<Void, @Nullable Throwable> exceptionHandler = ($, thr) -> {\n            if (thr != null) {\n                this.logger.warn(\"Exception removing member {} from group {}\", id, this.id(), thr);\n            }\n        };\n        this.userManager.saveParty(this).whenComplete(exceptionHandler);\n        this.userManager.user(id).thenAccept(user -> {\n            final WrappedCarbonPlayer wrapped = (WrappedCarbonPlayer) user;\n            if (Objects.equals(wrapped.partyId(), this.id)) {\n                wrapped.party(null);\n            }\n        }).whenComplete(exceptionHandler);\n    }\n\n    @Override\n    public Set<UUID> members() {\n        if (this.disbanded) {\n            throw new IllegalStateException(\"This party was disbanded.\");\n        }\n        return Set.copyOf(this.members);\n    }\n\n    @Override\n    public void disband() {\n        if (this.disbanded) {\n            throw new IllegalStateException(\"This party is already disbanded.\");\n        }\n        this.disbandRaw();\n        this.userManager.disbandParty(this.id);\n    }\n\n    public void disbandRaw() {\n        this.disbanded = true;\n        this.server.players().stream().filter(p -> this.members.contains(p.uuid())).forEach(p -> ((WrappedCarbonPlayer) p).party(null));\n        for (final UUID member : this.members) {\n            this.emitLeaveEvent(member);\n        }\n    }\n\n    public Set<UUID> rawMembers() {\n        return this.members;\n    }\n\n    public void addMemberRaw(final UUID id) {\n        this.members.add(id);\n\n        this.events.emit(new PartyJoinEvent() {\n\n            @Override\n            public UUID playerId() {\n                return id;\n            }\n\n            @Override\n            public Party party() {\n                return PartyImpl.this;\n            }\n        });\n\n        this.notifyJoin(id);\n    }\n\n    public void removeMemberRaw(final UUID id) {\n        this.members.remove(id);\n\n        this.emitLeaveEvent(id);\n\n        this.notifyLeave(id);\n    }\n\n    private void emitLeaveEvent(final UUID id) {\n        this.events.emit(new PartyLeaveEvent() {\n\n            @Override\n            public UUID playerId() {\n                return id;\n            }\n\n            @Override\n            public Party party() {\n                return PartyImpl.this;\n            }\n        });\n    }\n\n    public Map<UUID, ChangeType> pollChanges() {\n        final Map<UUID, ChangeType> ret = Map.copyOf(this.changes());\n        ret.forEach((id, t) -> this.changes().remove(id));\n        return ret;\n    }\n\n    @Override\n    public Component name() {\n        return this.name;\n    }\n\n    public String serializedName() {\n        return Objects.requireNonNullElseGet(this.serializedName, () -> GsonComponentSerializer.gson().serialize(this.name));\n    }\n\n    @Override\n    public UUID id() {\n        return this.id;\n    }\n\n    private void notifyJoin(final UUID joined) {\n        this.notifyMembersChanged(joined, (p, party, member) -> {\n            this.messages.playerJoinedParty(member, party.name(), p.displayName());\n        });\n    }\n\n    private void notifyLeave(final UUID left) {\n        this.notifyMembersChanged(left, (p, party, member) -> {\n            this.messages.playerLeftParty(member, party.name(), p.displayName());\n        });\n    }\n\n    private void notifyMembersChanged(final UUID changed, final ChangeNotifier notify) {\n        final Supplier<CompletableFuture<? extends CarbonPlayer>> changedPlayer = Suppliers.memoize(() -> this.userManager.user(changed));\n        for (final CarbonPlayer player : this.server.players()) {\n            if (player.uuid().equals(changed)) {\n                continue;\n            }\n            if (this.members.contains(player.uuid())) {\n                changedPlayer.get().thenAccept(p -> {\n                    notify.notify(p, this, player);\n                }).whenComplete(($, thr) -> {\n                    if (thr != null) {\n                        this.logger.warn(\"Exception notifying members of party change\", thr);\n                    }\n                });\n            }\n        }\n    }\n\n    @FunctionalInterface\n    private interface ChangeNotifier {\n\n        void notify(CarbonPlayer changed, Party party, CarbonPlayer member);\n\n    }\n\n    @Override\n    public String toString() {\n        return \"PartyImpl[\" +\n            \"name=\" + this.name + \", \" +\n            \"id=\" + this.id + \", \" +\n            \"members=\" + this.members + \", \" +\n            \"changes=\" + this.changes + ']';\n    }\n\n    public enum ChangeType {\n        ADD, REMOVE\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/PartyInvites.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport com.google.inject.Inject;\nimport com.google.inject.Provider;\nimport com.google.inject.Singleton;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ConcurrentHashMap;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.messaging.packets.InvalidatePartyInvitePacket;\nimport net.draycia.carbon.common.messaging.packets.PacketFactory;\nimport net.draycia.carbon.common.messaging.packets.PartyInvitePacket;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic final class PartyInvites {\n\n    private final Map<UUID, Cache<UUID, UUID>> pendingInvites = new ConcurrentHashMap<>();\n    private final Provider<MessagingManager> messaging;\n    private final PacketFactory packetFactory;\n    private final UserManagerInternal<?> users;\n    private final Logger logger;\n    private final CarbonMessages messages;\n    private final ConfigManager config;\n\n    @Inject\n    private PartyInvites(\n        final Provider<MessagingManager> messaging,\n        final PacketFactory packetFactory,\n        final UserManagerInternal<?> users,\n        final Logger logger,\n        final CarbonMessages messages,\n        final ConfigManager config\n    ) {\n        this.messaging = messaging;\n        this.packetFactory = packetFactory;\n        this.users = users;\n        this.logger = logger;\n        this.messages = messages;\n        this.config = config;\n    }\n\n    public void sendInvite(final UUID from, final UUID to, final UUID party) {\n        final Cache<UUID, UUID> cache = this.orCreateInvitesFor(to);\n        cache.put(from, party);\n        this.clean();\n\n        this.messaging.get().queuePacket(() -> this.packetFactory.partyInvite(from, to, party));\n    }\n\n    public void invalidateInvite(final UUID from, final UUID to) {\n        this.invalidateInvite_(from, to);\n\n        this.messaging.get().queuePacket(() -> this.packetFactory.invalidatePartyInvite(from, to));\n    }\n\n    private void invalidateInvite_(final UUID from, final UUID to) {\n        final @Nullable Cache<UUID, UUID> cache = this.invitesFor(to);\n        if (cache != null) {\n            cache.invalidate(from);\n        }\n        this.clean();\n    }\n\n    public @Nullable Cache<UUID, UUID> invitesFor(final UUID recipient) {\n        return this.pendingInvites.get(recipient);\n    }\n\n    private Cache<UUID, UUID> orCreateInvitesFor(final UUID recipient) {\n        return this.pendingInvites.computeIfAbsent(recipient, $ -> this.makeCache());\n    }\n\n    private Cache<UUID, UUID> makeCache() {\n        return Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(this.config.primaryConfig().partyChat().expireInvitesAfterSeconds)).build();\n    }\n\n    public void handle(final InvalidatePartyInvitePacket pkt) {\n        this.invalidateInvite_(pkt.from(), pkt.to());\n        this.clean();\n    }\n\n    private void clean() {\n        this.pendingInvites.values().removeIf(it -> it.asMap().size() == 0);\n    }\n\n    public void handle(final PartyInvitePacket pkt) {\n        final @Nullable Cache<UUID, UUID> cache = this.orCreateInvitesFor(pkt.to());\n        cache.put(pkt.from(), pkt.party());\n        this.clean();\n\n        final CompletableFuture<? extends CarbonPlayer> to = this.users.user(pkt.to());\n        final CompletableFuture<? extends CarbonPlayer> from = this.users.user(pkt.to());\n        final CompletableFuture<Party> party = this.users.party(pkt.party());\n\n        CompletableFuture.allOf(to, from, party).thenRun(() -> {\n            if (to.join().online()) {\n                this.messages.receivedPartyInvite(to.join(), from.join().displayName(), from.join().username(), party.join().name());\n            }\n        }).whenComplete(($, thr) -> {\n            if (thr != null) {\n                this.logger.warn(\"Exception handling {}\", pkt, thr);\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/PersistentUserProperty.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport com.google.gson.JsonDeserializationContext;\nimport com.google.gson.JsonDeserializer;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonParseException;\nimport com.google.gson.JsonSerializationContext;\nimport com.google.gson.JsonSerializer;\nimport java.lang.reflect.ParameterizedType;\nimport java.lang.reflect.Type;\nimport java.util.List;\nimport java.util.Objects;\nimport java.util.concurrent.CopyOnWriteArrayList;\nimport java.util.concurrent.atomic.AtomicReference;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.ApiStatus;\n\n@DefaultQualifier(NonNull.class)\npublic final class PersistentUserProperty<T> {\n\n    private final AtomicReference<@Nullable T> valueReference;\n    private final List<Runnable> updateListeners = new CopyOnWriteArrayList<>();\n    private volatile boolean changed = false;\n\n    public PersistentUserProperty(final @Nullable T value) {\n        this.valueReference = new AtomicReference<>(value);\n    }\n\n    /**\n     * Set the value without setting the changed flag. This is a hack.\n     *\n     * @param value value\n     */\n    @ApiStatus.Internal\n    public void internalSet(final @Nullable T value) {\n        this.valueReference.set(value);\n    }\n\n    public void set(final @Nullable T value) {\n        final @Nullable T old = this.valueReference.getAndSet(value);\n        if (Objects.equals(value, old)) {\n            return;\n        }\n        this.changed = true;\n        for (final Runnable updateListener : this.updateListeners) {\n            updateListener.run();\n        }\n    }\n\n    public void saved() {\n        this.changed = false;\n    }\n\n    public void registerUpdateListener(final Runnable runnable) {\n        this.updateListeners.add(runnable);\n    }\n\n    public T get() {\n        return Objects.requireNonNull(this.valueReference.get(), \"value required but not present\");\n    }\n\n    public boolean hasValue() {\n        return this.valueReference.get() != null;\n    }\n\n    public @Nullable T orNull() {\n        return this.valueReference.get();\n    }\n\n    public boolean changed() {\n        return this.changed;\n    }\n\n    public static <T> PersistentUserProperty<T> of(final @Nullable T value) {\n        return new PersistentUserProperty<>(value);\n    }\n\n    public static <T> PersistentUserProperty<T> empty() {\n        return new PersistentUserProperty<>(null);\n    }\n\n    public static final class Serializer implements JsonSerializer<PersistentUserProperty<?>>, JsonDeserializer<PersistentUserProperty<?>> {\n\n        @Override\n        public PersistentUserProperty<?> deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {\n            final Type propType = ((ParameterizedType) typeOfT).getActualTypeArguments()[0];\n            return new PersistentUserProperty<>(context.deserialize(json, propType));\n        }\n\n        @Override\n        public JsonElement serialize(final PersistentUserProperty<?> src, final Type typeOfSrc, final JsonSerializationContext context) {\n            final Type propType = ((ParameterizedType) typeOfSrc).getActualTypeArguments()[0];\n            return context.serialize(src.orNull(), propType);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/PlatformUserManager.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.google.inject.Module;\nimport com.google.inject.Singleton;\nimport com.google.inject.assistedinject.FactoryModuleBuilder;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.common.messaging.packets.DisbandPartyPacket;\nimport net.draycia.carbon.common.messaging.packets.PartyChangePacket;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic final class PlatformUserManager implements UserManagerInternal<WrappedCarbonPlayer> {\n\n    private final UserManagerInternal<CarbonPlayerCommon> backingManager;\n    private final PlayerFactory playerFactory;\n    private final Injector injector;\n\n    @Inject\n    private PlatformUserManager(\n        final @Backing UserManagerInternal<CarbonPlayerCommon> backingManager,\n        final PlayerFactory playerFactory,\n        final Injector injector\n    ) {\n        this.backingManager = backingManager;\n        this.playerFactory = playerFactory;\n        this.injector = injector;\n    }\n\n    @Override\n    public CompletableFuture<WrappedCarbonPlayer> user(final UUID uuid) {\n        return this.backingManager.user(uuid).thenApply(common -> {\n            final WrappedCarbonPlayer wrapped = this.playerFactory.wrap(common);\n            common.markTransientLoaded(!wrapped.online());\n            return wrapped;\n        });\n    }\n\n    @Override\n    public Party createParty(final Component name) {\n        final PartyImpl party = PartyImpl.create(name);\n        this.injector.injectMembers(party);\n        return party;\n    }\n\n    @Override\n    public void shutdown() {\n        this.backingManager.shutdown();\n    }\n\n    @Override\n    public void saveCompleteMessageReceived(final UUID playerId) {\n        this.backingManager.saveCompleteMessageReceived(playerId);\n    }\n\n    @Override\n    public CompletableFuture<Void> saveIfNeeded(final WrappedCarbonPlayer player) {\n        return this.backingManager.saveIfNeeded(player.carbonPlayerCommon());\n    }\n\n    @Override\n    public CompletableFuture<Void> loggedOut(final UUID uuid) {\n        return this.backingManager.loggedOut(uuid);\n    }\n\n    @Override\n    public void cleanup() {\n        this.backingManager.cleanup();\n    }\n\n    @Override\n    public CompletableFuture<@Nullable Party> party(final UUID id) {\n        return this.backingManager.party(id);\n    }\n\n    @Override\n    public CompletableFuture<Void> saveParty(final PartyImpl info) {\n        return this.backingManager.saveParty(info);\n    }\n\n    @Override\n    public void disbandParty(final UUID id) {\n        this.backingManager.disbandParty(id);\n    }\n\n    @Override\n    public void partyChangeMessageReceived(final PartyChangePacket pkt) {\n        this.backingManager.partyChangeMessageReceived(pkt);\n    }\n\n    @Override\n    public void disbandPartyMessageReceived(final DisbandPartyPacket pkt) {\n        this.backingManager.disbandPartyMessageReceived(pkt);\n    }\n\n    public interface PlayerFactory {\n\n        WrappedCarbonPlayer wrap(CarbonPlayerCommon common);\n\n        static Module moduleFor(final Class<? extends WrappedCarbonPlayer> carbonPlayerImpl) {\n            return new FactoryModuleBuilder()\n                .implement(WrappedCarbonPlayer.class, carbonPlayerImpl)\n                .build(PlatformUserManager.PlayerFactory.class);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/PlayerUtils.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.function.Function;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class PlayerUtils {\n\n    private PlayerUtils() {\n    }\n\n    // return value is mostly useful to check if saves are still running; exceptions are already handled on returned futures\n    @SuppressWarnings(\"unchecked\")\n    public static <C extends CarbonPlayer> List<CompletableFuture<Void>> saveLoggedInPlayers(\n        final CarbonServer carbonServer,\n        final UserManagerInternal<C> userManager,\n        final Logger logger\n    ) {\n        return carbonServer.players().stream()\n            .map(player -> PlayerUtils.savePlayer(userManager, (C) player, logger))\n            .toList();\n    }\n\n    private static <C extends CarbonPlayer> CompletableFuture<Void> savePlayer(\n        final UserManagerInternal<C> userManager,\n        final C player,\n        final Logger logger\n    ) {\n        final var saveResult = userManager.saveIfNeeded(player);\n\n        // avoid fetching the username if it wasn't populated yet; a bit ugly but works (since userManager is always UserManagerInternal<WrappedCarbonPlayer>)\n        final @Nullable CarbonPlayerCommon common = player instanceof WrappedCarbonPlayer wrapped ? wrapped.carbonPlayerCommon() : null;\n        if (common == null) {\n            throw new IllegalStateException(\"Failed to unwrap \" + CarbonPlayerCommon.class.getSimpleName() + \" from \" + player.getClass());\n        }\n        final @Nullable String username = common.username;\n\n        return saveResult.exceptionally(saveExceptionHandler(logger, username, player.uuid()));\n    }\n\n    public static <T> Function<Throwable, @Nullable T> joinExceptionHandler(final Logger logger, final String username, final UUID uuid) {\n        return thr -> {\n            logger.warn(\"Exception handling join for player uuid='{}', username='{}'\", uuid, username(username), thr);\n            return null;\n        };\n    }\n\n    public static Function<Throwable, @Nullable Void> saveExceptionHandler(final Logger logger, final @Nullable String username, final UUID uuid) {\n        return thr -> {\n            logger.warn(\"Exception saving data for player uuid='{}', username='{}'\", uuid, username(username), thr);\n            return null;\n        };\n    }\n\n    private static String username(final @Nullable String username) {\n        return username == null ? \"<unresolved>\" : username;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/ProfileCache.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.reflect.TypeToken;\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.io.BufferedReader;\nimport java.io.BufferedWriter;\nimport java.io.IOException;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Duration;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.UUID;\nimport net.draycia.carbon.common.DataDirectory;\nimport net.draycia.carbon.common.serialisation.gson.UUIDSerializerGson;\nimport net.draycia.carbon.common.util.FileUtil;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.loader.AtomicFiles;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic final class ProfileCache {\n\n    private static final long REMOVE_AFTER = Duration.ofDays(7).toMillis();\n    private static final long REMOVE_NULL_IDS_AFTER = Duration.ofHours(1).toMillis();\n\n    private final Gson gson;\n    private final Path cacheFile;\n    private final Map<UUID, CacheEntry> byId;\n    private final Map<String, CacheEntry> byName;\n    private final Set<CacheEntry> entries;\n\n    private record CacheEntry(@Nullable UUID uuid, @Nullable String name, long updated) {\n\n    }\n\n    @Inject\n    private ProfileCache(final @DataDirectory Path dataDirectory) {\n        this.gson = new GsonBuilder()\n            .registerTypeAdapter(UUID.class, new UUIDSerializerGson())\n            .create();\n        this.cacheFile = dataDirectory.resolve(\"users/profile_cache.json\");\n        this.byId = new HashMap<>();\n        this.byName = new HashMap<>();\n        this.entries = new HashSet<>();\n        this.load();\n    }\n\n    public synchronized @Nullable String cachedName(final UUID id) {\n        final @Nullable CacheEntry entry = this.byId.get(id);\n        if (entry == null) {\n            return null;\n        } else if (entry.updated() < cutoff()) {\n            return null;\n        }\n        return entry.name();\n    }\n\n    public synchronized @Nullable UUID cachedId(final String name) {\n        final @Nullable CacheEntry entry = this.byName.get(name);\n        if (entry == null) {\n            return null;\n        } else if (entry.updated() < cutoff()) {\n            return null;\n        }\n        return entry.uuid();\n    }\n\n    public synchronized boolean hasCachedEntry(final String name) {\n        final @Nullable CacheEntry entry = this.byName.get(name);\n        if (entry == null) {\n            return false;\n        }\n        return entry.updated() >= cutoff();\n    }\n\n    public synchronized boolean hasCachedEntry(final UUID uuid) {\n        final @Nullable CacheEntry entry = this.byId.get(uuid);\n        if (entry == null) {\n            return false;\n        }\n        return entry.updated() >= cutoff();\n    }\n\n    public synchronized void cache(final @Nullable UUID uuid, final @Nullable String name) {\n        final @Nullable CacheEntry r1 = uuid == null ? null : this.byId.remove(uuid);\n        final @Nullable CacheEntry r2 = name == null ? null : this.byName.remove(name);\n        if (r1 != null) {\n            this.entries.remove(r1);\n        }\n        if (r2 != null) {\n            this.entries.remove(r2);\n        }\n        final CacheEntry entry = new CacheEntry(uuid, name, System.currentTimeMillis());\n        this.entries.add(entry);\n        if (entry.name() != null) {\n            this.byName.put(entry.name(), entry);\n        }\n        if (entry.uuid() != null) {\n            this.byId.put(entry.uuid(), entry);\n        }\n    }\n\n    private synchronized void cleanup() {\n        final long cutoff = cutoff();\n        final long nullIdCutoff = nullIdCutoff();\n        for (final Iterator<CacheEntry> iterator = this.entries.iterator(); iterator.hasNext();) {\n            final CacheEntry entry = iterator.next();\n            if (entry.updated() < cutoff || entry.uuid() == null && entry.updated() < nullIdCutoff) {\n                iterator.remove();\n                if (entry.uuid() != null) {\n                    this.byId.remove(entry.uuid());\n                }\n                if (entry.name() != null) {\n                    this.byName.remove(entry.name());\n                }\n            }\n        }\n    }\n\n    private static long nullIdCutoff() {\n        return System.currentTimeMillis() - REMOVE_NULL_IDS_AFTER;\n    }\n\n    private static long cutoff() {\n        return System.currentTimeMillis() - REMOVE_AFTER;\n    }\n\n    private synchronized void load() {\n        this.entries.clear();\n        this.byId.clear();\n        this.byName.clear();\n        if (!Files.exists(this.cacheFile)) {\n            return;\n        }\n        try {\n            try (final BufferedReader reader = Files.newBufferedReader(this.cacheFile)) {\n                final Set<CacheEntry> load = this.gson.fromJson(reader, new TypeToken<Set<CacheEntry>>() {}.getType());\n                this.entries.addAll(load);\n                for (final CacheEntry entry : this.entries) {\n                    if (entry.name() != null) {\n                        this.byName.put(entry.name(), entry);\n                    }\n                    if (entry.uuid() != null) {\n                        this.byId.put(entry.uuid(), entry);\n                    }\n                }\n            }\n        } catch (final IOException ex) {\n            throw new RuntimeException(\"Failed to load cache\", ex);\n        }\n    }\n\n    public synchronized void save() {\n        this.cleanup();\n        try (final BufferedWriter writer = AtomicFiles.atomicBufferedWriter(FileUtil.mkParentDirs(this.cacheFile), StandardCharsets.UTF_8)) {\n            this.gson.toJson(this.entries, writer);\n        } catch (final IOException ex) {\n            throw new RuntimeException(\"Failed to save cache\", ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/ProfileResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic interface ProfileResolver {\n\n    CompletableFuture<@Nullable UUID> resolveUUID(String username, boolean cacheOnly);\n\n    default CompletableFuture<@Nullable UUID> resolveUUID(final String username) {\n        return this.resolveUUID(username, false);\n    }\n\n    CompletableFuture<@Nullable String> resolveName(UUID uuid, boolean cacheOnly);\n\n    default CompletableFuture<@Nullable String> resolveName(final UUID uuid) {\n        return this.resolveName(uuid, false);\n    }\n\n    void shutdown();\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/UserManagerInternal.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.messaging.packets.DisbandPartyPacket;\nimport net.draycia.carbon.common.messaging.packets.PartyChangePacket;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic interface UserManagerInternal<C extends CarbonPlayer> extends UserManager<C> {\n\n    void shutdown();\n\n    CompletableFuture<Void> saveIfNeeded(C player);\n\n    CompletableFuture<Void> loggedOut(UUID uuid);\n\n    void saveCompleteMessageReceived(UUID playerId);\n\n    void cleanup();\n\n    CompletableFuture<Void> saveParty(PartyImpl info);\n\n    void disbandParty(UUID id);\n\n    void partyChangeMessageReceived(PartyChangePacket pkt);\n\n    void disbandPartyMessageReceived(DisbandPartyPacket pkt);\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/WrappedCarbonPlayer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users;\n\nimport io.github.miniplaceholders.api.MiniPlaceholders;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.Optional;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.api.util.InventorySlot;\nimport net.draycia.carbon.common.config.PrimaryConfig;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil;\nimport net.draycia.carbon.common.messages.SourcedAudience;\nimport net.draycia.carbon.common.messages.TagPermissions;\nimport net.kyori.adventure.identity.Identity;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.TextReplacementConfig;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;\nimport net.luckperms.api.LuckPermsProvider;\nimport net.luckperms.api.model.user.User;\nimport net.luckperms.api.util.Tristate;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\n\nimport static java.util.Objects.requireNonNullElse;\n\n@DefaultQualifier(NonNull.class)\npublic abstract class WrappedCarbonPlayer implements CarbonPlayer {\n\n    protected final CarbonPlayerCommon carbonPlayerCommon;\n\n    protected WrappedCarbonPlayer(final CarbonPlayerCommon carbonPlayerCommon) {\n        this.carbonPlayerCommon = carbonPlayerCommon;\n    }\n\n    public CarbonPlayerCommon carbonPlayerCommon() {\n        return this.carbonPlayerCommon;\n    }\n\n    public @Nullable User user() {\n        return LuckPermsProvider.get().getUserManager().getUser(this.uuid());\n    }\n\n    public Component parseMessageTags(final String message) {\n        final TagResolver.Builder resolver = TagResolver.builder();\n\n        if (MiniPlaceholdersUtil.miniPlaceholdersLoaded() && this.hasPermission(\"carbon.chatplaceholders\")) {\n            resolver.resolver(MiniPlaceholders.globalPlaceholders());\n            resolver.resolver(MiniPlaceholders.audiencePlaceholders());\n        }\n\n        return TagPermissions.parseTags(this, TagPermissions.MESSAGE, message, this::hasPermission, resolver);\n    }\n\n    @Override\n    public boolean awareOf(final CarbonPlayer other) {\n        if (other.vanished()) {\n            return this.hasPermission(\"carbon.whisper.vanished\");\n        }\n\n        return true;\n    }\n\n    @Override\n    public Set<UUID> ignoring() {\n        return this.carbonPlayerCommon.ignoring();\n    }\n\n    @Override\n    public boolean ignoring(final UUID player) {\n        return this.carbonPlayerCommon.ignoring(player);\n    }\n\n    @Override\n    public boolean ignoring(final CarbonPlayer player) {\n        return this.carbonPlayerCommon.ignoring(player);\n    }\n\n    @Override\n    public void ignoring(final UUID player, final boolean nowIgnoring) {\n        this.carbonPlayerCommon.ignoring(player, nowIgnoring);\n    }\n\n    @Override\n    public void ignoring(final CarbonPlayer player, final boolean nowIgnoring) {\n        this.carbonPlayerCommon.ignoring(player, nowIgnoring);\n    }\n\n    @Override\n    public boolean ignoringDirectMessages() {\n        return this.carbonPlayerCommon.ignoringDirectMessages();\n    }\n\n    @Override\n    public void ignoringDirectMessages(final boolean ignoring) {\n        this.carbonPlayerCommon.ignoringDirectMessages(ignoring);\n    }\n\n    @Override\n    public boolean hasPermission(final String permission) {\n        final @Nullable User user = this.user();\n\n        if (user == null) {\n            return false;\n        }\n\n        final var data = user.getCachedData().getPermissionData(user.getQueryOptions());\n        return data.checkPermission(permission) == Tristate.TRUE;\n    }\n\n    @Override\n    public String primaryGroup() {\n        final @Nullable User user = this.user();\n\n        if (user == null) {\n            return \"default\";\n        }\n\n        return user.getPrimaryGroup();\n    }\n\n    @Override\n    public List<String> groups() {\n        final @Nullable User user = this.user();\n\n        if (user == null) {\n            return List.of(\"default\");\n        }\n\n        final var groups = new ArrayList<String>();\n\n        for (final var group : user.getInheritedGroups(user.getQueryOptions())) {\n            groups.add(group.getName());\n        }\n\n        return groups;\n    }\n\n    @Override\n    public String username() {\n        return this.carbonPlayerCommon.username();\n    }\n\n    // take care not to call get(Identity.DISPLAY_NAME) on a CarbonPlayer\n    // from this method - it would result in a stack overflow when pointers\n    // are retrieved from EmptyAudienceWithPointers\n    @Override\n    public Component displayName() {\n        final @Nullable Component nick = this.nickname();\n        if (nick != null) {\n            final PrimaryConfig.NicknameSettings nicknames = this.carbonPlayerCommon.configManager().primaryConfig().nickname();\n\n            if (nicknames.skipFormatWhenNameMatches) {\n                final String plainNick = PlainTextComponentSerializer.plainText().serialize(nick);\n                if (plainNick.equals(this.username())) {\n                    return nick;\n                }\n            }\n\n            try {\n                return this.carbonPlayerCommon.messageRenderer().render(\n                    SourcedAudience.of(this, this),\n                    nicknames.format,\n                    Map.of(\"username\", Tag.preProcessParsed(this.username()), \"nickname\", Tag.selfClosingInserting(nick)),\n                    null,\n                    null\n                );\n            } catch (final StackOverflowError overflow) {\n                throw new RuntimeException(\"Invalid nickname format '%s'. Makes circular reference to CarbonPlayer#displayName().\".formatted(nicknames.format), overflow);\n            }\n        }\n        return this.platformDisplayName().orElseGet(() -> Component.text(this.username()));\n    }\n\n    protected abstract Optional<Component> platformDisplayName();\n\n    @Override\n    public boolean hasNickname() {\n        return this.carbonPlayerCommon.hasNickname();\n    }\n\n    @Override\n    public @Nullable Component nickname() {\n        return this.carbonPlayerCommon.nickname();\n    }\n\n    @Override\n    public void nickname(final @Nullable Component nickname) {\n        this.carbonPlayerCommon.nickname(nickname);\n    }\n\n    @Override\n    public UUID uuid() {\n        return this.carbonPlayerCommon.uuid();\n    }\n\n    @Override\n    public @Nullable Component createItemHoverComponent(final InventorySlot slot) {\n        return this.carbonPlayerCommon.createItemHoverComponent(slot);\n    }\n\n    @Override\n    public @Nullable Locale locale() {\n        return this.carbonPlayerCommon.locale();\n    }\n\n    @Override\n    public ChannelMessage channelForMessage(final Component message) {\n        final String text = PlainTextComponentSerializer.plainText().serialize(message);\n        Component formattedMessage = message;\n\n        ChatChannel channel = requireNonNullElse(this.selectedChannel(), this.carbonPlayerCommon.channelRegistry().defaultChannel());\n\n        for (final Key channelKey : this.carbonPlayerCommon.channelRegistry().keys()) {\n            final ChatChannel chatChannel = this.carbonPlayerCommon.channelRegistry().channelOrThrow(channelKey);\n            final @Nullable String prefix = chatChannel.quickPrefix();\n\n            if (prefix == null) {\n                continue;\n            }\n\n            if (text.startsWith(prefix) && chatChannel.permissions().speechPermitted(this).permitted()) {\n                channel = chatChannel;\n                formattedMessage = formattedMessage.replaceText(TextReplacementConfig.builder()\n                    .once()\n                    .matchLiteral(channel.quickPrefix())\n                    .replacement(Component.empty())\n                    .build());\n                break;\n            }\n        }\n\n        return new ChannelMessage(formattedMessage, channel);\n    }\n\n    @Override\n    public @Nullable ChatChannel selectedChannel() {\n        return this.carbonPlayerCommon.selectedChannel();\n    }\n\n    @Override\n    public void selectedChannel(final @Nullable ChatChannel chatChannel) {\n        this.carbonPlayerCommon.selectedChannel(chatChannel);\n    }\n\n    @Override\n    public boolean muted() {\n        return this.carbonPlayerCommon.muted();\n    }\n\n    @Override\n    public void muted(final boolean muted) {\n        this.carbonPlayerCommon.muted(muted);\n    }\n\n    @Override\n    public long muteExpiration() {\n        return this.carbonPlayerCommon.muteExpiration();\n    }\n\n    @Override\n    public void muteExpiration(final long epochMillis) {\n        this.carbonPlayerCommon.muteExpiration(epochMillis);\n    }\n\n    @Override\n    public boolean deafened() {\n        return this.carbonPlayerCommon.deafened();\n    }\n\n    @Override\n    public void deafened(final boolean deafened) {\n        this.carbonPlayerCommon.deafened(deafened);\n    }\n\n    @Override\n    public boolean spying() {\n        if (this.carbonPlayerCommon.spying() && this.carbonPlayerCommon.configManager().primaryConfig().spyPermissionRequired() &&\n            this.online() && !this.hasPermission(\"carbon.spy\")) {\n\n            this.spying(false);\n            if (this.carbonPlayerCommon.configManager().primaryConfig().spyDisabledMessage()) {\n                this.carbonPlayerCommon.carbonMessages().commandSpyDisabled(this);\n            }\n            return false;\n        }\n\n        return this.carbonPlayerCommon.spying();\n    }\n\n    @Override\n    public void spying(final boolean spying) {\n        this.carbonPlayerCommon.spying(spying);\n    }\n\n    @Override\n    public void sendMessageAsPlayer(final String message) {\n        this.carbonPlayerCommon.sendMessageAsPlayer(message);\n    }\n\n    @Override\n    public boolean online() {\n        return this.carbonPlayerCommon.online();\n    }\n\n    @Override\n    public @Nullable UUID whisperReplyTarget() {\n        return this.carbonPlayerCommon.whisperReplyTarget();\n    }\n\n    @Override\n    public void whisperReplyTarget(final @Nullable UUID uuid) {\n        this.carbonPlayerCommon.whisperReplyTarget(uuid);\n    }\n\n    @Override\n    public @Nullable UUID lastWhisperTarget() {\n        return this.carbonPlayerCommon.lastWhisperTarget();\n    }\n\n    @Override\n    public void lastWhisperTarget(final @Nullable UUID uuid) {\n        this.carbonPlayerCommon.lastWhisperTarget(uuid);\n    }\n\n    @Override\n    public @NotNull Identity identity() {\n        return this.carbonPlayerCommon.identity();\n    }\n\n    @Override\n    public boolean vanished() {\n        return this.carbonPlayerCommon.vanished();\n    }\n\n    @Override\n    public List<Key> leftChannels() {\n        return this.carbonPlayerCommon.leftChannels();\n    }\n\n    @Override\n    public void joinChannel(final ChatChannel channel) {\n        this.carbonPlayerCommon.joinChannel(channel);\n    }\n\n    @Override\n    public void leaveChannel(final ChatChannel channel) {\n        this.carbonPlayerCommon.leaveChannel(channel);\n    }\n\n    @Override\n    public boolean equals(final @Nullable Object other) {\n        if (other == null || this.getClass() != other.getClass()) {\n            return false;\n        }\n\n        final WrappedCarbonPlayer that = (WrappedCarbonPlayer) other;\n\n        return this.carbonPlayerCommon.equals(that.carbonPlayerCommon);\n    }\n\n    @Override\n    public int hashCode() {\n        return this.carbonPlayerCommon.hashCode();\n    }\n\n    public @Nullable UUID partyId() {\n        return this.carbonPlayerCommon.partyId();\n    }\n\n    @Override\n    public CompletableFuture<@Nullable Party> party() {\n        return this.carbonPlayerCommon.party();\n    }\n\n    public void party(final @Nullable Party party) {\n        this.carbonPlayerCommon.party(party);\n    }\n\n    @Override\n    public boolean applyOptionalChatFilters() {\n        return this.carbonPlayerCommon.applyOptionalChatFilters();\n    }\n\n    @Override\n    public void applyOptionalChatFilters(final boolean applyOptionalChatFilters) {\n        this.carbonPlayerCommon.applyOptionalChatFilters(applyOptionalChatFilters);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/DatabaseUserManager.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.google.inject.Provider;\nimport com.zaxxer.hikari.HikariConfig;\nimport com.zaxxer.hikari.HikariDataSource;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Objects;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.function.Consumer;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.config.DatabaseSettings;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.messaging.packets.PacketFactory;\nimport net.draycia.carbon.common.users.CachingUserManager;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.users.PartyImpl;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.common.users.db.argument.ComponentArgumentFactory;\nimport net.draycia.carbon.common.users.db.argument.KeyArgumentFactory;\nimport net.draycia.carbon.common.users.db.mapper.ComponentColumnMapper;\nimport net.draycia.carbon.common.users.db.mapper.KeyColumnMapper;\nimport net.draycia.carbon.common.users.db.mapper.PartyRowMapper;\nimport net.draycia.carbon.common.users.db.mapper.PlayerRowMapper;\nimport net.draycia.carbon.common.util.ConcurrentUtil;\nimport net.draycia.carbon.common.util.SQLDrivers;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.flywaydb.core.Flyway;\nimport org.flywaydb.core.api.logging.Log;\nimport org.flywaydb.core.api.logging.LogCreator;\nimport org.flywaydb.core.api.logging.LogFactory;\nimport org.jdbi.v3.core.Handle;\nimport org.jdbi.v3.core.Jdbi;\nimport org.jdbi.v3.core.statement.PreparedBatch;\nimport org.jdbi.v3.core.statement.Update;\nimport org.jdbi.v3.sqlobject.SqlObjectPlugin;\n\n@DefaultQualifier(NonNull.class)\npublic final class DatabaseUserManager extends CachingUserManager {\n\n    private final Jdbi jdbi;\n    private final QueriesLocator locator;\n    private final ChannelRegistry channelRegistry;\n    private final HikariDataSource dataSource;\n\n    private DatabaseUserManager(\n        final Jdbi jdbi,\n        final HikariDataSource dataSource,\n        final QueriesLocator locator,\n        final Logger logger,\n        final ProfileResolver profileResolver,\n        final Injector injector,\n        final Provider<MessagingManager> messagingManager,\n        final PacketFactory packetFactory,\n        final ChannelRegistry channelRegistry,\n        final CarbonServer server\n    ) {\n        super(\n            logger,\n            profileResolver,\n            injector,\n            messagingManager,\n            packetFactory,\n            server\n        );\n        this.jdbi = jdbi;\n        this.dataSource = dataSource;\n        this.locator = locator;\n        this.channelRegistry = channelRegistry;\n    }\n\n    @Override\n    public CarbonPlayerCommon loadOrCreate(final UUID uuid) {\n        return this.jdbi.withHandle(handle -> {\n            final @Nullable CarbonPlayerCommon carbonPlayerCommon = handle.createQuery(this.locator.query(\"select-player\"))\n                .bind(\"id\", uuid)\n                .mapTo(CarbonPlayerCommon.class)\n                .findOne()\n                .orElse(null);\n            if (carbonPlayerCommon == null) {\n                return new CarbonPlayerCommon(null, uuid);\n            }\n\n            handle.createQuery(this.locator.query(\"select-ignores\"))\n                .bind(\"id\", uuid)\n                .mapTo(UUID.class)\n                .forEach(ignoredPlayer -> carbonPlayerCommon.ignoring(ignoredPlayer, true, true));\n            handle.createQuery(this.locator.query(\"select-leftchannels\"))\n                .bind(\"id\", uuid)\n                .mapTo(Key.class)\n                .forEach(channel -> {\n                    final @Nullable ChatChannel chatChannel = this.channelRegistry.channel(channel);\n\n                    if (chatChannel == null) {\n                        return;\n                    }\n\n                    carbonPlayerCommon.leaveChannel(chatChannel, true);\n                });\n            return carbonPlayerCommon;\n        });\n    }\n\n    @Override\n    public void saveSync(final CarbonPlayerCommon player) {\n        this.jdbi.useTransaction(handle -> {\n            final int inserted = this.bindPlayerArguments(handle.createUpdate(this.locator.query(\"insert-player\")), player).execute();\n            if (inserted != 1) {\n                this.bindPlayerArguments(handle.createUpdate(this.locator.query(\"update-player\")), player).execute();\n            }\n\n            handle.createUpdate(this.locator.query(\"clear-ignores\"))\n                .bind(\"id\", player.uuid())\n                .execute();\n            handle.createUpdate(this.locator.query(\"clear-leftchannels\"))\n                .bind(\"id\", player.uuid())\n                .execute();\n\n            final Set<UUID> ignored = player.ignoring();\n            if (!ignored.isEmpty()) {\n                final PreparedBatch batch = handle.prepareBatch(this.locator.query(\"save-ignores\"));\n                for (final UUID ignoredPlayer : ignored) {\n                    batch.bind(\"id\", player.uuid()).bind(\"ignoredplayer\", ignoredPlayer).add();\n                }\n                batch.execute();\n            }\n\n            final List<Key> left = player.leftChannels();\n            if (!left.isEmpty()) {\n                final PreparedBatch batch = handle.prepareBatch(this.locator.query(\"save-leftchannels\"));\n                for (final Key leftChannel : left) {\n                    batch.bind(\"id\", player.uuid()).bind(\"channel\", leftChannel).add();\n                }\n                batch.execute();\n            }\n        });\n    }\n\n    @Override\n    protected @Nullable PartyImpl loadParty(final UUID uuid) {\n        return this.jdbi.withHandle(handle -> {\n            final @Nullable PartyImpl party = this.selectParty(handle, uuid);\n            if (party == null) {\n                return null;\n            }\n\n            final List<UUID> members = handle.createQuery(this.locator.query(\"select-party-members\"))\n                .bind(\"partyid\", uuid)\n                .mapTo(UUID.class)\n                .list();\n\n            party.rawMembers().addAll(members);\n\n            return party;\n        });\n    }\n\n    private @Nullable PartyImpl selectParty(final Handle handle, final UUID uuid) {\n        return handle.createQuery(this.locator.query(\"select-party\"))\n            .bind(\"partyid\", uuid)\n            .mapTo(PartyImpl.class)\n            .findOne()\n            .orElse(null);\n    }\n\n    @Override\n    protected void saveSync(final PartyImpl party, final Map<UUID, PartyImpl.ChangeType> changes) {\n        this.jdbi.useTransaction(handle -> {\n            final @Nullable PartyImpl existing = this.selectParty(handle, party.id());\n            if (existing == null) {\n                handle.createUpdate(this.locator.query(\"insert-party\"))\n                    .bind(\"partyid\", party.id())\n                    .bind(\"name\", party.serializedName())\n                    .execute();\n            }\n\n            @Nullable PreparedBatch add = null;\n            @Nullable PreparedBatch remove = null;\n            for (final Map.Entry<UUID, PartyImpl.ChangeType> entry : changes.entrySet()) {\n                final UUID id = entry.getKey();\n                final PartyImpl.ChangeType type = entry.getValue();\n                switch (type) {\n                    case ADD -> {\n                        if (add == null) {\n                            add = handle.prepareBatch(this.locator.query(\"insert-party-member\"));\n                        }\n                        add.bind(\"partyid\", party.id()).bind(\"playerid\", id).add();\n                    }\n                    case REMOVE -> {\n                        if (remove == null) {\n                            remove = handle.prepareBatch(this.locator.query(\"drop-party-member\"));\n                        }\n                        remove.bind(\"playerid\", id).add();\n                    }\n                }\n            }\n            if (add != null) {\n                add.execute();\n            }\n            if (remove != null) {\n                remove.execute();\n            }\n        });\n    }\n\n    @Override\n    public void disbandSync(final UUID id) {\n        this.jdbi.useHandle(handle -> {\n            handle.createUpdate(this.locator.query(\"drop-party\")).bind(\"partyid\", id).execute();\n            handle.createUpdate(this.locator.query(\"clear-party-members\")).bind(\"partyid\", id).execute();\n        });\n    }\n\n    @Override\n    public void shutdown() {\n        super.shutdown();\n        this.dataSource.close();\n    }\n\n    private Update bindPlayerArguments(final Update update, final CarbonPlayerCommon player) {\n        final @Nullable Component nickname = player.nicknameRaw();\n        @Nullable String nicknameJson = GsonComponentSerializer.gson().serializeOrNull(nickname);\n        if (nicknameJson != null && nicknameJson.toCharArray().length > 8192) {\n            this.logger.error(\"Serialized nickname for player {} was too long ({}>8192), it cannot be saved: {}\", player.uuid(), nicknameJson.length(), nicknameJson);\n            nicknameJson = null;\n        }\n        return update.bind(\"id\", player.uuid())\n            .bind(\"muted\", player.muted())\n            .bind(\"muteexpiration\", player.muteExpiration())\n            .bind(\"deafened\", player.deafened())\n            .bind(\"selectedchannel\", player.selectedChannelKey())\n            .bind(\"displayname\", nicknameJson)\n            .bind(\"lastwhispertarget\", player.lastWhisperTarget())\n            .bind(\"whisperreplytarget\", player.whisperReplyTarget())\n            .bind(\"spying\", player.spying())\n            .bind(\"ignoringdms\", player.ignoringDirectMessages())\n            .bind(\"party\", player.partyId())\n            .bind(\"applycustomfilters\", player.applyOptionalChatFilters());\n    }\n\n    public static final class Factory {\n\n        private final ChannelRegistry channelRegistry;\n        private final ConfigManager configManager;\n        private final Logger logger;\n        private final ProfileResolver profileResolver;\n        private final Injector injector;\n        private final Provider<MessagingManager> messagingManager;\n        private final PacketFactory packetFactory;\n        private final CarbonServer server;\n\n        @Inject\n        private Factory(\n            final ChannelRegistry channelRegistry,\n            final ConfigManager configManager,\n            final Logger logger,\n            final ProfileResolver profileResolver,\n            final Injector injector,\n            final Provider<MessagingManager> messagingManager,\n            final PacketFactory packetFactory,\n            final CarbonServer server\n        ) {\n            this.channelRegistry = channelRegistry;\n            this.configManager = configManager;\n            this.logger = logger;\n            this.profileResolver = profileResolver;\n            this.injector = injector;\n            this.messagingManager = messagingManager;\n            this.packetFactory = packetFactory;\n            this.server = server;\n        }\n\n        public DatabaseUserManager create(final String migrationsLocation, final Consumer<Jdbi> configureJdbi) {\n            return this.create(migrationsLocation, configureJdbi, this.configManager.primaryConfig().databaseSettings());\n        }\n\n        public DatabaseUserManager create(final String migrationsLocation, final Consumer<Jdbi> configureJdbi, final DatabaseSettings databaseSettings) {\n            SQLDrivers.loadFrom(this.getClass().getClassLoader());\n\n            final HikariConfig hikariConfig = new HikariConfig();\n            hikariConfig.setJdbcUrl(databaseSettings.url());\n            hikariConfig.setUsername(databaseSettings.username());\n            hikariConfig.setPassword(databaseSettings.password());\n            hikariConfig.setPoolName(\"CarbonChat-HikariPool\");\n            hikariConfig.setThreadFactory(ConcurrentUtil.carbonThreadFactory(this.logger, \"HikariPool\"));\n\n            final DatabaseSettings.ConnectionPool cfg = Objects.requireNonNull(this.configManager.primaryConfig().databaseSettings().connectionPool());\n            hikariConfig.setMaximumPoolSize(cfg.maximumPoolSize);\n            hikariConfig.setMinimumIdle(cfg.minimumIdle);\n            hikariConfig.setMaxLifetime(cfg.maximumLifetime);\n            hikariConfig.setKeepaliveTime(cfg.keepaliveTime);\n            hikariConfig.setConnectionTimeout(cfg.connectionTimeout);\n\n            final HikariDataSource dataSource = new HikariDataSource(hikariConfig);\n\n            final Flyway flyway = Flyway.configure(CarbonChat.class.getClassLoader())\n                .baselineVersion(\"0\")\n                .baselineOnMigrate(true)\n                .locations(migrationsLocation)\n                .dataSource(dataSource)\n                .validateMigrationNaming(true)\n                .validateOnMigrate(true)\n                .load();\n\n            LogFactory.setLogCreator(new CarbonLogCreator(this.logger));\n            this.logger.info(\"Executing Flyway database migrations...\");\n            flyway.repair();\n            flyway.migrate();\n            LogFactory.setLogCreator(null);\n\n            final Jdbi jdbi = Jdbi.create(dataSource)\n                .registerArgument(new ComponentArgumentFactory())\n                .registerArgument(new KeyArgumentFactory())\n                .registerRowMapper(CarbonPlayerCommon.class, new PlayerRowMapper())\n                .registerRowMapper(PartyImpl.class, new PartyRowMapper())\n                .registerColumnMapper(Key.class, new KeyColumnMapper())\n                .registerColumnMapper(Component.class, new ComponentColumnMapper())\n                .installPlugin(new SqlObjectPlugin());\n\n            configureJdbi.accept(jdbi);\n\n            return new DatabaseUserManager(\n                jdbi,\n                dataSource,\n                new QueriesLocator(this.configManager.primaryConfig().storageType()),\n                this.logger,\n                this.profileResolver,\n                this.injector,\n                this.messagingManager,\n                this.packetFactory,\n                this.channelRegistry,\n                this.server\n            );\n        }\n\n    }\n\n    private record CarbonLogCreator(Logger logger) implements LogCreator {\n\n        @Override\n        public Log createLogger(final Class<?> clazz) {\n            final Logger l = this.logger;\n            return new Log() {\n                @Override\n                public boolean isDebugEnabled() {\n                    return true;\n                }\n\n                @Override\n                public void debug(final String message) {\n                    l.debug(\"  [{}] {}\", clazz.getSimpleName(), message);\n                }\n\n                @Override\n                public void info(final String message) {\n                    l.info(\"  [{}] {}\", clazz.getSimpleName(), message);\n                }\n\n                @Override\n                public void warn(final String message) {\n                    l.warn(\"  [{}] {}\", clazz.getSimpleName(), message);\n                }\n\n                @Override\n                public void error(final String message) {\n                    l.error(\"  [{}] {}\", clazz.getSimpleName(), message);\n                }\n\n                @Override\n                public void error(final String message, final Exception e) {\n                    l.error(\"  [{}] {}\", clazz.getSimpleName(), message, e);\n                }\n\n                @Override\n                public void notice(final String message) {\n                    l.info(\"  [{}] (Notice) {}\", clazz.getSimpleName(), message);\n                }\n            };\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/QueriesLocator.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db;\n\nimport com.google.common.base.Splitter;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.regex.Pattern;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.common.config.PrimaryConfig;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jdbi.v3.core.locator.ClasspathSqlLocator;\nimport org.jdbi.v3.core.locator.internal.ClasspathBuilder;\n\n@DefaultQualifier(NonNull.class)\npublic final class QueriesLocator {\n\n    private static final String PREFIX = \"queries/\";\n    private static final Splitter SPLITTER = Splitter.on(';');\n    private final ClasspathSqlLocator locator = ClasspathSqlLocator.create();\n    private final PrimaryConfig.StorageType storageType;\n    private final Pattern templatePattern = Pattern.compile(\"\\\\{([^}]*?)}\");\n    private final Map<String, String> cache = new ConcurrentHashMap<>();\n\n    public QueriesLocator(final PrimaryConfig.StorageType storageType) {\n        this.storageType = storageType;\n    }\n\n    public List<String> queries(final String name) {\n        return SPLITTER.splitToList(this.query(name));\n    }\n\n    public String query(final String name) {\n        return this.locate(PREFIX + name);\n    }\n\n    private String locate(final String name) {\n        return this.cache.computeIfAbsent(name, $ -> {\n            final String sql = this.locator.getResource(\n                CarbonChat.class.getClassLoader(),\n                new ClasspathBuilder()\n                    .appendDotPath(name)\n                    .setExtension(\"sql\")\n                    .build());\n            return this.processTemplates(sql);\n        });\n    }\n\n    private String processTemplates(final String sql) {\n        return this.templatePattern.matcher(sql).replaceAll(match -> {\n            final String insideBraces = match.group(1);\n            try {\n                final int colonIndex = insideBraces.indexOf(':');\n                String prefix = insideBraces.substring(0, colonIndex);\n                final String content = insideBraces.substring(colonIndex + 1);\n                boolean not = false;\n                if (prefix.startsWith(\"!\")) {\n                    not = true;\n                    prefix = prefix.substring(1);\n                }\n                final PrimaryConfig.StorageType storageType = PrimaryConfig.StorageType.valueOf(prefix);\n                if (not) {\n                    return storageType != this.storageType ? content : \"\";\n                } else {\n                    return storageType == this.storageType ? content : \"\";\n                }\n            } catch (final Exception ex) {\n                return match.group(0);\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/argument/BinaryUUIDArgumentFactory.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db.argument;\n\nimport java.sql.Types;\nimport java.util.UUID;\nimport org.jdbi.v3.core.argument.AbstractArgumentFactory;\nimport org.jdbi.v3.core.argument.Argument;\nimport org.jdbi.v3.core.config.ConfigRegistry;\n\npublic final class BinaryUUIDArgumentFactory extends AbstractArgumentFactory<UUID> {\n\n    public BinaryUUIDArgumentFactory() {\n        super(Types.BINARY); // BINARY(16)\n    }\n\n    @Override\n    public Argument build(final UUID value, final ConfigRegistry config) {\n        return (position, statement, ctx) -> statement.setBytes(position, unhex(value.toString().replace(\"-\", \"\")));\n    }\n\n    private static byte[] unhex(final String s) {\n        final int len = s.length();\n        final byte[] data = new byte[len / 2];\n        for (int i = 0; i < len; i += 2) {\n            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)\n                + Character.digit(s.charAt(i + 1), 16));\n        }\n        return data;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/argument/ComponentArgumentFactory.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db.argument;\n\nimport java.sql.Types;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;\nimport org.jdbi.v3.core.argument.AbstractArgumentFactory;\nimport org.jdbi.v3.core.argument.Argument;\nimport org.jdbi.v3.core.config.ConfigRegistry;\n\npublic final class ComponentArgumentFactory extends AbstractArgumentFactory<Component> {\n\n    public ComponentArgumentFactory() {\n        super(Types.VARCHAR);\n    }\n\n    @Override\n    public Argument build(final Component value, final ConfigRegistry config) {\n        return (position, statement, ctx) -> statement.setString(position, GsonComponentSerializer.gson().serialize(value));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/argument/KeyArgumentFactory.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db.argument;\n\nimport java.sql.Types;\nimport net.kyori.adventure.key.Key;\nimport org.jdbi.v3.core.argument.AbstractArgumentFactory;\nimport org.jdbi.v3.core.argument.Argument;\nimport org.jdbi.v3.core.config.ConfigRegistry;\n\npublic final class KeyArgumentFactory extends AbstractArgumentFactory<Key> {\n\n    public KeyArgumentFactory() {\n        super(Types.VARCHAR);\n    }\n\n    @Override\n    public Argument build(final Key value, final ConfigRegistry config) {\n        return (position, statement, ctx) -> statement.setString(position, value.toString());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/mapper/BinaryUUIDColumnMapper.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db.mapper;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.UUID;\nimport net.draycia.carbon.common.util.FastUuidSansHyphens;\nimport net.draycia.carbon.common.util.Strings;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.jdbi.v3.core.mapper.ColumnMapper;\nimport org.jdbi.v3.core.statement.StatementContext;\n\npublic final class BinaryUUIDColumnMapper implements ColumnMapper<UUID> {\n\n    @Override\n    public UUID map(final ResultSet rs, final int columnNumber, final StatementContext ctx) throws SQLException {\n        final byte @Nullable [] bytes = rs.getBytes(columnNumber);\n\n        if (bytes != null) {\n            return FastUuidSansHyphens.parseUuid(Strings.asHexString(bytes));\n        }\n\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/mapper/ComponentColumnMapper.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db.mapper;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport net.draycia.carbon.common.util.Strings;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;\nimport org.jdbi.v3.core.mapper.ColumnMapper;\nimport org.jdbi.v3.core.statement.StatementContext;\n\npublic final class ComponentColumnMapper implements ColumnMapper<Component> {\n\n    @Override\n    public Component map(final ResultSet rs, final int columnNumber, final StatementContext ctx) throws SQLException {\n        return GsonComponentSerializer.gson().deserializeOrNull(Strings.trim(rs.getString(columnNumber)));\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/mapper/KeyColumnMapper.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db.mapper;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport net.draycia.carbon.common.util.Strings;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.intellij.lang.annotations.Subst;\nimport org.jdbi.v3.core.mapper.ColumnMapper;\nimport org.jdbi.v3.core.statement.StatementContext;\n\npublic final class KeyColumnMapper implements ColumnMapper<Key> {\n\n    @Override\n    public Key map(final ResultSet rs, final int columnNumber, final StatementContext ctx) throws SQLException {\n        final @Nullable @Subst(\"key:value\") String keyValue = Strings.trim(rs.getString(columnNumber));\n\n        if (keyValue != null) {\n            return Key.key(keyValue);\n        }\n\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/mapper/NativeUUIDColumnMapper.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db.mapper;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.UUID;\nimport org.jdbi.v3.core.mapper.ColumnMapper;\nimport org.jdbi.v3.core.statement.StatementContext;\n\n// todo: I'm not entirely sure that this is necessary, but I don't feel like testing PSQL\npublic final class NativeUUIDColumnMapper implements ColumnMapper<UUID> {\n\n    @Override\n    public UUID map(final ResultSet rs, final int columnNumber, final StatementContext ctx) throws SQLException {\n        return rs.getObject(columnNumber, UUID.class);\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/mapper/PartyRowMapper.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db.mapper;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.UUID;\nimport net.draycia.carbon.common.users.PartyImpl;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jdbi.v3.core.mapper.ColumnMapper;\nimport org.jdbi.v3.core.mapper.RowMapper;\nimport org.jdbi.v3.core.statement.StatementContext;\n\n@DefaultQualifier(NonNull.class)\npublic final class PartyRowMapper implements RowMapper<PartyImpl> {\n\n    @Override\n    public PartyImpl map(final ResultSet rs, final StatementContext ctx) throws SQLException {\n        final ColumnMapper<Component> component = ctx.findColumnMapperFor(Component.class).orElseThrow();\n        final ColumnMapper<UUID> uuid = ctx.findColumnMapperFor(UUID.class).orElseThrow();\n        return PartyImpl.create(\n            component.map(rs, \"name\", ctx),\n            uuid.map(rs, \"partyid\", ctx)\n        );\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/db/mapper/PlayerRowMapper.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.db.mapper;\n\nimport java.sql.ResultSet;\nimport java.sql.SQLException;\nimport java.util.UUID;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jdbi.v3.core.mapper.ColumnMapper;\nimport org.jdbi.v3.core.mapper.RowMapper;\nimport org.jdbi.v3.core.statement.StatementContext;\n\n@DefaultQualifier(NonNull.class)\npublic final class PlayerRowMapper implements RowMapper<CarbonPlayerCommon> {\n\n    @Override\n    public CarbonPlayerCommon map(final ResultSet rs, final StatementContext ctx) throws SQLException {\n        final ColumnMapper<UUID> uuid = ctx.findColumnMapperFor(UUID.class).orElseThrow();\n        return new CarbonPlayerCommon(\n            rs.getBoolean(\"muted\"),\n            rs.getLong(\"muteexpiration\"),\n            rs.getBoolean(\"deafened\"),\n            ctx.findColumnMapperFor(Key.class).orElseThrow().map(rs, \"selectedchannel\", ctx),\n            null,\n            uuid.map(rs, \"id\", ctx),\n            ctx.findColumnMapperFor(Component.class).orElseThrow().map(rs, \"displayname\", ctx),\n            uuid.map(rs, \"lastwhispertarget\", ctx),\n            uuid.map(rs, \"whisperreplytarget\", ctx),\n            rs.getBoolean(\"spying\"),\n            rs.getBoolean(\"ignoringdms\"),\n            uuid.map(rs, \"party\", ctx),\n            rs.getBoolean(\"applycustomfilters\")\n        );\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/users/json/JSONUserManager.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.users.json;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.google.inject.Provider;\nimport java.io.IOException;\nimport java.io.Reader;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Map;\nimport java.util.UUID;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.common.DataDirectory;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.messaging.packets.PacketFactory;\nimport net.draycia.carbon.common.serialisation.gson.ChatChannelSerializerGson;\nimport net.draycia.carbon.common.serialisation.gson.UUIDSerializerGson;\nimport net.draycia.carbon.common.users.CachingUserManager;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.users.PartyImpl;\nimport net.draycia.carbon.common.users.PersistentUserProperty;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.common.util.Exceptions;\nimport net.draycia.carbon.common.util.FileUtil;\nimport net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class JSONUserManager extends CachingUserManager {\n\n    private final Gson serializer;\n    private final Path userDirectory;\n    private final Path partyDirectory;\n    private final ChannelRegistry channelRegistry;\n\n    @Inject\n    public JSONUserManager(\n        final @DataDirectory Path dataDirectory,\n        final Logger logger,\n        final ProfileResolver profileResolver,\n        final Injector injector,\n        final ChatChannelSerializerGson channelSerializer,\n        final UUIDSerializerGson uuidSerializer,\n        final Provider<MessagingManager> messagingManager,\n        final PacketFactory packetFactory,\n        final CarbonChannelRegistry channelRegistry,\n        final CarbonServer server\n    ) throws IOException {\n        super(\n            logger,\n            profileResolver,\n            injector,\n            messagingManager,\n            packetFactory,\n            server\n        );\n        this.userDirectory = dataDirectory.resolve(\"users\");\n        this.partyDirectory = dataDirectory.resolve(\"party\");\n        this.channelRegistry = channelRegistry;\n\n        Files.createDirectories(this.userDirectory);\n        Files.createDirectories(this.partyDirectory);\n\n        this.serializer = GsonComponentSerializer.gson().populator()\n            .apply(new GsonBuilder())\n            .registerTypeAdapter(ChatChannel.class, channelSerializer)\n            .registerTypeAdapter(UUID.class, uuidSerializer)\n            .registerTypeAdapter(PersistentUserProperty.class, new PersistentUserProperty.Serializer())\n            .setPrettyPrinting()\n            .create();\n    }\n\n    @Override\n    protected CarbonPlayerCommon loadOrCreate(final UUID uuid) {\n        final Path userFile = this.userFile(uuid);\n\n        if (Files.exists(userFile)) {\n            try {\n                final @Nullable CarbonPlayerCommon player;\n                try (final Reader reader = Files.newBufferedReader(userFile)) {\n                    player = this.serializer.fromJson(reader, CarbonPlayerCommon.class);\n                }\n\n                if (player == null) {\n                    throw new IllegalStateException(\"Player file found but was empty.\");\n                }\n                player.leftChannels().forEach(channel -> {\n                    if (this.channelRegistry.channel(channel) == null) {\n                        player.joinChannel(channel, true);\n                    }\n                });\n\n                return player;\n            } catch (final IOException exception) {\n                throw new RuntimeException(exception);\n            }\n        }\n\n        return new CarbonPlayerCommon(null, uuid);\n    }\n\n    private Path userFile(final UUID id) {\n        return this.userDirectory.resolve(id + \".json\");\n    }\n\n    private Path partyFile(final UUID id) {\n        return this.partyDirectory.resolve(id + \".json\");\n    }\n\n    @Override\n    public void saveSync(final CarbonPlayerCommon player) {\n        final Path userFile = this.userFile(player.uuid());\n\n        try {\n            final String json = this.serializer.toJson(player);\n\n            if (json == null || json.isBlank()) {\n                throw new IllegalStateException(\"No data to save - toJson returned null or blank.\");\n            }\n\n            Files.writeString(FileUtil.mkParentDirs(userFile), json);\n        } catch (final IOException exception) {\n            throw new RuntimeException(\"Exception while saving data for player [%s]\".formatted(player.username()), exception);\n        }\n    }\n\n    @Override\n    protected @Nullable PartyImpl loadParty(final UUID uuid) {\n        final Path partyFile = this.partyFile(uuid);\n\n        if (Files.exists(partyFile)) {\n            try {\n                final @Nullable PartyImpl party;\n                try (final Reader reader = Files.newBufferedReader(partyFile)) {\n                    party = this.serializer.<@Nullable PartyImpl>fromJson(reader, PartyImpl.class);\n                }\n\n                if (party == null) {\n                    throw new IllegalStateException(\"Party file found but was empty.\");\n                }\n\n                return party;\n            } catch (final IOException exception) {\n                throw new RuntimeException(exception);\n            }\n        }\n\n        return null;\n    }\n\n    @Override\n    protected void saveSync(final PartyImpl party, final Map<UUID, PartyImpl.ChangeType> changes) {\n        final Path partyFile = this.partyFile(party.id());\n\n        try {\n            final String json = this.serializer.toJson(party);\n\n            if (json == null || json.isBlank()) {\n                throw new IllegalStateException(\"No data to save - toJson returned null or blank.\");\n            }\n\n            Files.writeString(FileUtil.mkParentDirs(partyFile), json);\n        } catch (final IOException exception) {\n            throw new RuntimeException(\"Exception while saving data for party \" + party, exception);\n        }\n    }\n\n    @Override\n    public void disbandSync(final UUID id) {\n        try {\n            Files.deleteIfExists(this.partyFile(id));\n        } catch (final IOException ex) {\n            Exceptions.rethrow(ex);\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/CarbonDependencies.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport java.nio.file.Path;\nimport java.util.Set;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport xyz.jpenilla.gremlin.runtime.DependencyCache;\nimport xyz.jpenilla.gremlin.runtime.DependencyResolver;\nimport xyz.jpenilla.gremlin.runtime.DependencySet;\nimport xyz.jpenilla.gremlin.runtime.logging.Slf4jGremlinLogger;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonDependencies {\n\n    private CarbonDependencies() {\n    }\n\n    public static Set<Path> resolve(final Path cacheDir) {\n        final DependencySet deps = DependencySet.readFromClasspathResource(\n            CarbonDependencies.class.getClassLoader(), \"carbon-dependencies.txt\");\n        final DependencyCache cache = new DependencyCache(cacheDir);\n        final Logger logger = LoggerFactory.getLogger(CarbonDependencies.class.getSimpleName());\n        final Set<Path> files;\n        try (final DependencyResolver downloader = new DependencyResolver(new Slf4jGremlinLogger(logger))) {\n            files = downloader.resolve(deps, cache).jarFiles();\n        }\n        cache.cleanup();\n        return files;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/ChannelUtils.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport net.draycia.carbon.api.CarbonChatProvider;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.kyori.adventure.text.Component;\n\npublic final class ChannelUtils {\n\n    private ChannelUtils() {\n\n    }\n\n    public static void broadcastMessageToChannel(final Component msg, final ChatChannel channel) {\n\n        // TODO: Emit events\n\n        for (final CarbonPlayer recipient : CarbonChatProvider.carbonChat().server().players()) {\n            if (channel.permissions().hearingPermitted(recipient).permitted() && !recipient.leftChannels().contains(channel.key())) {\n                recipient.sendMessage(msg);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/CloudUtils.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Provider;\nimport java.io.PrintWriter;\nimport java.io.StringWriter;\nimport java.lang.reflect.Type;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.regex.Pattern;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.command.CarbonCommand;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.draycia.carbon.common.command.exception.CommandCompleted;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.format.NamedTextColor;\nimport net.kyori.adventure.util.ComponentMessageThrowable;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.exception.ArgumentParseException;\nimport org.incendo.cloud.exception.CommandExecutionException;\nimport org.incendo.cloud.exception.InvalidCommandSenderException;\nimport org.incendo.cloud.exception.InvalidSyntaxException;\nimport org.incendo.cloud.exception.NoPermissionException;\nimport org.incendo.cloud.util.TypeUtils;\n\nimport static org.incendo.cloud.exception.handling.ExceptionHandler.unwrappingHandler;\n\n@DefaultQualifier(NonNull.class)\npublic final class CloudUtils {\n\n    private static final Component NULL = Component.text(\"null\");\n    private static final Pattern SPECIAL_CHARACTERS_PATTERN = Pattern.compile(\"[^\\\\s\\\\w\\\\-]\");\n\n    @Inject\n    private static Provider<Set<CarbonCommand>> commands;\n\n    private CloudUtils() {\n\n    }\n\n    public static Map<Key, CommandSettings> defaultCommandSettings() {\n        final Map<Key, CommandSettings> settings = new HashMap<>();\n\n        for (final var command : commands.get()) {\n            settings.put(command.key(), command.defaultCommandSettings());\n        }\n\n        return settings;\n    }\n\n    public static void registerCommands(final Set<CarbonCommand> commands, final Map<Key, CommandSettings> settings) {\n        for (final var command : commands) {\n            command.commandSettings(settings.get(command.key()));\n\n            if (command.commandSettings().enabled()) {\n                command.init();\n            }\n        }\n    }\n\n    public static Component message(final Throwable throwable) {\n        final @Nullable Component msg = ComponentMessageThrowable.getOrConvertMessage(throwable);\n        return msg == null ? NULL : msg;\n    }\n\n    public static void decorateCommandManager(\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final Logger logger\n    ) {\n        registerExceptionHandlers(commandManager, carbonMessages, logger);\n    }\n\n    public static void registerExceptionHandlers(\n        final CommandManager<Commander> commandManager,\n        final CarbonMessages carbonMessages,\n        final Logger logger\n    ) {\n        commandManager.exceptionController()\n            .registerHandler(ArgumentParseException.class, ctx ->\n                carbonMessages.errorCommandArgumentParsing(ctx.context().sender(), CloudUtils.message(ctx.exception().getCause())))\n            .registerHandler(InvalidCommandSenderException.class, ctx -> {\n                final Set<Type> types = ctx.exception().requiredSenderTypes();\n                if (types.size() != 1) {\n                    throw new IllegalStateException();\n                }\n                carbonMessages.errorCommandInvalidSender(ctx.context().sender(), TypeUtils.simpleName(types.iterator().next()));\n            })\n            .registerHandler(InvalidSyntaxException.class, ctx ->\n                carbonMessages.errorCommandInvalidSyntax(ctx.context().sender(), Component.text(ctx.exception().correctSyntax()).replaceText(\n                    config -> config.match(SPECIAL_CHARACTERS_PATTERN)\n                        .replacement(match -> match.color(NamedTextColor.WHITE)))))\n            .registerHandler(NoPermissionException.class, ctx ->\n                carbonMessages.errorCommandNoPermission(ctx.context().sender()))\n            .registerHandler(CommandExecutionException.class, ctx -> {\n                final Throwable cause = ctx.exception().getCause();\n\n                logger.warn(\"Unexpected exception executing command\", cause);\n\n                final StringWriter writer = new StringWriter();\n                cause.printStackTrace(new PrintWriter(writer));\n                final String stackTrace = writer.toString().replaceAll(\"\\t\", \"    \");\n                final @Nullable Component throwableMessage = CloudUtils.message(cause);\n\n                carbonMessages.errorCommandCommandExecution(ctx.context().sender(), throwableMessage, stackTrace);\n            })\n            .registerHandler(CommandExecutionException.class, unwrappingHandler(CommandCompleted.class))\n            .registerHandler(CommandCompleted.class, ctx -> {\n                final @Nullable Component msg = ctx.exception().componentMessage();\n                if (msg != null) {\n                    ctx.context().sender().sendMessage(msg);\n                }\n            });\n    }\n\n    public static CarbonPlayer nonPlayerMustProvidePlayer(final CarbonMessages messages, final Commander commander) {\n        if (commander instanceof PlayerCommander playerCommander) {\n            return playerCommander.carbonPlayer();\n        }\n        throw CommandCompleted.withMessage(messages.commandNeedsPlayer());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/ColorUtils.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport java.util.regex.Pattern;\nimport net.kyori.adventure.text.format.NamedTextColor;\nimport net.kyori.adventure.text.format.TextColor;\nimport net.kyori.adventure.text.format.TextDecoration;\nimport net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;\nimport net.kyori.adventure.text.serializer.legacy.LegacyFormat;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n/**\n * Basic color related utilities.\n *\n * @since 1.0.0\n */\n@DefaultQualifier(NonNull.class)\npublic final class ColorUtils {\n\n    private static final Pattern spigotLegacyRGB =\n        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])\");\n    private static final Pattern pluginRGB =\n        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])\");\n    private static final String hexReplacement = \"<#$1$2$3$4$5$6>\";\n\n    private ColorUtils() {\n\n    }\n\n    /**\n     * Parses the input into a color.<br>\n     * Supports named colors, legacy, and hex inputs.\n     *\n     * @param input the color input\n     * @return the color\n     * @since 1.0.0\n     */\n    public static @Nullable TextColor parseColor(String input) {\n        if (input.isEmpty()) {\n            return NamedTextColor.WHITE;\n        }\n\n        for (final NamedTextColor namedColor : NamedTextColor.NAMES.values()) {\n            if (namedColor.toString().equalsIgnoreCase(input)) {\n                return namedColor;\n            }\n        }\n\n        if (input.contains(\"&\") || input.contains(\"§\")) {\n            input = input.replace(\"&\", \"§\");\n\n            return LegacyComponentSerializer.legacySection().deserialize(input).color();\n        }\n\n        return TextColor.fromCSSHexString(input);\n    }\n\n    /**\n     * Converts the input legacy, legacy rgb, and alternate color formats to MiniMessage color tags.\n     *\n     * @param input the message to convert\n     * @return the converted message\n     * @since 1.0.0\n     */\n    public static String legacyToMiniMessage(final String input) {\n        String output = input;\n\n        // Legacy RGB\n        output = spigotLegacyRGB.matcher(output).replaceAll(hexReplacement);\n\n        // Alternate RGB, TAB (neznamy) && KiteBoard\n        output = pluginRGB.matcher(output).replaceAll(hexReplacement);\n\n        // Legacy Colors\n        for (final char c : \"0123456789abcdefABCDEF\".toCharArray()) {\n            final @Nullable LegacyFormat format = LegacyComponentSerializer.parseChar(Character.toLowerCase(c));\n\n            if (format != null) {\n                final @Nullable TextColor color = format.color();\n\n                if (color != null) {\n                    output = output.replaceAll(\"[§&]\" + c, \"<\" + color.asHexString() + \">\");\n                }\n            }\n        }\n\n        // Legacy Formatting\n        for (final char c : \"klmnoKLMNO\".toCharArray()) {\n            final @Nullable LegacyFormat format = LegacyComponentSerializer.parseChar(Character.toLowerCase(c));\n\n            if (format != null) {\n                final @Nullable TextDecoration decoration = format.decoration();\n\n                if (decoration != null) {\n                    output = output.replaceAll(\"[§&]\" + c, \"<\" + decoration.name() + \">\");\n                }\n            }\n        }\n\n        return output;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/ConcurrentUtil.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport com.google.common.util.concurrent.ThreadFactoryBuilder;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.TimeUnit;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class ConcurrentUtil {\n\n    private ConcurrentUtil() {\n    }\n\n    public static void shutdownExecutor(final ExecutorService service, final TimeUnit timeoutUnit, final long timeoutLength) {\n        service.shutdown();\n        boolean didShutdown;\n        try {\n            didShutdown = service.awaitTermination(timeoutLength, timeoutUnit);\n        } catch (final InterruptedException ignore) {\n            didShutdown = false;\n        }\n        if (!didShutdown) {\n            service.shutdownNow();\n        }\n    }\n\n    public static ThreadFactory carbonThreadFactory(final Logger logger, final String name) {\n        return new ThreadFactoryBuilder()\n            .setDaemon(true)\n            .setNameFormat(\"CarbonChat \" + name + \" Thread #%d\")\n            .setUncaughtExceptionHandler((thread, thr) -> logger.warn(\"Uncaught exception on thread {}\", thread.getName(), thr))\n            .build();\n    }\n\n    public static ScheduledExecutorService createPeriodicTasksPool(final Logger logger) {\n        return new ExceptionLoggingScheduledThreadPoolExecutor(\n            1,\n            carbonThreadFactory(logger, \"Periodic Tasks\"),\n            logger\n        );\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/DiscordRecipient.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport net.kyori.adventure.audience.Audience;\n\npublic final class DiscordRecipient implements Audience {\n\n    public static final DiscordRecipient INSTANCE = new DiscordRecipient();\n\n    private DiscordRecipient() {\n\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/EmptyAudienceWithPointers.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport net.kyori.adventure.identity.Identity;\nimport net.kyori.adventure.pointer.Pointers;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class EmptyAudienceWithPointers implements ForwardingAudience.Single {\n\n    private final Pointers pointers;\n\n    private EmptyAudienceWithPointers(final Pointers pointers) {\n        this.pointers = pointers;\n    }\n\n    @Override\n    public Audience audience() {\n        return Audience.empty();\n    }\n\n    @Override\n    public Pointers pointers() {\n        return this.pointers;\n    }\n\n    public static EmptyAudienceWithPointers forCarbonPlayer(final CarbonPlayer player) {\n        return new EmptyAudienceWithPointers(Pointers.builder()\n            .withStatic(Identity.UUID, player.uuid())\n            .withStatic(Identity.NAME, player.username())\n            .withDynamic(Identity.DISPLAY_NAME, player::displayName)\n            .build());\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/ExceptionLoggingScheduledThreadPoolExecutor.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport java.util.concurrent.Callable;\nimport java.util.concurrent.ScheduledFuture;\nimport java.util.concurrent.ScheduledThreadPoolExecutor;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.TimeUnit;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class ExceptionLoggingScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor {\n\n    private final Logger logger;\n\n    public ExceptionLoggingScheduledThreadPoolExecutor(final int corePoolSize, final ThreadFactory threadFactory, final Logger logger) {\n        super(corePoolSize, threadFactory);\n        this.logger = logger;\n    }\n\n    @Override\n    public ScheduledFuture<?> schedule(final Runnable command, final long delay, final TimeUnit unit) {\n        return super.schedule(new ExceptionLoggingRunnable(command, this.logger), delay, unit);\n    }\n\n    @Override\n    public <V> ScheduledFuture<V> schedule(final Callable<V> callable, final long delay, final TimeUnit unit) {\n        throw new UnsupportedOperationException();\n    }\n\n    @Override\n    public ScheduledFuture<?> scheduleAtFixedRate(final Runnable command, final long initialDelay, final long period, final TimeUnit unit) {\n        return super.scheduleAtFixedRate(new ExceptionLoggingRunnable(command, this.logger), initialDelay, period, unit);\n    }\n\n    @Override\n    public ScheduledFuture<?> scheduleWithFixedDelay(final Runnable command, final long initialDelay, final long delay, final TimeUnit unit) {\n        return super.scheduleWithFixedDelay(new ExceptionLoggingRunnable(command, this.logger), initialDelay, delay, unit);\n    }\n\n    private record ExceptionLoggingRunnable(Runnable wrapped, Logger logger) implements Runnable {\n        @Override\n        public void run() {\n            try {\n                this.wrapped.run();\n            } catch (final Throwable thr) {\n                this.logger.error(\"Error executing task '{}'\", this.wrapped, thr);\n                Exceptions.rethrow(thr);\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/Exceptions.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport java.util.function.Consumer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class Exceptions {\n\n    private Exceptions() {\n    }\n\n    @SuppressWarnings(\"unchecked\")\n    public static <X extends Throwable> RuntimeException rethrow(final Throwable t) throws X {\n        throw (X) t;\n    }\n\n    public static <T, X extends Throwable> Consumer<T> sneaky(final CheckedConsumer<T, X> consumer) {\n        return t -> {\n            try {\n                consumer.accept(t);\n            } catch (final Throwable thr) {\n                rethrow(thr);\n            }\n        };\n    }\n\n    @FunctionalInterface\n    public interface CheckedConsumer<T, X extends Throwable> {\n        void accept(T t) throws X;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/FastUuidSansHyphens.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport java.util.Arrays;\nimport java.util.UUID;\n\n/*\n * The MIT License (MIT)\n *\n * Copyright (c) 2018 Jon Chambers\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\n/**\n * This is a modified FastUUID implementation. The primary difference is that it does not dash its\n * UUIDs. As the native Java 9+ UUID.toString() implementation dashes its UUIDs, we use the FastUUID\n * methods, which ought to be faster than a String.replace().\n */\npublic final class FastUuidSansHyphens {\n\n    private static final int MOJANG_BROKEN_UUID_LENGTH = 32;\n\n    private static final char[] HEX_DIGITS =\n        new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};\n\n    private static final long[] HEX_VALUES = new long[128];\n\n    static {\n        Arrays.fill(HEX_VALUES, -1);\n\n        HEX_VALUES['0'] = 0x0;\n        HEX_VALUES['1'] = 0x1;\n        HEX_VALUES['2'] = 0x2;\n        HEX_VALUES['3'] = 0x3;\n        HEX_VALUES['4'] = 0x4;\n        HEX_VALUES['5'] = 0x5;\n        HEX_VALUES['6'] = 0x6;\n        HEX_VALUES['7'] = 0x7;\n        HEX_VALUES['8'] = 0x8;\n        HEX_VALUES['9'] = 0x9;\n\n        HEX_VALUES['a'] = 0xa;\n        HEX_VALUES['b'] = 0xb;\n        HEX_VALUES['c'] = 0xc;\n        HEX_VALUES['d'] = 0xd;\n        HEX_VALUES['e'] = 0xe;\n        HEX_VALUES['f'] = 0xf;\n\n        HEX_VALUES['A'] = 0xa;\n        HEX_VALUES['B'] = 0xb;\n        HEX_VALUES['C'] = 0xc;\n        HEX_VALUES['D'] = 0xd;\n        HEX_VALUES['E'] = 0xe;\n        HEX_VALUES['F'] = 0xf;\n    }\n\n    private FastUuidSansHyphens() {\n        // A private constructor prevents callers from accidentally instantiating FastUUID instances\n    }\n\n    /**\n     * Parses a UUID from the given character sequence. The character sequence must represent a\n     * Mojang UUID.\n     *\n     * @param uuidSequence the character sequence from which to parse a UUID\n     *\n     * @return the UUID represented by the given character sequence\n     *\n     * @throws IllegalArgumentException if the given character sequence does not conform to the string\n     *         representation of a Mojang UUID.\n     */\n    public static UUID parseUuid(final CharSequence uuidSequence) {\n        if (uuidSequence.length() != MOJANG_BROKEN_UUID_LENGTH) {\n            throw new IllegalArgumentException(\"Illegal UUID string: \" + uuidSequence);\n        }\n\n        long mostSignificantBits = hexValueForChar(uuidSequence.charAt(0)) << 60;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(1)) << 56;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(2)) << 52;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(3)) << 48;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(4)) << 44;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(5)) << 40;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(6)) << 36;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(7)) << 32;\n\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(8)) << 28;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(9)) << 24;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(10)) << 20;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(11)) << 16;\n\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(12)) << 12;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(13)) << 8;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(14)) << 4;\n        mostSignificantBits |= hexValueForChar(uuidSequence.charAt(15));\n\n        long leastSignificantBits = hexValueForChar(uuidSequence.charAt(16)) << 60;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(17)) << 56;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(18)) << 52;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(19)) << 48;\n\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(20)) << 44;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(21)) << 40;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(22)) << 36;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(23)) << 32;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(24)) << 28;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(25)) << 24;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(26)) << 20;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(27)) << 16;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(28)) << 12;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(29)) << 8;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(30)) << 4;\n        leastSignificantBits |= hexValueForChar(uuidSequence.charAt(31));\n\n        return new UUID(mostSignificantBits, leastSignificantBits);\n    }\n\n    /**\n     * Returns a string representation of the given UUID. The returned string is formatted as a\n     * Mojang-style UUID.\n     *\n     * @param uuid the UUID to represent as a string\n     *\n     * @return a string representation of the given UUID\n     */\n    public static String toString(final UUID uuid) {\n        final long mostSignificantBits = uuid.getMostSignificantBits();\n        final long leastSignificantBits = uuid.getLeastSignificantBits();\n\n        final char[] uuidChars = new char[MOJANG_BROKEN_UUID_LENGTH];\n\n        uuidChars[0] = HEX_DIGITS[(int) ((mostSignificantBits & 0xf000000000000000L) >>> 60)];\n        uuidChars[1] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0f00000000000000L) >>> 56)];\n        uuidChars[2] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00f0000000000000L) >>> 52)];\n        uuidChars[3] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000f000000000000L) >>> 48)];\n        uuidChars[4] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000f00000000000L) >>> 44)];\n        uuidChars[5] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000f0000000000L) >>> 40)];\n        uuidChars[6] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000f000000000L) >>> 36)];\n        uuidChars[7] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000f00000000L) >>> 32)];\n        uuidChars[8] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000f0000000L) >>> 28)];\n        uuidChars[9] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000000f000000L) >>> 24)];\n        uuidChars[10] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000000f00000L) >>> 20)];\n        uuidChars[11] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000000f0000L) >>> 16)];\n        uuidChars[12] = HEX_DIGITS[(int) ((mostSignificantBits & 0x000000000000f000L) >>> 12)];\n        uuidChars[13] = HEX_DIGITS[(int) ((mostSignificantBits & 0x0000000000000f00L) >>> 8)];\n        uuidChars[14] = HEX_DIGITS[(int) ((mostSignificantBits & 0x00000000000000f0L) >>> 4)];\n        uuidChars[15] = HEX_DIGITS[(int) (mostSignificantBits & 0x000000000000000fL)];\n        uuidChars[16] = HEX_DIGITS[(int) ((leastSignificantBits & 0xf000000000000000L) >>> 60)];\n        uuidChars[17] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0f00000000000000L) >>> 56)];\n        uuidChars[18] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00f0000000000000L) >>> 52)];\n        uuidChars[19] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000f000000000000L) >>> 48)];\n        uuidChars[20] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000f00000000000L) >>> 44)];\n        uuidChars[21] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000f0000000000L) >>> 40)];\n        uuidChars[22] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000f000000000L) >>> 36)];\n        uuidChars[23] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000f00000000L) >>> 32)];\n        uuidChars[24] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000f0000000L) >>> 28)];\n        uuidChars[25] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000000f000000L) >>> 24)];\n        uuidChars[26] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000000f00000L) >>> 20)];\n        uuidChars[27] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000000f0000L) >>> 16)];\n        uuidChars[28] = HEX_DIGITS[(int) ((leastSignificantBits & 0x000000000000f000L) >>> 12)];\n        uuidChars[29] = HEX_DIGITS[(int) ((leastSignificantBits & 0x0000000000000f00L) >>> 8)];\n        uuidChars[30] = HEX_DIGITS[(int) ((leastSignificantBits & 0x00000000000000f0L) >>> 4)];\n        uuidChars[31] = HEX_DIGITS[(int) (leastSignificantBits & 0x000000000000000fL)];\n\n        return new String(uuidChars);\n    }\n\n    private static long hexValueForChar(final char c) {\n        try {\n            if (HEX_VALUES[c] < 0) {\n                throw new IllegalArgumentException(\"Illegal hexadecimal digit: \" + c);\n            }\n        } catch (final ArrayIndexOutOfBoundsException e) {\n            throw new IllegalArgumentException(\"Illegal hexadecimal digit: \" + c);\n        }\n\n        return HEX_VALUES[c];\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/FileUtil.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport com.google.common.hash.Hashing;\nimport java.io.IOException;\nimport java.io.UncheckedIOException;\nimport java.nio.file.DirectoryStream;\nimport java.nio.file.FileAlreadyExistsException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic final class FileUtil {\n\n    private FileUtil() {\n    }\n\n    /**\n     * Calculates the SHA256 hash of {@code file} and returns it as a hex string.\n     *\n     * @param file file to hash\n     * @return SHA256 hash string\n     * @throws IOException              on I/O error\n     * @throws IllegalArgumentException when {@code file} is not a regular file\n     */\n    public static String hashString(final Path file) throws IOException {\n        if (!Files.isRegularFile(file)) {\n            throw new IllegalArgumentException(\"Path '%s' is not a regular file, cannot generate hash string.\".formatted(file));\n        }\n        final byte[] hash = com.google.common.io.Files.asByteSource(file.toFile()).hash(Hashing.sha256()).asBytes();\n        return Strings.asHexString(hash);\n    }\n\n    /**\n     * Lists directory entries in {@code path}.\n     *\n     * <p>If {@code path} does not exist, returns an empty list.</p>\n     *\n     * <p>If {@code path} exists, but is not a directory, throws {@link IllegalArgumentException}</p>\n     *\n     * @param path directory\n     * @return directory entries\n     * @throws IllegalArgumentException when {@code path} exists but is not a directory\n     * @throws UncheckedIOException     on I/O error\n     */\n    public static List<Path> listDirectoryEntries(final Path path) {\n        return listDirectoryEntries(path, \"*\");\n    }\n\n    /**\n     * Lists directory entries in {@code path} matching {@code glob}.\n     *\n     * <p>If {@code path} does not exist, returns an empty list.</p>\n     *\n     * <p>If {@code path} exists, but is not a directory, throws {@link IllegalArgumentException}</p>\n     *\n     * @param path directory\n     * @param glob glob pattern\n     * @return matching directory entries\n     * @throws IllegalArgumentException when {@code path} exists but is not a directory\n     * @throws UncheckedIOException     on I/O error\n     */\n    public static List<Path> listDirectoryEntries(final Path path, final String glob) {\n        if (!Files.exists(path)) {\n            return List.of();\n        } else if (!Files.isDirectory(path)) {\n            throw new IllegalArgumentException(\"Path '%s' exists but is not a directory!\".formatted(path));\n        }\n\n        try (final DirectoryStream<Path> stream = Files.newDirectoryStream(path, glob)) {\n            final List<Path> ret = new ArrayList<>();\n            stream.forEach(ret::add);\n            return ret;\n        } catch (final IOException exception) {\n            throw new UncheckedIOException(\"Failed to list directory entries matching '%s' in path '%s'.\".formatted(glob, path), exception);\n        }\n    }\n\n    /**\n     * Attempts to create the parent directories of {@code path} if necessary.\n     *\n     * <p>Returns {@code path} when successful.</p>\n     *\n     * @param path path\n     * @return {@code path}\n     * @throws IOException on I/O error\n     */\n    public static Path mkParentDirs(final Path path) throws IOException {\n        final Path parent = path.getParent();\n        if (parent != null && !Files.isDirectory(parent)) {\n            try {\n                Files.createDirectories(parent);\n            } catch (final FileAlreadyExistsException ex) {\n                if (!Files.isDirectory(parent)) {\n                    throw ex;\n                }\n            }\n        }\n        return path;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/Pagination.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.Collections;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.RandomAccess;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.ComponentLike;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static java.util.Objects.requireNonNull;\nimport static net.kyori.adventure.text.Component.empty;\n\n@DefaultQualifier(NonNull.class)\npublic interface Pagination<T> {\n\n    ComponentLike header(int page, int pages);\n\n    ComponentLike footer(int page, int pages);\n\n    ComponentLike pageOutOfRange(int page, int pages);\n\n    ComponentLike item(T item, boolean lastOfPage);\n\n    default List<Component> render(\n        final Collection<T> content,\n        final int page,\n        final int itemsPerPage\n    ) {\n        if (content.isEmpty()) {\n            throw new IllegalArgumentException(\"Cannot paginate an empty collection.\");\n        }\n\n        final int pages = (int) Math.ceil(content.size() / (itemsPerPage * 1.00));\n        if (page < 1 || page > pages) {\n            return Collections.singletonList(this.pageOutOfRange(page, pages).asComponent());\n        }\n\n        final List<Component> renderedContent = new ArrayList<>();\n\n        final Component header = this.header(page, pages).asComponent();\n        if (header != empty()) {\n            renderedContent.add(header);\n        }\n\n        final int start = itemsPerPage * (page - 1);\n        final int maxIndex = start + itemsPerPage;\n\n        if (content instanceof RandomAccess && content instanceof final List<T> contentList) {\n            for (int i = start; i < maxIndex; i++) {\n                if (i > content.size() - 1) {\n                    break;\n                }\n                renderedContent.add(this.item(contentList.get(i), i == maxIndex - 1).asComponent());\n            }\n        } else {\n            final Iterator<T> iterator = content.iterator();\n            for (int i = 0; i < start && iterator.hasNext(); i++) {\n                iterator.next();\n            }\n            for (int i = start; i < maxIndex && iterator.hasNext(); ++i) {\n                renderedContent.add(this.item(iterator.next(), i == maxIndex - 1).asComponent());\n            }\n        }\n\n        final Component footer = this.footer(page, pages).asComponent();\n        if (footer != empty()) {\n            renderedContent.add(footer);\n        }\n\n        return Collections.unmodifiableList(renderedContent);\n    }\n\n    static <T> Builder<T> builder() {\n        return new Builder<>();\n    }\n\n    final class Builder<T> {\n        private BiIntFunction<ComponentLike> headerRenderer = ($, $$) -> empty();\n        private BiIntFunction<ComponentLike> footerRenderer = ($, $$) -> empty();\n        private @MonotonicNonNull BiIntFunction<ComponentLike> pageOutOfRangeRenderer = null;\n        private @MonotonicNonNull ItemRenderer<T> itemRenderer = null;\n\n        private Builder() {\n        }\n\n        public Builder<T> header(final BiIntFunction<ComponentLike> headerRenderer) {\n            this.headerRenderer = headerRenderer;\n            return this;\n        }\n\n        public Builder<T> footer(final BiIntFunction<ComponentLike> footerRenderer) {\n            this.footerRenderer = footerRenderer;\n            return this;\n        }\n\n        public Builder<T> pageOutOfRange(final BiIntFunction<ComponentLike> pageOutOfRangeRenderer) {\n            this.pageOutOfRangeRenderer = pageOutOfRangeRenderer;\n            return this;\n        }\n\n        public Builder<T> item(final ItemRenderer<T> itemRenderer) {\n            this.itemRenderer = itemRenderer;\n            return this;\n        }\n\n        public Pagination<T> build() {\n            return new DelegatingPaginationImpl<>(\n                requireNonNull(this.headerRenderer, \"Must provide a header renderer!\"),\n                requireNonNull(this.footerRenderer, \"Must provide a footer renderer!\"),\n                requireNonNull(this.pageOutOfRangeRenderer, \"Must provide a page out of range renderer!\"),\n                requireNonNull(this.itemRenderer, \"Must provide an item renderer!\")\n            );\n        }\n\n        @FunctionalInterface\n        public interface ItemRenderer<T> {\n            ComponentLike render(T item, boolean lastOfPage);\n        }\n\n        private record DelegatingPaginationImpl<T>(\n            BiIntFunction<ComponentLike> headerRenderer,\n            BiIntFunction<ComponentLike> footerRenderer,\n            BiIntFunction<ComponentLike> pageOutOfRangeRenderer,\n            ItemRenderer<T> itemRenderer\n        ) implements Pagination<T> {\n            @Override\n            public ComponentLike header(final int page, final int pages) {\n                return this.headerRenderer.apply(page, pages);\n            }\n\n            @Override\n            public ComponentLike footer(final int page, final int pages) {\n                return this.footerRenderer.apply(page, pages);\n            }\n\n            @Override\n            public ComponentLike pageOutOfRange(final int page, final int pages) {\n                return this.pageOutOfRangeRenderer.apply(page, pages);\n            }\n\n            @Override\n            public ComponentLike item(final T item, final boolean lastOfPage) {\n                return this.itemRenderer.render(item, lastOfPage);\n            }\n        }\n    }\n\n    @FunctionalInterface\n    interface BiIntFunction<T> {\n\n        T apply(int i, int i1);\n\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/PaginationHelper.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport com.google.inject.Inject;\nimport java.util.function.IntFunction;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.ComponentLike;\nimport net.kyori.adventure.text.TextComponent;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static net.kyori.adventure.text.Component.empty;\nimport static net.kyori.adventure.text.Component.space;\nimport static net.kyori.adventure.text.Component.text;\nimport static net.kyori.adventure.text.event.ClickEvent.runCommand;\n\n@DefaultQualifier(NonNull.class)\npublic final class PaginationHelper {\n\n    private final CarbonMessages messages;\n\n    @Inject\n    private PaginationHelper(final CarbonMessages messages) {\n        this.messages = messages;\n    }\n\n    public Pagination.BiIntFunction<ComponentLike> footerRenderer(final IntFunction<String> commandFunction) {\n        return (currentPage, pages) -> {\n            if (pages == 1) {\n                return empty(); // we don't need to see 'Page 1/1'\n            }\n            final TextComponent.Builder buttons = text();\n            if (currentPage > 1) {\n                buttons.append(this.previousPageButton(currentPage, commandFunction));\n            }\n            if (currentPage > 1 && currentPage < pages) {\n                buttons.append(space());\n            }\n            if (currentPage < pages) {\n                buttons.append(this.nextPageButton(currentPage, commandFunction));\n            }\n            return this.messages.paginationFooter(currentPage, pages, buttons.build());\n        };\n    }\n\n    private Component previousPageButton(final int currentPage, final IntFunction<String> commandFunction) {\n        return text()\n            .content(\"←\")\n            .clickEvent(runCommand(commandFunction.apply(currentPage - 1)))\n            .hoverEvent(this.messages.paginationClickForPreviousPage())\n            .build();\n    }\n\n    private Component nextPageButton(final int currentPage, final IntFunction<String> commandFunction) {\n        return text()\n            .content(\"→\")\n            .clickEvent(runCommand(commandFunction.apply(currentPage + 1)))\n            .hoverEvent(this.messages.paginationClickForNextPage())\n            .build();\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/SQLDrivers.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport java.sql.Driver;\nimport java.util.ServiceLoader;\n\npublic final class SQLDrivers {\n\n    private SQLDrivers() {\n    }\n\n    public static void loadFrom(final ClassLoader loader) {\n        ServiceLoader.load(Driver.class, loader).stream()\n            .forEach(provider -> forceInit(provider.type()));\n    }\n\n    private static <T> Class<T> forceInit(final Class<T> klass) {\n        try {\n            Class.forName(klass.getName(), true, klass.getClassLoader());\n        } catch (final ClassNotFoundException e) {\n            throw new AssertionError(e);\n        }\n        return klass;\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/Strings.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport com.google.common.base.Suppliers;\nimport java.net.URI;\nimport java.net.URISyntaxException;\nimport java.util.function.Supplier;\nimport java.util.regex.Pattern;\nimport net.kyori.adventure.text.TextReplacementConfig;\nimport net.kyori.adventure.text.event.ClickEvent;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class Strings {\n\n    private static final Pattern DEFAULT_URL_PATTERN = Pattern.compile(\"(?:(https?)://)?([-\\\\w_.]+\\\\.\\\\w{2,})(/([A-Za-z0-9\\\\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*)?\");\n    private static final Pattern URL_SCHEME_PATTERN = Pattern.compile(\"^[a-z][a-z0-9+\\\\-.]*:\");\n    public static final Supplier<TextReplacementConfig> URL_REPLACEMENT_CONFIG = Suppliers.memoize(\n        () -> TextReplacementConfig.builder()\n            .match(DEFAULT_URL_PATTERN)\n            .replacement(url -> {\n                String clickUrl = url.content();\n                if (!URL_SCHEME_PATTERN.matcher(clickUrl).find()) {\n                    clickUrl = \"http://\" + clickUrl;\n                }\n\n                try {\n                    final URI ignored = new URI(clickUrl); // just to validate that the uri is valid\n                    return url.clickEvent(ClickEvent.openUrl(clickUrl));\n                } catch (final URISyntaxException ignored) {\n                    return url;\n                }\n            })\n            .build()\n    );\n\n    private Strings() {\n    }\n\n    public static @Nullable String trim(final @Nullable String s) {\n        return s == null ? null : s.trim();\n    }\n\n    public static String asHexString(final byte[] bytes) {\n        final StringBuilder sb = new StringBuilder(bytes.length * 2);\n        for (final byte b : bytes) {\n            sb.append(\"%02x\".formatted(b & 0xFF));\n        }\n        return sb.toString();\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/java/net/draycia/carbon/common/util/UpdateChecker.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.common.util;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.URI;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.jar.Manifest;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic record UpdateChecker(Logger logger) {\n\n    private static final String GITHUB_REPO = \"Hexaoxide/Carbon\";\n    private static final String UPDATE_CHECKER_FETCHING_VERSION_INFORMATION = \"Fetching version information...\";\n    private static final String DEV_BUILD_NOTICE = \"This is a development version of CarbonChat (<version>)!\";\n    private static final String UPDATE_CHECKER_BEHIND_RELEASES = \"CarbonChat is <behind> version(s) out of date (<version>).\";\n    private static final String UPDATE_CHECKER_DOWNLOAD_RELEASE = \"Download the latest release (<latest>) from <link>\";\n    private static final String RELEASE_DOWNLOADS_URL = \"https://modrinth.com/plugin/carbon/versions\";\n    private static final Gson GSON = new GsonBuilder().create();\n\n    public void checkVersion() {\n        this.logger.info(UPDATE_CHECKER_FETCHING_VERSION_INFORMATION);\n\n        final @Nullable Manifest manifest = manifest(UpdateChecker.class); // we expect to be shaded into platform jars\n        if (manifest == null) {\n            this.logger.warn(\"Failed to locate manifest, cannot check for updates.\");\n            return;\n        }\n\n        final String currentVersion = manifest.getMainAttributes().getValue(\"carbon-version\");\n\n        final Releases releases;\n        try {\n            releases = this.fetchReleases();\n        } catch (final IOException e) {\n            this.logger.warn(\"Failed to list releases, cannot check for updates.\", e);\n            return;\n        }\n\n        final String ver = \"v\" + currentVersion;\n        if (releases.releaseList().get(0).equals(ver)) {\n            return;\n        }\n        if (currentVersion.contains(\"-SNAPSHOT\")) {\n            this.logger.info(DEV_BUILD_NOTICE.replace(\"<version>\", ver));\n        } else {\n            final int versionsBehind = releases.releaseList().indexOf(ver);\n            this.logger.info(\n                UPDATE_CHECKER_BEHIND_RELEASES\n                    .replace(\"<behind>\", String.valueOf(versionsBehind == -1 ? \"?\" : versionsBehind))\n                    .replace(\"<version>\", ver)\n            );\n        }\n        this.logger.info(\n            UPDATE_CHECKER_DOWNLOAD_RELEASE\n                .replace(\"<latest>\", releases.releaseList().get(0))\n                .replace(\"<link>\", RELEASE_DOWNLOADS_URL) // , releases.releaseUrls().get(releases.releaseList().get(0)))\n        );\n    }\n\n    private Releases fetchReleases() throws IOException {\n        final JsonArray result;\n        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))) {\n            result = GSON.fromJson(reader, JsonArray.class);\n        }\n\n        final Map<String, String> versionMap = new LinkedHashMap<>();\n        for (final JsonElement element : result) {\n            versionMap.put(\n                element.getAsJsonObject().get(\"tag_name\").getAsString(),\n                element.getAsJsonObject().get(\"html_url\").getAsString()\n            );\n        }\n        return new Releases(new ArrayList<>(versionMap.keySet()), versionMap);\n    }\n\n    private record Releases(List<String> releaseList, Map<String, String> releaseUrls) {\n    }\n\n    public static @Nullable Manifest manifest(final Class<?> clazz) {\n        final String classLocation = \"/\" + clazz.getName().replace(\".\", \"/\") + \".class\";\n        final @Nullable URL resource = clazz.getResource(classLocation);\n        if (resource == null) {\n            return null;\n        }\n        final String classFilePath = resource.toString().replace(\"\\\\\", \"/\");\n        final String archivePath = classFilePath.substring(0, classFilePath.length() - classLocation.length());\n        try (final InputStream stream = URI.create(archivePath + \"/META-INF/MANIFEST.MF\").toURL().openStream()) {\n            return new Manifest(stream);\n        } catch (final IOException ex) {\n            return null;\n        }\n    }\n\n}\n"
  },
  {
    "path": "common/src/main/resources/carbon-permissions.yml",
    "content": "carbon.clearchat:\n  description: \"Clears the chat for all players except those with carbon.chearchat.exempt.\"\n  children:\n    carbon.clearchat.clear: true\ncarbon.clearchat.clear: \"Clears the chat for all players except those with carbon.chearchat.exempt.\"\ncarbon.clearchat.exempt: \"Exempts the player from having their chat cleared when /clearchat is executed.\"\ncarbon.debug: \"Allows the sender to quickly check what carbon think's the player's primary and non-primary groups are.\"\ncarbon.help: \"Shows Carbon's help menu, detailing each part of Carbon's commands.\"\ncarbon.hideidentity: \"Prevents messages from the player from being blocked clientside.\"\ncarbon.ignore: \"Ignores the player, hiding messages they send in chat and in whispers.\"\ncarbon.ignore.exempt: \"Prevents the player from being ignored.\"\ncarbon.ignore.unignore: \"Removes the player from the sender's ignore list.\"\ncarbon.itemlink: \"Shows the player's held or equipped item in chat.\"\ncarbon.crossserver: \"Allows cross server messages to be received by the player.\"\ncarbon.parties: \"Allows the creation and use of chat parties.\"\ncarbon.parties.ping_sound: \"Allows the ping sound to play when receiving party messages.\"\ncarbon.ping_sounds: \"Allows the ping sound to play when receiving pings.\"\ncarbon.mute: \"Mutes the player, preventing them from sending messages or whispers.\"\ncarbon.mute.exempt: \"Prevents the player from being muted.\"\ncarbon.mute.info: \"Shows if the player is muted or now.\"\ncarbon.mute.notify: \"Notifies the player when someone else has been mute.\"\ncarbon.mute.unmute: \"Unmutes the player, allowing them to use chat and send whispers.\"\ncarbon.nickname: \"Checks your nickname.\"\ncarbon.nickname.set: \"Set or remove your nickname.\"\ncarbon.nickname.others: \"Checks other player's nicknames.\"\ncarbon.nickname.others.set: \"Set or remove other player's nicknames.\"\ncarbon.reload: \"Reloads Carbon's config, channel settings, and translations.\"\ncarbon.whisper: \"Sends private messages to other players.\"\ncarbon.whisper.continue: \"Sends a message to the last player you whispered.\"\ncarbon.whisper.reply: \"Sends a message to the last player who messaged you.\"\ncarbon.whisper.vanished: \"Allows the player to send messages to vanished players.\"\ncarbon.whisper.ping_sounds: \"Allows the ping sound to play when receiving whispers.\"\n\n# Nickname tag permissions\ncarbon.nickname.tags.color: \"Allows the use of colors in nicknames.\"\ncarbon.nickname.tags.gradient: \"Allows the use of gradients in nicknames.\"\ncarbon.nickname.tags.rainbow: \"Allows the use of the rainbow tag in nicknames.\"\ncarbon.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.\"\ncarbon.nickname.tags.obfuscated: \"Allows the use of obfuscated in nicknames.\"\ncarbon.nickname.tags.bold: \"Allows the use of bold in nicknames.\"\ncarbon.nickname.tags.strikethrough: \"Allows the use of strikethrough in nicknames.\"\ncarbon.nickname.tags.underlined: \"Allows the use of underline in nicknames.\"\ncarbon.nickname.tags.italic: \"Allows the use of italics in nicknames.\"\ncarbon.nickname.tags.hover: \"Allows the use of hover events in nicknames.\"\ncarbon.nickname.tags.click: \"Allows the use of click events in nicknames.\"\ncarbon.nickname.tags.translatable: \"Allows the use of translatable components in nicknames.\"\ncarbon.nickname.tags.keybind: \"Allows the use of keybind components in nicknames.\"\ncarbon.nickname.tags.insertion: \"Allows the use of insertions in nicknames.\"\ncarbon.nickname.tags.font: \"Allows the use of fonts in nicknames.\"\ncarbon.nickname.tags.reset: \"Allows the use of the reset tag in nicknames.\"\ncarbon.nickname.tags.newline: \"Allows the use of the newline tag in nicknames.\"\ncarbon.nickname.tags.pride: \"Allows the use of the pride tag in nicknames.\"\ncarbon.nickname.tags.shadow_color: \"Allows the use of the shadow tag in nicknames.\"\ncarbon.nickname.tags.transition: \"Allows the use of the transition tag in nicknames.\"\n\n# Party name tag permissions\ncarbon.parties.name.tags.color: \"Allows the use of colors in party names.\"\ncarbon.parties.name.tags.gradient: \"Allows the use of gradients in party names.\"\ncarbon.parties.name.tags.rainbow: \"Allows the use of the rainbow tag in party names.\"\ncarbon.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.\"\ncarbon.parties.name.tags.obfuscated: \"Allows the use of obfuscated in party names.\"\ncarbon.parties.name.tags.bold: \"Allows the use of bold in party names.\"\ncarbon.parties.name.tags.strikethrough: \"Allows the use of strikethrough in party names.\"\ncarbon.parties.name.tags.underlined: \"Allows the use of underline in party names.\"\ncarbon.parties.name.tags.italic: \"Allows the use of italics in party names.\"\ncarbon.parties.name.tags.hover: \"Allows the use of hover events in party names.\"\ncarbon.parties.name.tags.click: \"Allows the use of click events in party names.\"\ncarbon.parties.name.tags.translatable: \"Allows the use of translatable components in party names.\"\ncarbon.parties.name.tags.keybind: \"Allows the use of keybind components in party names.\"\ncarbon.parties.name.tags.insertion: \"Allows the use of insertions in party names.\"\ncarbon.parties.name.tags.font: \"Allows the use of fonts in party names.\"\ncarbon.parties.name.tags.reset: \"Allows the use of the reset tag in party names.\"\ncarbon.parties.name.tags.newline: \"Allows the use of the newline tag in party names.\"\ncarbon.parties.name.tags.pride: \"Allows the use of the pride tag in party names.\"\ncarbon.parties.name.tags.shadow_color: \"Allows the use of the shadow tag in party names.\"\ncarbon.parties.name.tags.transition: \"Allows the use of the transition tag in party names.\"\n\n# Message tag permissions\ncarbon.messagetags.color: \"Allows the use of colors in messages.\"\ncarbon.messagetags.gradient: \"Allows the use of gradients in messages.\"\ncarbon.messagetags.rainbow: \"Allows the use of the rainbow tag in messages.\"\ncarbon.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.\"\ncarbon.messagetags.obfuscated: \"Allows the use of obfuscated in messages.\"\ncarbon.messagetags.bold: \"Allows the use of bold in messages.\"\ncarbon.messagetags.strikethrough: \"Allows the use of strikethrough in messages.\"\ncarbon.messagetags.underlined: \"Allows the use of underline in messages.\"\ncarbon.messagetags.italic: \"Allows the use of italics in messages.\"\ncarbon.messagetags.hover: \"Allows the use of hover events in messages.\"\ncarbon.messagetags.click: \"Allows the use of click events in messages.\"\ncarbon.messagetags.translatable: \"Allows the use of translatable components in messages.\"\ncarbon.messagetags.keybind: \"Allows the use of keybind components in messages.\"\ncarbon.messagetags.insertion: \"Allows the use of insertions in messages.\"\ncarbon.messagetags.font: \"Allows the use of fonts in messages.\"\ncarbon.messagetags.reset: \"Allows the use of the reset tag in messages.\"\ncarbon.messagetags.newline: \"Allows the use of the newline tag in messages.\"\ncarbon.messagetags.pride: \"Allows the use of the pride tag in messages.\"\ncarbon.messagetags.shadow_color: \"Allows the use of the shadow tag in messages.\"\ncarbon.messagetags.transition: \"Allows the use of the transition tag in messages.\"\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-de_AT.properties",
    "content": "channel.change=<green>Du schreibst nun in </green><channel>\ncommand.clearchat.description=Löscht den Chat für alle Spieler.\ncommand.continue.argument.message=Die zu sendende Nachricht.\ncommand.continue.description=Sendet eine Nachricht an die letzte Person, der du geschrieben hast.\ncommand.debug.argument.player=Der Spieler, von dem die Gruppen überprüft werden.\ncommand.debug.description=Zeigt die Berechtigungsgruppen der Spieler an.\ncommand.filter.optional.enabled=<green>Optionaler Chatfilter aktiviert\\!\ncommand.filter.optional.disabled=<red>Optionaler Chatfilter deaktiviert\\!\ncommand.filter.optional.description=Schaltet den optionalen Chatfilter um.\ncommand.help.argument.query=Die Suchanfrage.\ncommand.help.description=Carbon Befehlsliste.\ncommand.help.misc.arguments=Argumente\ncommand.help.misc.available_commands=Verfügbare Befehle\ncommand.help.misc.click_for_next_page=Klicken für die nächste Seite\ncommand.help.misc.click_for_previous_page=Klicken für die vorherige Seite\ncommand.help.misc.click_to_show_help=Klicken, um Hilfe für diesen Befehl anzuzeigen\ncommand.help.misc.command=Befehl\ncommand.help.misc.description=Beschreibung\ncommand.help.misc.help=Hilfe\ncommand.help.misc.no_description=Keine Beschreibung\ncommand.help.misc.no_results_for_query=Keine Ergebnisse für die Abfrage\ncommand.help.misc.optional=Optional\ncommand.help.misc.page_out_of_range=Error\\: Seitn <page> is ned in Reichweitn. Muss in da Reichweitn [1, <max_pages>] sei\ncommand.help.misc.showing_results_for_query=Suchergebnisse für die Abfrage anzeigen\ncommand.ignore.argument.player=Der Name des zu ignorierenden Spielers.\ncommand.ignore.argument.uuid=Die UUID des zu ignorierenden Spielers.\ncommand.ignore.description=Versteckt alle eingehenden Nachrichten von ignorierten Spielern.\ncommand.ignorelist.description=Zeigt eine paginierte Liste mit Spielern an, welche du ignorierst.\ncommand.ignorelist.none_ignored=<green>Du ignorierst keine Spieler.\ncommand.ignorelist.pagination_header=<bold>Ignorierte Spieler\ncommand.ignorelist.pagination_element= - <display_name> <click\\:run_command\\:''/unignore <username>''><hover\\:show_text\\:''Click to unignore <username>''><gray>[<white>nicht mehr ignorieren</white>]</gray>\ncommand.join.description=Trete einem Kanal bei, den du zuvor verlassen hast.\ncommand.leave.description=Verlasse einen Kanal, auf den du derzeit Zugriff hast.\ncommand.mute.argument.player=Der Name des Spielers, der stummgeschaltet werden soll.\ncommand.mute.argument.uuid=Die UUID des Players, der stummgeschaltet werden soll.\ncommand.mute.argument.duration=Die Dauer, für die der Spieler stummgeschaltet wird.\ncommand.mute.description=Schaltet Spieler stumm, sodass sie weder den Chat nutzen noch anderen Spielern Flüsternachrichten senden können.\ncommand.muteinfo.argument.player=Der Name des Spielers.\ncommand.muteinfo.argument.uuid=Die UUID des Spielers.\ncommand.muteinfo.description=Zeigt an, ob Spieler stumm sind oder nicht.\ncommand.nickname.argument.nickname=Der zu setzende Nickname.\ncommand.nickname.argument.player=Der Name des Zielspielers.\ncommand.nickname.description=Zeigt deinen Nickname an.\ncommand.nickname.set.description=Legt deinen Nickname fest.\ncommand.nickname.reset.description=Entfernt deinen Nickname.\ncommand.nickname.others.description=Zeigt Nicknamen des Spielers.\ncommand.nickname.others.set.description=Legt Nicknamen des Spielers fest.\ncommand.nickname.others.reset.description=Entfernt jeden eingestellten Nickname vom Ziel.\ncommand.reload.description=Lädt Carbons Konfiguration, Kanaleinstellungen und Übersetzungen neu. Lädt und entlädt keine Kanäle.\ncommand.reply.argument.message=Die Nachricht, mit welcher geantwortet wird.\ncommand.reply.description=Sendet eine Nachricht an die letzte Person, der du geschrieben hast.\ncommand.togglemsg.description=Erlaubt und verbietet anderen Spielern, dir Nachrichten zu schreiben.\ncommand.unignore.argument.player=Der Name des Spielers, der nicht mehr ignoriert werden soll.\ncommand.unignore.argument.uuid=Die UUID des Spielers, der nicht mehr ignoriert werden soll.\ncommand.unignore.description=Beendet das Verstecken von Nachrichten des angegebenen Spielers.\ncommand.unmute.argument.player=Der Name des Spielers, dessen Stummschaltung aufgehoben werden soll.\ncommand.unmute.argument.uuid=Die UUID des Spielers, dessen Stummschaltung aufgehoben werden soll.\ncommand.unmute.description=Deaktiviert die Stummschaltung der Spieler, sodass sie den Chat nutzen und anderen Spielern Flüsternachrichten senden können.\ncommand.updateusername.argument.player=Der Name des zu aktualisierenden Spielers.\ncommand.updateusername.argument.uuid=Die UUID des zu aktualisierenden Spielers.\ncommand.updateusername.description=Aktualisiert den Benutzernamen des Spielers auf ihren Mojang-Namen.\ncommand.updateusername.fetching=Lade Benutzername...\ncommand.updateusername.notupdated=Benutzername konnte nicht abgerufen werden.\ncommand.updateusername.updated=Der Benutzername von <newname> wurde aktualisiert.\ncommand.whisper.argument.message=Die zu sendende Nachricht.\ncommand.whisper.argument.player=Der Name des Spielers, dem die Nachricht gesendet werden soll.\ncommand.whisper.description=Sendet eine private Nachricht an den angegebenen Spieler.\ncommand.party.pagination_header=<green>Partymitglieder</green>\\:\ncommand.party.pagination_element=<online\\:''<green>''\\:''<gray>''> -<reset> <display_name>\ncommand.party.created=<green>Die Party \"</green><party_name><green>\" wurde erfolgreich erstellt und du bist ihr beigetreten\\!\ncommand.party.not_in_party=<red>Du bist nicht in einer Party. Benutze ''/party create'', um eine zu erstellen, oder ''/party accept'' um eine Einladung anzunehmen.\ncommand.party.current_party=<green>Du bist in der Party<white>\\:</white></green> <party_name>\ncommand.party.must_leave_current_first=<red>Du musst zuerst deine aktuelle Party verlassen.\ncommand.party.name_too_long=<red>Der Name der Party ist zu lang.\ncommand.party.received_invite=<hover\\:show_text\\:''<green>Zum Akzeptieren anklicken<click\\:run_command\\:''/party accept <sender_username>''><green>Du wurdest von </green><sender_display_name><green> zur Party \"</green><party_name><green>\" eingeladen. Klicke auf diese Nachricht, um zu akzeptieren.\ncommand.party.sent_invite=<green>Einladung zur Party an </green><recipient_display_name><green> gesendet.\ncommand.party.must_specify_invite=<red>Du musst angeben, wessen Partyeinladung angenommen werden soll.\ncommand.party.no_pending_invites=<red>Du hast keine ausstehenden Partyeinladungen.\ncommand.party.no_invite_from=<red>Du hast keine ausstehende Partyeinladung von </red><sender_display_name><red>.\ncommand.party.joined_party=<green>Erfolgreich der Party \"</green><party_name><green>\" beigetreten\\!\ncommand.party.left_party=<green>Erfolgreich die Party \"</green><party_name><green>\" verlassen.\ncommand.party.disbanded=<green>Die Party \"</green><party_name><green>\" wurde erfolgreich aufgelöst.\ncommand.party.cannot_disband_multiple_members=<red>Die Party \"</red><party_name><red>\" kann nicht aufgelöst werden, da du nicht das letzte Mitglied bist.\ncommand.party.must_be_in_party=<red>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.\ncommand.party.cannot_invite_self=<red>Du kannst dich nicht selbst einladen.\ncommand.party.description=Erhalte Informationen und sehe die Mitglieder deiner aktuellen Party.\ncommand.party.create.description=Eine neue Party erstellen.\ncommand.party.invite.description=Lade einen Spieler in deine Party ein.\ncommand.party.accept.description=Einladungen für Partys akzeptieren.\ncommand.party.leave.description=Verlasse deine aktuelle Party.\ncommand.party.already_in_party=<display_name><red> ist bereits in deiner Party.\ncommand.party.disband.description=Löse deine aktuelle Party auf.\ncommand.realname.description=Zeigt den echten Namen des Spielers an.\ncommand.realname.argument.player=Der Anzeigename des Spielers.\ncommand.spy.enabled=<green>Spionage ist jetzt aktiviert.\ncommand.spy.disabled=<red>Spionage ist jetzt deaktiviert.\ncommand.spy.description=Ermöglicht es einem Spieler, alle privaten und Kanalnachrichten zu sehen, die er sonst nicht sehen würde.\nduration.days=<days>d<hours>h<minutes>m<seconds>s\nduration.hours=<hours>h<minutes>m<seconds>s\nparty.player_joined=<display_name><green> ist deiner Party beigetreten.\nparty.player_left=<display_name><green> hat deine Party verlassen.\nparty.cannot_use_channel=<red>Du musst einer Party beitreten, um diesen Kanal zu nutzen.\nparty.spy=<red>Spy <party_name> <red>[<username>\\: <white><message><red>]\nconfig.reload.failed=<red>Konfiguration konnte nicht neu geladen werden\nconfig.reload.success=<green>Konfiguration erfolgreich neu geladen\nerror.command.argument_parsing=<red>Ungültiges Befehlsargument\\: <gray><throwable_message>\nerror.command.command_execution=<hover\\:show_text\\:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    Klicken zum Kopieren\"><click\\:copy_to_clipboard\\:''<stacktrace>''><red>Bei der Ausführung dieses Befehls ist ein interner Fehler aufgetreten.\nerror.command.invalid_player=Kein Spieler für die Eingabe \"<input>\" gefunden\nerror.command.invalid_sender=<red>Ungültiger Befehlsabsender. Du musst vom Typ <gray><sender_type> sein\nerror.command.invalid_syntax=<red>Ungültige Befehlssyntax. Der korrekte Befehlssyntax ist\\: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>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.\nerror.command.command_needs_player=<red>Nicht-Spieler müssen das Spielerargument angeben, um diesen Befehl auszuführen.\nignore.already_ignored=<red>Du ignorierst bereits <target>\nignore.not_ignored=<red>Du ignorierst <target> nicht\nignore.exempt=<red>Du kannst <target> nicht ignorieren\nignore.invalid_target=<red>Kein Ziel gefunden\nignore.now_ignoring=<green>Du ignorierst jetzt <target>\nignore.no_longer_ignoring=<green>Du ignorierst nicht mehr <target>\nmute.alert.players=<red><target> <red>wurde stummgeschaltet\nmute.alert.players.temp=<red><target> <red>wurde für <duration> stummgeschaltet\nmute.alert.target=<red>Du wurdest stummgeschaltet\nmute.alert.target.temp=<red>Du wurdest für <duration> stummgeschaltet\nmute.cannot_speak=<red>Du kannst nicht sprechen, während du stummgeschaltet bist\nmute.exempt=<red>Dieser Spieler darf nicht stummgeschaltet werden\nmute.info.muted=<red><target> <red>ist stummgeschaltet\nmute.info.muted.duration=<red><target> <red>ist für <duration> <red>stummgeschaltet\nmute.info.not_muted=<red><target> <gold>ist nicht stummgeschaltet\nmute.info.self.muted=<red>Du bist stummgeschaltet\nmute.info.self.not_muted=<green>Du bist nicht stummgeschaltet\nmute.no_target=<red>Kein Spieler zum Stummschalten angegeben.\nmute.spy.prefix=<red><hover\\:show_text\\:''<red>Stummgeschaltet</red>''>M</hover></red>\nmute.unmute.alert.players=<green><target> <green> ist nicht mehr stummgeschaltet.\nmute.unmute.alert.target=<green>Du bist nicht mehr stummgeschaltet.\nmute.unmute.no_target=<red>Kein Spieler zum Entstummen angegeben.\nnickname.reset.others=<gold>Der Nickname von \"<green><target></green>\"<gold> wurde zurückgesetzt.\nnickname.reset=<gold>Dein Nickname wurde zurückgesetzt\nnickname.set.others=<green>Du hast den Nickname von </green><target><green> auf </green><nickname> <green>gesetzt\nnickname.set=<green>Dein Nickname wurde auf </green><nickname> <green>gesetzt\nnickname.show.others.unset=<target><red> hat keinen Nickname gesetzt\nnickname.show.others=<green>Der Nickname von </green><target><green> lautet </green><nickname>\nnickname.show.unset=<red>Du hast keinen Nickname gesetzt\nnickname.show=<green>Dein Nickname ist </green><nickname>\nnickname.error.character_limit=<red>Der Nickname \"<nickname>\" hat das Zeichenlimit überschritten. Er muss auf <min_length>~<max_length> Zeichen gesetzt werden.\nnickname.error.blacklist=<red>Nickname \"<nickname>\" ist nicht erlaubt. Bitte wähle einen anderen Namen.\nnickname.error.filter=<red>Nicknamen müssen alphanumerisch sein\\!\nnickname.realname=<target>s echter Name ist <username>\nreply.target.missing=<red>Du hast niemanden zum Antworten\nreply.target.self=<red>Du kannst dir nicht selbst zuflüstern\nwhisper.console=<gold>[<green><sender_display_name></green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.continue.target_missing=<red>Du hast niemanden zum Flüstern\nwhisper.error=<red>Fehler beim Senden der privaten Nachricht\nwhisper.from=<click\\:suggest_command\\:''/whisper <sender_username> ''><hover\\:show_text\\:''Klicken, um eine Antwort zu beginnen''><gold>[<green><sender_display_name></green>] -> [<green>Du</green>] <message>\nwhisper.from.spy=<red>SPY [</red><green><sender_display_name></green>]<red> -> [</red><green><recipient_display_name></green><red>]</red> <message>\nwhisper.ignored_by_target=<red><target> <red>ignoriert dich\nwhisper.ignoring_target=<red>Du ignorierst <target>\nwhisper.ignoring_all=<red>Du kannst keine Nachrichten senden, solange sie ignoriert werden\\!\nwhisper.no_permission.receive=<red>Dieser Spieler hat keine Berechtigung, Nachrichten zu erhalten\\!\nwhisper.no_permission.send=<red>Du hast keine Berechtigung, Flüsternachrichten zu senden\\!\nwhisper.to=<click\\:suggest_command\\:''/whisper <recipient_username> ''><hover\\:show_text\\:''Klicke, um eine weitere Nachticht an  <recipient_display_name> zu senden''><gold>[<green>Du</green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.toggled.on=Private Nachrichten werden nun empfangen.\nwhisper.toggled.off=Private Nachrichten werden nun nicht mehr empfangen.\nchannel.cooldown=<red>Du kannst den Chat in <remaining> Sekunden wieder verwenden\\!\nchannel.radius.empty_recipients=<red>Du bist nicht nah genug an jemanden, um eine Nachricht zu senden\nchannel.radius.spy=<red>Spy [<username>\\: <white><message><red>]\nchannel.joined=<green>Du bist dem Kanal wieder beigetreten</green>\nchannel.left=<red>Du hast den Kanal verlassen</red>\nchannel.no_permission=<red>Du hast keine Berechtigung, um diesen Kanal zu verwenden</red>\nchannel.already_left=<red>Du hast diesen Kanal bereits verlassen</red>\nchannel.not_left=<red>Du hast diesen Kanal nicht verlassen</red>\nchannel.not_found=<red>Kanal nicht gefunden</red>\npagination.page_out_of_range=<red>Seite <page> ist außerhalb des Bereichs\\! Es gibt nur <pages> Seiten.\npagination.click_for_next_page=Klicken für die nächste Seite\npagination.click_for_previous_page=Klicken für die vorherige Seite\npagination.footer=<gray>Seite <page><white>/</white><pages> <aqua><buttons>\nintegrations.towny.cannot_use_alliance_channel=<red>Du musst einer Allianz beitreten, um diesen Kanal zu nutzen.\nintegrations.towny.cannot_use_nation_channel=<red>Du musst einer Nation beitreten, um diesen Kanal zu nutzen.\nintegrations.towny.cannot_use_town_channel=<red>Du musst einer Stadt beitreten, um diesen Kanal nutzen zu können.\nintegrations.mcmmo.cannot_use_party_channel=<red>Du musst einer mcMMO Party beitreten, um diesen Kanal nutzen zu können.\nintegrations.fuuid.cannot_use_faction_channel=<red>Du musst einer Fraktion beitreten, um diesen Kanal nutzen zu können.\nintegrations.fuuid.cannot_use_alliance_channel=<red>Du musst einer Allianz beitreten, um diesen Kanal zu nutzen.\nintegrations.fuuid.cannot_use_truce_channel=<red>Du musst einen Waffenstillstand mit einer anderen Fraktion haben, um diesen Kanal nutzen zu können.\nintegrations.fuuid.cannot_use_mod_channel=<red>Du musst ein Fraktionsmod/Admin sein, um diesen Kanal nutzen zu können.\nintegrations.plotsquared.cannot_use_plot_channel=<red>Du musst in einem Grundstück sein, um diesen Kanal nutzen zu können.\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-de_CH.properties",
    "content": ""
  },
  {
    "path": "common/src/main/resources/locale/messages-de_DE.properties",
    "content": "channel.change=<green>Du schreibst nun in </green><channel>\ncommand.clearchat.description=Löscht den Chat für alle Spieler.\ncommand.continue.argument.message=Die zu sendende Nachricht.\ncommand.continue.description=Sendet eine Nachricht an die letzte Person, der du geschrieben hast.\ncommand.debug.argument.player=Der Spieler, von dem die Gruppen überprüft werden.\ncommand.debug.description=Zeigt die Berechtigungsgruppen der Spieler an.\ncommand.filter.optional.enabled=<green>Optionaler Chatfilter aktiviert\\!\ncommand.filter.optional.disabled=<red>Optionaler Chatfilter deaktiviert\\!\ncommand.filter.optional.description=Schaltet den optionalen Chatfilter um.\ncommand.help.argument.query=Die Suchanfrage.\ncommand.help.description=Carbon Befehlsliste.\ncommand.help.misc.arguments=Argumente\ncommand.help.misc.available_commands=Verfügbare Befehle\ncommand.help.misc.click_for_next_page=Klicken für die nächste Seite\ncommand.help.misc.click_for_previous_page=Klicken für die vorherige Seite\ncommand.help.misc.click_to_show_help=Klicken, um Hilfe für diesen Befehl anzuzeigen\ncommand.help.misc.command=Befehl\ncommand.help.misc.description=Beschreibung\ncommand.help.misc.help=Hilfe\ncommand.help.misc.no_description=Keine Beschreibung\ncommand.help.misc.no_results_for_query=Keine Ergebnisse für die Abfrage\ncommand.help.misc.optional=Optional\ncommand.help.misc.page_out_of_range=Fehler\\: Seite <page> ist nicht im Bereich. Muss im Bereich [1, <max_pages>] sein\ncommand.help.misc.showing_results_for_query=Suchergebnisse für die Abfrage anzeigen\ncommand.ignore.argument.player=Der Name des zu ignorierenden Spielers.\ncommand.ignore.argument.uuid=Die UUID des zu ignorierenden Spielers.\ncommand.ignore.description=Versteckt alle eingehenden Nachrichten von ignorierten Spielern.\ncommand.ignorelist.description=Zeigt eine paginierte Liste mit Spielern an, welche du ignorierst.\ncommand.ignorelist.none_ignored=<green>Du ignorierst keine Spieler.\ncommand.ignorelist.pagination_header=<bold>Ignorierte Spieler\ncommand.ignorelist.pagination_element= - <display_name> <click\\:run_command\\:''/unignore <username>''><hover\\:show_text\\:''Click to unignore <username>''><gray>[<white>nicht mehr ignorieren</white>]</gray>\ncommand.join.description=Trete einem Kanal bei, den du zuvor verlassen hast.\ncommand.leave.description=Verlasse einen Kanal, auf den du derzeit Zugriff hast.\ncommand.mute.argument.player=Der Name des Spielers, der stummgeschaltet werden soll.\ncommand.mute.argument.uuid=Die UUID des Players, der stummgeschaltet werden soll.\ncommand.mute.argument.duration=Die Dauer, für die der Spieler stummgeschaltet wird.\ncommand.mute.description=Schaltet Spieler stumm, sodass sie weder den Chat nutzen noch anderen Spielern Flüsternachrichten senden können.\ncommand.muteinfo.argument.player=Der Name des Spielers.\ncommand.muteinfo.argument.uuid=Die UUID des Spielers.\ncommand.muteinfo.description=Zeigt an, ob Spieler stumm sind oder nicht.\ncommand.nickname.argument.nickname=Der zu setzende Nickname.\ncommand.nickname.argument.player=Der Name des Zielspielers.\ncommand.nickname.description=Zeigt deinen Nickname an.\ncommand.nickname.set.description=Legt deinen Nickname fest.\ncommand.nickname.reset.description=Entfernt deinen Nickname.\ncommand.nickname.others.description=Zeigt Nicknamen des Spielers.\ncommand.nickname.others.set.description=Legt Nicknamen des Spielers fest.\ncommand.nickname.others.reset.description=Entfernt jeden eingestellten Nickname vom Ziel.\ncommand.reload.description=Lädt Carbons Konfiguration, Kanaleinstellungen und Übersetzungen neu. Lädt und entlädt keine Kanäle.\ncommand.reply.argument.message=Die Nachricht, mit welcher geantwortet wird.\ncommand.reply.description=Sendet eine Nachricht an die letzte Person, der du geschrieben hast.\ncommand.togglemsg.description=Erlaubt und verbietet anderen Spielern, dir Nachrichten zu schreiben.\ncommand.unignore.argument.player=Der Name des Spielers, der nicht mehr ignoriert werden soll.\ncommand.unignore.argument.uuid=Die UUID des Spielers, der nicht mehr ignoriert werden soll.\ncommand.unignore.description=Beendet das Verstecken von Nachrichten des angegebenen Spielers.\ncommand.unmute.argument.player=Der Name des Spielers, dessen Stummschaltung aufgehoben werden soll.\ncommand.unmute.argument.uuid=Die UUID des Spielers, dessen Stummschaltung aufgehoben werden soll.\ncommand.unmute.description=Deaktiviert die Stummschaltung der Spieler, sodass sie den Chat nutzen und anderen Spielern Flüsternachrichten senden können.\ncommand.updateusername.argument.player=Der Name des zu aktualisierenden Spielers.\ncommand.updateusername.argument.uuid=Die UUID des zu aktualisierenden Spielers.\ncommand.updateusername.description=Aktualisiert den Benutzernamen des Spielers auf ihren Mojang-Namen.\ncommand.updateusername.fetching=Lade Benutzername...\ncommand.updateusername.notupdated=Benutzername konnte nicht abgerufen werden.\ncommand.updateusername.updated=Der Benutzername von <newname> wurde aktualisiert.\ncommand.whisper.argument.message=Die zu sendende Nachricht.\ncommand.whisper.argument.player=Der Name des Spielers, dem die Nachricht gesendet werden soll.\ncommand.whisper.description=Sendet eine private Nachricht an den angegebenen Spieler.\ncommand.party.pagination_header=<green>Partymitglieder</green>\\:\ncommand.party.pagination_element=<online\\:''<green>''\\:''<gray>''> -<reset> <display_name>\ncommand.party.created=<green>Die Party \"</green><party_name><green>\" wurde erfolgreich erstellt und du bist ihr beigetreten\\!\ncommand.party.not_in_party=<red>Du bist nicht in einer Party. Benutze ''/party create'', um eine zu erstellen, oder ''/party accept'' um eine Einladung anzunehmen.\ncommand.party.current_party=<green>Du bist in der Party<white>\\:</white></green> <party_name>\ncommand.party.must_leave_current_first=<red>Du musst zuerst deine aktuelle Party verlassen.\ncommand.party.name_too_long=<red>Der Name der Party ist zu lang.\ncommand.party.received_invite=<hover\\:show_text\\:''<green>Zum Akzeptieren anklicken<click\\:run_command\\:''/party accept <sender_username>''><green>Du wurdest von </green><sender_display_name><green> zur Party \"</green><party_name><green>\" eingeladen. Klicke auf diese Nachricht, um zu akzeptieren.\ncommand.party.sent_invite=<green>Einladung zur Party an </green><recipient_display_name><green> gesendet.\ncommand.party.must_specify_invite=<red>Du musst angeben, wessen Partyeinladung angenommen werden soll.\ncommand.party.no_pending_invites=<red>Du hast keine ausstehenden Partyeinladungen.\ncommand.party.no_invite_from=<red>Du hast keine ausstehende Partyeinladung von </red><sender_display_name><red>.\ncommand.party.joined_party=<green>Erfolgreich der Party \"</green><party_name><green>\" beigetreten\\!\ncommand.party.left_party=<green>Erfolgreich die Party \"</green><party_name><green>\" verlassen.\ncommand.party.disbanded=<green>Die Party \"</green><party_name><green>\" wurde erfolgreich aufgelöst.\ncommand.party.cannot_disband_multiple_members=<red>Die Party \"</red><party_name><red>\" kann nicht aufgelöst werden, da du nicht das letzte Mitglied bist.\ncommand.party.must_be_in_party=<red>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.\ncommand.party.cannot_invite_self=<red>Du kannst dich nicht selbst einladen.\ncommand.party.description=Erhalte Informationen und sehe die Mitglieder deiner aktuellen Party.\ncommand.party.create.description=Eine neue Party erstellen.\ncommand.party.invite.description=Lade einen Spieler in deine Party ein.\ncommand.party.accept.description=Einladungen für Partys akzeptieren.\ncommand.party.leave.description=Verlasse deine aktuelle Party.\ncommand.party.already_in_party=<display_name><red> ist bereits in deiner Party.\ncommand.party.disband.description=Löse deine aktuelle Party auf.\ncommand.realname.description=Zeigt den echten Namen des Spielers an.\ncommand.realname.argument.player=Der Anzeigename des Spielers.\ncommand.spy.enabled=<green>Spionage ist jetzt aktiviert.\ncommand.spy.disabled=<red>Spionage ist jetzt deaktiviert.\ncommand.spy.description=Ermöglicht es einem Spieler, alle privaten und Kanalnachrichten zu sehen, die er sonst nicht sehen würde.\nduration.days=<days>d<hours>h<minutes>m<seconds>s\nduration.hours=<hours>h<minutes>m<seconds>s\nparty.player_joined=<display_name><green> ist deiner Party beigetreten.\nparty.player_left=<display_name><green> hat deine Party verlassen.\nparty.cannot_use_channel=<red>Du musst einer Party beitreten, um diesen Kanal zu nutzen.\nparty.spy=<red>Spy <party_name> <red>[<username>\\: <white><message><red>]\nconfig.reload.failed=<red>Konfiguration konnte nicht neu geladen werden\nconfig.reload.success=<green>Konfiguration erfolgreich neu geladen\nerror.command.argument_parsing=<red>Ungültiges Befehlsargument\\: <gray><throwable_message>\nerror.command.command_execution=<hover\\:show_text\\:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    Klicken zum Kopieren\"><click\\:copy_to_clipboard\\:''<stacktrace>''><red>Bei der Ausführung dieses Befehls ist ein interner Fehler aufgetreten.\nerror.command.invalid_player=Kein Spieler für die Eingabe \"<input>\" gefunden\nerror.command.invalid_sender=<red>Ungültiger Befehlsabsender. Du musst vom Typ <gray><sender_type> sein\nerror.command.invalid_syntax=<red>Ungültige Befehlssyntax. Der korrekte Befehlssyntax ist\\: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>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.\nerror.command.command_needs_player=<red>Nicht-Spieler müssen das Spielerargument angeben, um diesen Befehl auszuführen.\nignore.already_ignored=<red>Du ignorierst bereits <target>\nignore.not_ignored=<red>Du ignorierst <target> nicht\nignore.exempt=<red>Du kannst <target> nicht ignorieren\nignore.invalid_target=<red>Kein Ziel gefunden\nignore.now_ignoring=<green>Du ignorierst jetzt <target>\nignore.no_longer_ignoring=<green>Du ignorierst nicht mehr <target>\nmute.alert.players=<red><target> <red>wurde stummgeschaltet\nmute.alert.players.temp=<red><target> <red>wurde für <duration> stummgeschaltet\nmute.alert.target=<red>Du wurdest stummgeschaltet\nmute.alert.target.temp=<red>Du wurdest für <duration> stummgeschaltet\nmute.cannot_speak=<red>Du kannst nicht sprechen, während du stummgeschaltet bist\nmute.exempt=<red>Dieser Spieler darf nicht stummgeschaltet werden\nmute.info.muted=<red><target> <red>ist stummgeschaltet\nmute.info.muted.duration=<red><target> <red>ist für <duration> <red>stummgeschaltet\nmute.info.not_muted=<red><target> <gold>ist nicht stummgeschaltet\nmute.info.self.muted=<red>Du bist stummgeschaltet\nmute.info.self.not_muted=<green>Du bist nicht stummgeschaltet\nmute.no_target=<red>Kein Spieler zum Stummschalten angegeben.\nmute.spy.prefix=<red><hover\\:show_text\\:''<red>Stummgeschaltet</red>''>M</hover></red>\nmute.unmute.alert.players=<green><target> <green> ist nicht mehr stummgeschaltet.\nmute.unmute.alert.target=<green>Du bist nicht mehr stummgeschaltet.\nmute.unmute.no_target=<red>Kein Spieler zum Entstummen angegeben.\nnickname.reset.others=<gold>Der Nickname von \"<green><target></green>\"<gold> wurde zurückgesetzt.\nnickname.reset=<gold>Dein Nickname wurde zurückgesetzt\nnickname.set.others=<green>Du hast den Nickname von </green><target><green> auf </green><nickname> <green>gesetzt\nnickname.set=<green>Dein Nickname wurde auf </green><nickname> <green>gesetzt\nnickname.show.others.unset=<target><red> hat keinen Nickname gesetzt\nnickname.show.others=<green>Der Nickname von </green><target><green> lautet </green><nickname>\nnickname.show.unset=<red>Du hast keinen Nickname gesetzt\nnickname.show=<green>Dein Nickname ist </green><nickname>\nnickname.error.character_limit=<red>Der Nickname \"<nickname>\" hat das Zeichenlimit überschritten. Er muss auf <min_length>~<max_length> Zeichen gesetzt werden.\nnickname.error.blacklist=<red>Nickname \"<nickname>\" ist nicht erlaubt. Bitte wähle einen anderen Namen.\nnickname.error.filter=<red>Nicknamen müssen alphanumerisch sein\\!\nnickname.realname=<target>s echter Name ist <username>\nreply.target.missing=<red>Du hast niemanden zum Antworten\nreply.target.self=<red>Du kannst dir nicht selbst zuflüstern\nwhisper.console=<gold>[<green><sender_display_name></green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.continue.target_missing=<red>Du hast niemanden zum Flüstern\nwhisper.error=<red>Fehler beim Senden der privaten Nachricht\nwhisper.from=<click\\:suggest_command\\:''/whisper <sender_username> ''><hover\\:show_text\\:''Klicken, um eine Antwort zu beginnen''><gold>[<green><sender_display_name></green>] -> [<green>Du</green>] <message>\nwhisper.from.spy=<red>SPY [</red><green><sender_display_name></green>]<red> -> [</red><green><recipient_display_name></green><red>]</red> <message>\nwhisper.ignored_by_target=<red><target> <red>ignoriert dich\nwhisper.ignoring_target=<red>Du ignorierst <target>\nwhisper.ignoring_all=<red>Du kannst keine Nachrichten senden, solange sie ignoriert werden\\!\nwhisper.no_permission.receive=<red>Dieser Spieler hat keine Berechtigung, Nachrichten zu erhalten\\!\nwhisper.no_permission.send=<red>Du hast keine Berechtigung, Flüsternachrichten zu senden\\!\nwhisper.to=<click\\:suggest_command\\:''/whisper <recipient_username> ''><hover\\:show_text\\:''Klicke, um eine weitere Nachticht an  <recipient_display_name> zu senden''><gold>[<green>Du</green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.toggled.on=Private Nachrichten werden nun empfangen.\nwhisper.toggled.off=Private Nachrichten werden nun nicht mehr empfangen.\nchannel.cooldown=<red>Du kannst den Chat in <remaining> Sekunden wieder verwenden\\!\nchannel.radius.empty_recipients=<red>Du bist nicht nah genug an jemanden, um eine Nachricht zu senden\nchannel.radius.spy=<red>Spy [<username>\\: <white><message><red>]\nchannel.joined=<green>Du bist dem Kanal wieder beigetreten</green>\nchannel.left=<red>Du hast den Kanal verlassen</red>\nchannel.no_permission=<red>Du hast keine Berechtigung, um diesen Kanal zu verwenden</red>\nchannel.already_left=<red>Du hast diesen Kanal bereits verlassen</red>\nchannel.not_left=<red>Du hast diesen Kanal nicht verlassen</red>\nchannel.not_found=<red>Kanal nicht gefunden</red>\npagination.page_out_of_range=<red>Seite <page> ist außerhalb des Bereichs\\! Es gibt nur <pages> Seiten.\npagination.click_for_next_page=Klicken für die nächste Seite\npagination.click_for_previous_page=Klicken für die vorherige Seite\npagination.footer=<gray>Seite <page><white>/</white><pages> <aqua><buttons>\nintegrations.towny.cannot_use_alliance_channel=<red>Du musst einer Allianz beitreten, um diesen Kanal zu nutzen.\nintegrations.towny.cannot_use_nation_channel=<red>Du musst einer Nation beitreten, um diesen Kanal zu nutzen.\nintegrations.towny.cannot_use_town_channel=<red>Du musst einer Stadt beitreten, um diesen Kanal nutzen zu können.\nintegrations.mcmmo.cannot_use_party_channel=<red>Du musst einer mcMMO Party beitreten, um diesen Kanal nutzen zu können.\nintegrations.fuuid.cannot_use_faction_channel=<red>Du musst einer Fraktion beitreten, um diesen Kanal nutzen zu können.\nintegrations.fuuid.cannot_use_alliance_channel=<red>Du musst einer Allianz beitreten, um diesen Kanal zu nutzen.\nintegrations.fuuid.cannot_use_truce_channel=<red>Du musst einen Waffenstillstand mit einer anderen Fraktion haben, um diesen Kanal nutzen zu können.\nintegrations.fuuid.cannot_use_mod_channel=<red>Du musst ein Fraktionsmod/Admin sein, um diesen Kanal nutzen zu können.\nintegrations.plotsquared.cannot_use_plot_channel=<red>Du musst in einem Grundstück sein, um diesen Kanal nutzen zu können.\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-en_US.properties",
    "content": "channel.change=<green>You are now messaging </green><channel>\ncommand.clearchat.description=Clears the chat window for all players.\ncommand.continue.argument.message=The message to send.\ncommand.continue.description=Sends a message to the last person you messaged.\ncommand.debug.argument.player=The player to check the groups of.\ncommand.debug.description=Shows the permission groups of players.\ncommand.filter.optional.enabled=<green>Optional chat filter enabled!\ncommand.filter.optional.disabled=<red>Optional chat filter disabled!\ncommand.filter.optional.description=Toggles the optional chat filter.\ncommand.help.argument.query=The search query.\ncommand.help.description=Carbon command list.\ncommand.help.misc.arguments=Arguments\ncommand.help.misc.available_commands=Available Commands\ncommand.help.misc.click_for_next_page=Click for next page\ncommand.help.misc.click_for_previous_page=Click for previous page\ncommand.help.misc.click_to_show_help=Click to show help for this command\ncommand.help.misc.command=Command\ncommand.help.misc.description=Description\ncommand.help.misc.help=Help\ncommand.help.misc.no_description=No description\ncommand.help.misc.no_results_for_query=No results for query\ncommand.help.misc.optional=Optional\ncommand.help.misc.page_out_of_range=Error: Page <page> is not in range. Must be in range [1, <max_pages>]\ncommand.help.misc.showing_results_for_query=Showing search results for query\ncommand.ignore.argument.player=The name of the player to ignore.\ncommand.ignore.argument.uuid=The UUID of the player to ignore.\ncommand.ignore.description=Hides all incoming messages from ignored players.\ncommand.ignorelist.description=Displays a paginated list of who you are ignoring.\ncommand.ignorelist.none_ignored=<green>You are not ignoring any players.\ncommand.ignorelist.pagination_header=<bold>Ignored players\ncommand.ignorelist.pagination_element= - <display_name> <click:run_command:'/unignore <username>'><hover:show_text:'Click to unignore <username>'><gray>[<white>unignore</white>]</gray>\ncommand.join.description=Join a channel you have previously left.\ncommand.leave.description=Leave a channel that you currently have access to.\ncommand.mute.argument.player=The name of the player to mute.\ncommand.mute.argument.uuid=The UUID of the player to mute.\ncommand.mute.argument.duration=The duration the player will be muted for.\ncommand.mute.description=Mutes players, preventing them from using chat or whispering other players.\ncommand.muteinfo.argument.player=The name of the player.\ncommand.muteinfo.argument.uuid=The UUID of the player.\ncommand.muteinfo.description=Shows if players are muted or not.\ncommand.nickname.argument.nickname=The nickname to set.\ncommand.nickname.argument.player=The name of the target player.\ncommand.nickname.description=Shows your nickname.\ncommand.nickname.set.description=Sets your nickname.\ncommand.nickname.reset.description=Removes your nickname.\ncommand.nickname.others.description=Shows player nicknames.\ncommand.nickname.others.set.description=Sets player nicknames.\ncommand.nickname.others.reset.description=Removes any set nickname from the target.\ncommand.reload.description=Reloads Carbon's config, channel settings, and translations. Will not load or unload any channels.\ncommand.reply.argument.message=The message to reply with.\ncommand.reply.description=Sends a message to the last player that messaged you.\ncommand.togglemsg.description=Allows and disallows other players from mesaging you.\ncommand.unignore.argument.player=The name of the player to unignore.\ncommand.unignore.argument.uuid=The UUID of the player to unignore.\ncommand.unignore.description=Stops hiding messages from the specified player.\ncommand.unmute.argument.player=The name of the player to unmute.\ncommand.unmute.argument.uuid=The UUID of the player to unmute.\ncommand.unmute.description=Unmutes players, allowing them to use chat and whisper other players.\ncommand.updateusername.argument.player=The name of the player to update.\ncommand.updateusername.argument.uuid=The uuid of the player to update.\ncommand.updateusername.description=Updates the player's username to match their mojang name.\ncommand.updateusername.fetching=Fetching username...\ncommand.updateusername.notupdated=Unable to fetch username.\ncommand.updateusername.updated=Updated <newname>'s username!\ncommand.whisper.argument.message=The message to send.\ncommand.whisper.argument.player=The name of the player to message.\ncommand.whisper.description=Sends a private message to the specified player.\ncommand.party.pagination_header=<green>Party members</green>:\ncommand.party.pagination_element=<online:'<green>':'<gray>'> -<reset> <display_name>\ncommand.party.created=<green>Successfully created and joined party '</green><party_name><green>'!\ncommand.party.not_in_party=<red>You are not in a party. Use '/party create' to create one, or '/party accept' to accept an invite.\ncommand.party.current_party=<green>You are in party<white>:</white></green> <party_name>\ncommand.party.must_leave_current_first=<red>You must leave your current party first.\ncommand.party.name_too_long=<red>Party name is too long.\ncommand.party.received_invite=<hover:show_text:'<green>Click to accept'><click:run_command:'/party accept <sender_username>'><green>You were invited to the party '</green><party_name><green>' by </green><sender_display_name><green>. Click this message to accept.\ncommand.party.sent_invite=<green>Sent party invite to </green><recipient_display_name><green>.\ncommand.party.must_specify_invite=<red>You must specify whose party invite to accept.\ncommand.party.no_pending_invites=<red>You do not have any pending party invites.\ncommand.party.no_invite_from=<red>You do not have a pending invite from </red><sender_display_name><red>.\ncommand.party.joined_party=<green>Successfully joined party '</green><party_name><green>'!\ncommand.party.left_party=<green>Successfully left party '</green><party_name><green>'.\ncommand.party.disbanded=<green>Successfully disbanded party '</green><party_name><green>'.\ncommand.party.cannot_disband_multiple_members=<red>Cannot disband party '</red><party_name><red>', you are not the last member.\ncommand.party.must_be_in_party=<red>You must be in a party to use this command. Use '/party create' to create one, or '/party accept' to accept an invite.\ncommand.party.cannot_invite_self=<red>You cannot invite yourself.\ncommand.party.description=Get info about and see members of your current party.\ncommand.party.create.description=Create a new party.\ncommand.party.invite.description=Invite a player to your party.\ncommand.party.accept.description=Accept party invites.\ncommand.party.leave.description=Leave your current party.\ncommand.party.already_in_party=<display_name><red> is already in your party.\ncommand.party.disband.description=Disband your current party.\ncommand.realname.description=Shows the player's real name.\ncommand.realname.argument.player=The player's display name.\ncommand.spy.enabled=<green>Spying is now enabled.\ncommand.spy.disabled=<red>Spying is now disabled.\ncommand.spy.description=Allows a player to view all private and channel messages they otherwise wouldn't see.\nduration.days=<days>d<hours>h<minutes>m<seconds>s\nduration.hours=<hours>h<minutes>m<seconds>s\nparty.player_joined=<display_name><green> joined your party.\nparty.player_left=<display_name><green> left your party.\nparty.cannot_use_channel=<red>You must join a party to use this channel.\nparty.spy=<red>Spy <party_name> <red>[<username>: <white><message><red>]\nconfig.reload.failed=<red>Config failed to reload\nconfig.reload.success=<green>Config reloaded successfully\nerror.command.argument_parsing=<red>Invalid command argument: <gray><throwable_message>\nerror.command.command_execution=<hover:show_text:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    Click to copy\"><click:copy_to_clipboard:'<stacktrace>'><red>An internal error occurred while attempting to perform this command.\nerror.command.invalid_player=No player found for input '<input>'\nerror.command.invalid_sender=<red>Invalid command sender. You must be of type <gray><sender_type>\nerror.command.invalid_syntax=<red>Invalid command syntax. Correct command syntax is: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>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.\nerror.command.command_needs_player=<red>Non-players must provide the player argument to execute this command.\nignore.already_ignored=<red>You are already ignoring <target>\nignore.not_ignored=<red>You are not ignoring <target>\nignore.exempt=<red>You cannot ignore <target>\nignore.invalid_target=<red>No target found\nignore.now_ignoring=<green>You are now ignoring <target>\nignore.no_longer_ignoring=<green>You are no longer ignoring <target>\nmute.alert.players=<red><target> <red>has been muted\nmute.alert.players.temp=<red><target> <red>has been muted for <duration>\nmute.alert.target=<red>You have been muted\nmute.alert.target.temp=<red>You have been muted for <duration>\nmute.cannot_speak=<red>You cannot speak when muted\nmute.exempt=<red>That player is exempt from being muted\nmute.info.muted=<red><target> <red>is muted\nmute.info.muted.duration=<red><target> <red>is muted for <duration>\nmute.info.not_muted=<red><target> <gold>is not muted\nmute.info.self.muted=<red>You are muted\nmute.info.self.not_muted=<green>You are not muted\nmute.no_target=<red>No specified player to mute.\nmute.spy.prefix=<red><hover:show_text:'<red>Muted</red>'>M</hover></red>\nmute.unmute.alert.players=<green><target> <green>has been unmuted\nmute.unmute.alert.target=<green>You have been unmuted\nmute.unmute.no_target=<red>No specified player to unmute.\nnickname.reset.others=<green><target></green><gold>'s nickname was reset\nnickname.reset=<gold>Your nickname was reset\nnickname.set.others=<green>You set </green><target><green>'s nickname to </green><nickname>\nnickname.set=<green>Your nickname has been set to </green><nickname>\nnickname.show.others.unset=<target><red> does not have a nickname set\nnickname.show.others=<target><green>'s nickname is </green><nickname>\nnickname.show.unset=<red>You do not have a nickname set\nnickname.show=<green>Your nickname is </green><nickname>\nnickname.error.character_limit=<red>Nickname \"<nickname>\" has exceeded the character limit. Must be set to <min_length>~<max_length> characters.\nnickname.error.blacklist=<red>Nickname \"<nickname>\" is not allowed. Please choose another name.\nnickname.error.filter=<red>Nicknames must be alphanumeric!\nnickname.realname=<target>'s real name is <username>\nreply.target.missing=<red>You have no-one to reply to\nreply.target.self=<red>You cannot whisper to yourself\nwhisper.console=<gold>[<green><sender_display_name></green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.continue.target_missing=<red>You have no one to whisper\nwhisper.error=<red>Failed to send private message\nwhisper.from=<click:suggest_command:'/whisper <sender_username> '><hover:show_text:'Click to start a reply'><gold>[<green><sender_display_name></green>] -> [<green>You</green>] <message>\nwhisper.from.spy=<red>SPY [</red><green><sender_display_name></green>]<red> -> [</red><green><recipient_display_name></green><red>]</red> <message>\nwhisper.ignored_by_target=<red><target> <red>is ignoring you\nwhisper.ignoring_target=<red>You are ignoring <target>\nwhisper.ignoring_all=<red>You cannot send messages while they are ignored!\nwhisper.no_permission.receive=<red>That player doesn't have permission to receive messages!\nwhisper.no_permission.send=<red>You don't have permission to send whispers!\nwhisper.to=<click:suggest_command:'/whisper <recipient_username> '><hover:show_text:'Click to start another message to <recipient_display_name>'><gold>[<green>You</green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.toggled.on=Now receiving private messages.\nwhisper.toggled.off=No longer receiving private messages.\nchannel.cooldown=<red>You may use chat again in <remaining> seconds!\nchannel.radius.empty_recipients=<red>You're not close enough to anyone to send a message\nchannel.radius.spy=<red>Spy [<username>: <white><message><red>]\nchannel.joined=<green>You have rejoined the channel</green>\nchannel.left=<red>You have left the channel</red>\nchannel.no_permission=<red>You do not have permission to use this channel</red>\nchannel.already_left=<red>You have already left this channel</red>\nchannel.not_left=<red>You have not left this channel</red>\nchannel.not_found=<red>Channel not found</red>\npagination.page_out_of_range=<red>Page <page> is out of range! There are only <pages> pages.\npagination.click_for_next_page=Click for next page\npagination.click_for_previous_page=Click for previous page\npagination.footer=<gray>Page <page><white>/</white><pages> <aqua><buttons>\nintegrations.towny.cannot_use_alliance_channel=<red>You must join an alliance to use this channel.\nintegrations.towny.cannot_use_nation_channel=<red>You must join a nation to use this channel.\nintegrations.towny.cannot_use_town_channel=<red>You must join a town to use this channel.\nintegrations.mcmmo.cannot_use_party_channel=<red>You must join an mcMMO party to use this channel.\nintegrations.adp_parties.cannot_use_party_channel=<red>You must join a party to use this channel.\nintegrations.fuuid.cannot_use_faction_channel=<red>You must join a faction to use this channel.\nintegrations.fuuid.cannot_use_alliance_channel=<red>You must join an alliance to use this channel.\nintegrations.fuuid.cannot_use_truce_channel=<red>You must have a truce with another faction to use this channel.\nintegrations.fuuid.cannot_use_mod_channel=<red>You must be a faction mod/admin to use this channel.\nintegrations.plotsquared.cannot_use_plot_channel=<red>You must be in a plot to use this channel.\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-es_CL.properties",
    "content": "channel.change=<green>Ahora estás enviando mensajes a </green><channel>\ncommand.clearchat.description=Borra la ventana de chat para todos los jugadores.\ncommand.continue.argument.message=El mensaje a enviar.\ncommand.continue.description=Envía un mensaje a la última persona con la que hablaste.\ncommand.debug.argument.player=El jugador para verificar los grupos.\ncommand.debug.description=Muestra los grupos de permisos de los jugadores.\ncommand.filter.optional.enabled=<green>¡Filtro de chat opcional activado\\!\ncommand.filter.optional.disabled=<red>¡Filtro de chat opcional desactivado\\!\ncommand.filter.optional.description=Activa o desactiva el filtro de chat opcional.\ncommand.help.argument.query=La consulta de búsqueda.\ncommand.help.description=Lista de comandos de Carbon.\ncommand.help.misc.arguments=Argumentos\ncommand.help.misc.available_commands=Comandos Disponibles\ncommand.help.misc.click_for_next_page=Haz clic para la siguiente página\ncommand.help.misc.click_for_previous_page=Haz clic para la página anterior\ncommand.help.misc.click_to_show_help=Haz clic para mostrar la ayuda de este comando\ncommand.help.misc.command=Comando\ncommand.help.misc.description=Descripción\ncommand.help.misc.help=Ayuda\ncommand.help.misc.no_description=Sin descripción\ncommand.help.misc.no_results_for_query=No hay resultados para la consulta\ncommand.help.misc.optional=Opcional\ncommand.help.misc.page_out_of_range=Error\\: La página <page> no está en el rango. Debe estar en el rango [1, <max_pages>]\ncommand.help.misc.showing_results_for_query=Mostrando resultados de la búsqueda para la consulta\ncommand.ignore.argument.player=El nombre del jugador a ignorar.\ncommand.ignore.argument.uuid=El UUID del jugador a ignorar.\ncommand.ignore.description=Oculta todos los mensajes entrantes de los jugadores ignorados.\ncommand.ignorelist.description=Muestra una lista paginada de los jugadores que estás ignorando.\ncommand.ignorelist.none_ignored=<green>No estás ignorando a ningún jugador.\ncommand.ignorelist.pagination_header=<bold>Jugadores ignorados\ncommand.ignorelist.pagination_element= - <display_name> <click\\:run_command\\:''/unignore <username>''><hover\\:show_text\\:''Haz clic para dejar de ignorar a <username>''><gray>[<white>dejar de ignorar</white>]</gray>\ncommand.join.description=Únete a un canal que has abandonado previamente.\ncommand.leave.description=Abandona un canal al que actualmente tienes acceso.\ncommand.mute.argument.player=El nombre del jugador a silenciar.\ncommand.mute.argument.uuid=El UUID del jugador a silenciar.\ncommand.mute.argument.duration=La duración por la que el jugador será silenciado.\ncommand.mute.description=Silencia a los jugadores, impidiéndoles usar el chat o susurrar a otros jugadores.\ncommand.muteinfo.argument.player=El nombre del jugador.\ncommand.muteinfo.argument.uuid=El UUID del jugador.\ncommand.muteinfo.description=Muestra si los jugadores están silenciados o no.\ncommand.nickname.argument.nickname=El apodo a establecer.\ncommand.nickname.argument.player=El nombre del jugador objetivo.\ncommand.nickname.description=Muestra tu apodo.\ncommand.nickname.set.description=Establece tu apodo.\ncommand.nickname.reset.description=Elimina tu apodo.\ncommand.nickname.others.description=Muestra los apodos de los jugadores.\ncommand.nickname.others.set.description=Establece los apodos de los jugadores.\ncommand.nickname.others.reset.description=Elimina cualquier apodo establecido del objetivo.\ncommand.reload.description=Recarga la configuración de Carbon, los ajustes de los canales y las traducciones. No cargará ni descargará ningún canal.\ncommand.reply.argument.message=El mensaje para responder.\ncommand.reply.description=Envía un mensaje al último jugador que te envió un mensaje.\ncommand.togglemsg.description=Permite y no permite que otros jugadores te envíen mensajes.\ncommand.unignore.argument.player=El nombre del jugador para dejar de ignorar.\ncommand.unignore.argument.uuid=El UUID del jugador para dejar de ignorar.\ncommand.unignore.description=Deja de ocultar los mensajes del jugador especificado.\ncommand.unmute.argument.player=El nombre del jugador para desilenciar.\ncommand.unmute.argument.uuid=El UUID del jugador para desilenciar.\ncommand.unmute.description=Desilencia a los jugadores, permitiéndoles usar el chat y susurrar a otros jugadores.\ncommand.updateusername.argument.player=El nombre del jugador a actualizar.\ncommand.updateusername.argument.uuid=El UUID del jugador a actualizar.\ncommand.updateusername.description=Actualiza el nombre de usuario del jugador para que coincida con su nombre de Mojang.\ncommand.updateusername.fetching=Obteniendo nombre de usuario...\ncommand.updateusername.notupdated=No se pudo obtener el nombre de usuario.\ncommand.updateusername.updated=¡Nombre de usuario de <newname> actualizado\\!\ncommand.whisper.argument.message=El mensaje a enviar.\ncommand.whisper.argument.player=El nombre del jugador al que enviar el mensaje.\ncommand.whisper.description=Envía un mensaje privado al jugador especificado.\ncommand.party.pagination_header=<green>Miembros del grupo</green>\\:\ncommand.party.pagination_element=<online\\:''<green>''\\:''<gray>''> -<reset> <display_name>\ncommand.party.created=<green>¡Grupo creado y unido exitosamente ''</green><party_name><green>''\\!\ncommand.party.not_in_party=<red>No estás en un grupo. Usa ''/party create'' para crear uno, o ''/party accept'' para aceptar una invitación.\ncommand.party.current_party=<green>Estás en el grupo<white>\\:</white></green> <party_name>\ncommand.party.must_leave_current_first=<red>Debes abandonar tu grupo actual primero.\ncommand.party.name_too_long=<red>El nombre del grupo es demasiado largo.\ncommand.party.received_invite=<hover\\:show_text\\:''<green>Haz clic para aceptar''><click\\:run_command\\:''/party accept <sender_username>''><green>Fuiste invitado al grupo ''</green><party_name><green>'' por </green><sender_display_name><green>. Haz clic en este mensaje para aceptar.\ncommand.party.sent_invite=<green>Invitación de grupo enviada a </green><recipient_display_name><green>.\ncommand.party.must_specify_invite=<red>Debes especificar a quién aceptar la invitación.\ncommand.party.no_pending_invites=<red>No tienes invitaciones de grupo pendientes.\ncommand.party.no_invite_from=<red>No tienes una invitación pendiente de </red><sender_display_name><red>.\ncommand.party.joined_party=<green>¡Te has unido exitosamente al grupo ''</green><party_name><green>''\\!\ncommand.party.left_party=<green>Has abandonado exitosamente el grupo ''</green><party_name><green>''.\ncommand.party.disbanded=<green>Grupo disuelto exitosamente ''</green><party_name><green>''.\ncommand.party.cannot_disband_multiple_members=<red>No puedes disolver el grupo ''</red><party_name><red>'', no eres el último miembro.\ncommand.party.must_be_in_party=<red>Debes estar en un grupo para usar este comando. Usa ''/party create'' para crear uno, o ''/party accept'' para aceptar una invitación.\ncommand.party.cannot_invite_self=<red>No puedes invitarte a ti mismo.\ncommand.party.description=Obtén información y ve los miembros de tu grupo actual.\ncommand.party.create.description=Crea un nuevo grupo.\ncommand.party.invite.description=Invita a un jugador a tu grupo.\ncommand.party.accept.description=Acepta invitaciones de grupo.\ncommand.party.leave.description=Abandona tu grupo actual.\ncommand.party.already_in_party=<display_name><red> ya está en tu grupo.\ncommand.party.disband.description=Disuelve tu grupo actual.\ncommand.spy.enabled=<green>El espionaje está ahora activado.\ncommand.spy.disabled=<red>El espionaje está ahora desactivado.\ncommand.spy.description=Permite a un jugador ver todos los mensajes privados y de canal que de otra manera no vería.\nduration.days=<days>d<hours>h<minutes>m<seconds>s\nduration.hours=<hours>h<minutes>m<seconds>s\nparty.player_joined=<display_name><green> se ha unido a tu grupo.\nparty.player_left=<display_name><green> ha abandonado tu grupo.\nparty.cannot_use_channel=<red>Debes unirte a un grupo para usar este canal.\nparty.spy=<red>Espía <party_name> <red>[<username>\\: <white><message><red>]\nconfig.reload.failed=<red>Error al recargar la configuración\nconfig.reload.success=<green>Configuración recargada exitosamente\nerror.command.argument_parsing=<red>Argumento de comando inválido\\: <gray><throwable_message>\nerror.command.command_execution=<hover\\:show_text\\:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    Haz clic para copiar\"><click\\:copy_to_clipboard\\:''<stacktrace>''><red>Ocurrió un error interno al intentar ejecutar este comando.\nerror.command.invalid_player=No se encontró ningún jugador para la entrada ''<input>''\nerror.command.invalid_sender=<red>Remitente de comando inválido. Debes ser de tipo <gray><sender_type>\nerror.command.invalid_syntax=<red>Sintaxis de comando inválida. La sintaxis correcta es\\: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>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.\nerror.command.command_needs_player=<red>Los no jugadores deben proporcionar el argumento del jugador para ejecutar este comando.\nignore.already_ignored=<red>Ya estás ignorando a <target>\nignore.not_ignored=<red>No estás ignorando a <target>\nignore.exempt=<red>No puedes ignorar a <target>\nignore.invalid_target=<red>No se encontró el objetivo\nignore.now_ignoring=<green>Ahora estás ignorando a <target>\nignore.no_longer_ignoring=<green>Ya no estás ignorando a <target>\nmute.alert.players=<red><target> <red>ha sido silenciado\nmute.alert.players.temp=<red><target> <red>ha sido silenciado por <duration>\nmute.alert.target=<red>Has sido silenciado\nmute.alert.target.temp=<red>Has sido silenciado por <duration>\nmute.cannot_speak=<red>No puedes hablar cuando estás silenciado\nmute.exempt=<red>Ese jugador está exento de ser silenciado\nmute.info.muted=<red><target> <red>está silenciado\nmute.info.not_muted=<red><target> <gold>no está silenciado\nmute.info.self.muted=<red>Estás silenciado\nmute.info.self.not_muted=<green>No estás silenciado\nmute.no_target=<red>No se especificó ningún jugador para silenciar.\nmute.spy.prefix=<red><hover\\:show_text\\:''<red>Silenciado</red>''>S</hover></red>\nmute.unmute.alert.players=<green><target> <green>ha sido desilenciado\nmute.unmute.alert.target=<green>Has sido desilenciado\nmute.unmute.no_target=<red>No se especificó ningún jugador para desilenciar.\nnickname.reset.others=<green><target></green><gold>El apodo de <target> ha sido restablecido\nnickname.reset=<gold>Tu apodo ha sido restablecido\nnickname.set.others=<green>Has establecido el apodo de </green><target><green> a </green><nickname>\nnickname.set=<green>Tu apodo ha sido establecido a </green><nickname>\nnickname.show.others.unset=<target><red> no tiene un apodo establecido\nnickname.show.others=<target><green>El apodo de <target> es </green><nickname>\nnickname.show.unset=<red>No tienes un apodo establecido\nnickname.show=<green>Tu apodo es </green><nickname>\nnickname.error.character_limit=<red>El apodo \"<nickname>\" ha excedido el límite de caracteres. Debe tener entre <min_length>~<max_length> caracteres.\nnickname.error.blacklist=<red>El apodo \"<nickname>\" no está permitido. Por favor, elige otro nombre.\nnickname.error.filter=<red>¡Los apodos deben ser alfanuméricos\\!\nreply.target.missing=<red>No tienes a nadie a quien responder\nreply.target.self=<red>No puedes susurrarte a ti mismo\nwhisper.console=<gold>[<green><sender_display_name></green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.continue.target_missing=<red>No tienes a nadie a quien susurrar\nwhisper.error=<red>Error al enviar mensaje privado\nwhisper.from=<click\\:suggest_command\\:''/whisper <sender_username> ''><hover\\:show_text\\:''Haz clic para responder''><gold>[<green><sender_display_name></green>] -> [<green>Tú</green>] <message>\nwhisper.from.spy=<red>ESPÍA [</red><green><sender_display_name></green>]<red> -> [</red><green><recipient_display_name></green><red>]</red> <message>\nwhisper.ignored_by_target=<red><target> <red>te está ignorando\nwhisper.ignoring_target=<red>Estás ignorando a <target>\nwhisper.ignoring_all=<red>¡No puedes enviar mensajes mientras los ignoras\\!\nwhisper.no_permission.receive=<red>¡Ese jugador no tiene permiso para recibir mensajes\\!\nwhisper.no_permission.send=<red>¡No tienes permiso para enviar susurros\\!\nwhisper.to=<click\\:suggest_command\\:''/whisper <recipient_username> ''><hover\\:show_text\\:''Haz clic para enviar otro mensaje a <recipient_display_name>''><gold>[<green>Tú</green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.toggled.on=Ahora recibes mensajes privados.\nwhisper.toggled.off=Ya no recibes mensajes privados.\nchannel.cooldown=<red>¡Podrás usar el chat nuevamente en <remaining> segundos\\!\nchannel.radius.empty_recipients=<red>No estás lo suficientemente cerca de nadie para enviar un mensaje\nchannel.radius.spy=<red>Espía [<username>\\: <white><message><red>]\nchannel.joined=<green>Has vuelto a unirte al canal</green>\nchannel.left=<red>Has abandonado el canal</red>\nchannel.no_permission=<red>No tienes permiso para usar este canal</red>\nchannel.already_left=<red>Ya has abandonado este canal</red>\nchannel.not_left=<red>No has abandonado este canal</red>\nchannel.not_found=<red>Canal no encontrado</red>\npagination.page_out_of_range=<red>¡La página <page> está fuera de rango\\! Solo hay <pages> páginas.\npagination.click_for_next_page=Haz clic para la siguiente página\npagination.click_for_previous_page=Haz clic para la página anterior\npagination.footer=<gray>Página <page><white>/</white><pages> <aqua><buttons>\nintegrations.towny.cannot_use_alliance_channel=<red>Debes unirte a una alianza para usar este canal.\nintegrations.towny.cannot_use_nation_channel=<red>Debes unirte a una nación para usar este canal.\nintegrations.towny.cannot_use_town_channel=<red>Debes unirte a una ciudad para usar este canal.\nintegrations.mcmmo.cannot_use_party_channel=<red>Debes unirte a un grupo de mcMMO para usar este canal.\nintegrations.fuuid.cannot_use_faction_channel=<red>Debes unirte a una facción para usar este canal.\nintegrations.fuuid.cannot_use_alliance_channel=<red>Debes unirte a una alianza para usar este canal.\nintegrations.fuuid.cannot_use_truce_channel=<red>Debes tener una tregua con otra facción para usar este canal.\nintegrations.fuuid.cannot_use_mod_channel=<red>Debes ser un mod/admin de facción para usar este canal.\nintegrations.plotsquared.cannot_use_plot_channel=<red>Debes estar en una parcela para usar este canal.\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-es_ES.properties",
    "content": "channel.change=<green>Ahora estás enviando mensajes a </green><channel>\ncommand.clearchat.description=Borra la ventana de chat para todos los jugadores.\ncommand.continue.argument.message=El mensaje a enviar.\ncommand.continue.description=Envía un mensaje a la última persona con la que hablaste.\ncommand.debug.argument.player=El jugador para verificar los grupos.\ncommand.debug.description=Muestra los grupos de permisos de los jugadores.\ncommand.filter.optional.enabled=<green>¡Filtro de chat opcional activado\\!\ncommand.filter.optional.disabled=<red>¡Filtro de chat opcional desactivado\\!\ncommand.filter.optional.description=Activa o desactiva el filtro de chat opcional.\ncommand.help.argument.query=La consulta de búsqueda.\ncommand.help.description=Lista de comandos de Carbon.\ncommand.help.misc.arguments=Argumentos\ncommand.help.misc.available_commands=Comandos Disponibles\ncommand.help.misc.click_for_next_page=Haz clic para la siguiente página\ncommand.help.misc.click_for_previous_page=Haz clic para la página anterior\ncommand.help.misc.click_to_show_help=Haz clic para mostrar la ayuda de este comando\ncommand.help.misc.command=Comando\ncommand.help.misc.description=Descripción\ncommand.help.misc.help=Ayuda\ncommand.help.misc.no_description=Sin descripción\ncommand.help.misc.no_results_for_query=No hay resultados para la consulta\ncommand.help.misc.optional=Opcional\ncommand.help.misc.page_out_of_range=Error\\: La página <page> no está en el rango. Debe estar en el rango [1, <max_pages>]\ncommand.help.misc.showing_results_for_query=Mostrando resultados de la búsqueda para la consulta\ncommand.ignore.argument.player=El nombre del jugador a ignorar.\ncommand.ignore.argument.uuid=El UUID del jugador a ignorar.\ncommand.ignore.description=Oculta todos los mensajes entrantes de los jugadores ignorados.\ncommand.ignorelist.description=Muestra una lista paginada de los jugadores que estás ignorando.\ncommand.ignorelist.none_ignored=<green>No estás ignorando a ningún jugador.\ncommand.ignorelist.pagination_header=<bold>Jugadores ignorados\ncommand.ignorelist.pagination_element= - <display_name> <click\\:run_command\\:''/unignore <username>''><hover\\:show_text\\:''Haz clic para dejar de ignorar a <username>''><gray>[<white>dejar de ignorar</white>]</gray>\ncommand.join.description=Únete a un canal que has abandonado previamente.\ncommand.leave.description=Abandona un canal al que actualmente tienes acceso.\ncommand.mute.argument.player=El nombre del jugador a silenciar.\ncommand.mute.argument.uuid=El UUID del jugador a silenciar.\ncommand.mute.argument.duration=La duración por la que el jugador será silenciado.\ncommand.mute.description=Silencia a los jugadores, impidiéndoles usar el chat o susurrar a otros jugadores.\ncommand.muteinfo.argument.player=El nombre del jugador.\ncommand.muteinfo.argument.uuid=El UUID del jugador.\ncommand.muteinfo.description=Muestra si los jugadores están silenciados o no.\ncommand.nickname.argument.nickname=El apodo a establecer.\ncommand.nickname.argument.player=El nombre del jugador objetivo.\ncommand.nickname.description=Muestra tu apodo.\ncommand.nickname.set.description=Establece tu apodo.\ncommand.nickname.reset.description=Elimina tu apodo.\ncommand.nickname.others.description=Muestra los apodos de los jugadores.\ncommand.nickname.others.set.description=Establece los apodos de los jugadores.\ncommand.nickname.others.reset.description=Elimina cualquier apodo establecido del objetivo.\ncommand.reload.description=Recarga la configuración de Carbon, los ajustes de los canales y las traducciones. No cargará ni descargará ningún canal.\ncommand.reply.argument.message=El mensaje para responder.\ncommand.reply.description=Envía un mensaje al último jugador que te envió un mensaje.\ncommand.togglemsg.description=Permite y no permite que otros jugadores te envíen mensajes.\ncommand.unignore.argument.player=El nombre del jugador para dejar de ignorar.\ncommand.unignore.argument.uuid=El UUID del jugador para dejar de ignorar.\ncommand.unignore.description=Deja de ocultar los mensajes del jugador especificado.\ncommand.unmute.argument.player=El nombre del jugador para desilenciar.\ncommand.unmute.argument.uuid=El UUID del jugador para desilenciar.\ncommand.unmute.description=Desilencia a los jugadores, permitiéndoles usar el chat y susurrar a otros jugadores.\ncommand.updateusername.argument.player=El nombre del jugador a actualizar.\ncommand.updateusername.argument.uuid=El UUID del jugador a actualizar.\ncommand.updateusername.description=Actualiza el nombre de usuario del jugador para que coincida con su nombre de Mojang.\ncommand.updateusername.fetching=Obteniendo nombre de usuario...\ncommand.updateusername.notupdated=No se pudo obtener el nombre de usuario.\ncommand.updateusername.updated=¡Nombre de usuario de <newname> actualizado\\!\ncommand.whisper.argument.message=El mensaje a enviar.\ncommand.whisper.argument.player=El nombre del jugador al que enviar el mensaje.\ncommand.whisper.description=Envía un mensaje privado al jugador especificado.\ncommand.party.pagination_header=<green>Miembros del grupo</green>\\:\ncommand.party.pagination_element=<online\\:''<green>''\\:''<gray>''> -<reset> <display_name>\ncommand.party.created=<green>¡Grupo creado y unido exitosamente ''</green><party_name><green>''\\!\ncommand.party.not_in_party=<red>No estás en un grupo. Usa ''/party create'' para crear uno, o ''/party accept'' para aceptar una invitación.\ncommand.party.current_party=<green>Estás en el grupo<white>\\:</white></green> <party_name>\ncommand.party.must_leave_current_first=<red>Debes abandonar tu grupo actual primero.\ncommand.party.name_too_long=<red>El nombre del grupo es demasiado largo.\ncommand.party.received_invite=<hover\\:show_text\\:''<green>Haz clic para aceptar''><click\\:run_command\\:''/party accept <sender_username>''><green>Fuiste invitado al grupo ''</green><party_name><green>'' por </green><sender_display_name><green>. Haz clic en este mensaje para aceptar.\ncommand.party.sent_invite=<green>Invitación de grupo enviada a </green><recipient_display_name><green>.\ncommand.party.must_specify_invite=<red>Debes especificar a quién aceptar la invitación.\ncommand.party.no_pending_invites=<red>No tienes invitaciones de grupo pendientes.\ncommand.party.no_invite_from=<red>No tienes una invitación pendiente de </red><sender_display_name><red>.\ncommand.party.joined_party=<green>¡Te has unido exitosamente al grupo ''</green><party_name><green>''\\!\ncommand.party.left_party=<green>Has abandonado exitosamente el grupo ''</green><party_name><green>''.\ncommand.party.disbanded=<green>Grupo disuelto exitosamente ''</green><party_name><green>''.\ncommand.party.cannot_disband_multiple_members=<red>No puedes disolver el grupo ''</red><party_name><red>'', no eres el último miembro.\ncommand.party.must_be_in_party=<red>Debes estar en un grupo para usar este comando. Usa ''/party create'' para crear uno, o ''/party accept'' para aceptar una invitación.\ncommand.party.cannot_invite_self=<red>No puedes invitarte a ti mismo.\ncommand.party.description=Obtén información y ve los miembros de tu grupo actual.\ncommand.party.create.description=Crea un nuevo grupo.\ncommand.party.invite.description=Invita a un jugador a tu grupo.\ncommand.party.accept.description=Acepta invitaciones de grupo.\ncommand.party.leave.description=Abandona tu grupo actual.\ncommand.party.already_in_party=<display_name><red> ya está en tu grupo.\ncommand.party.disband.description=Disuelve tu grupo actual.\ncommand.realname.description=Muestra el nombre real del jugador.\ncommand.spy.enabled=<green>El espionaje está ahora activado.\ncommand.spy.disabled=<red>El espionaje está ahora desactivado.\ncommand.spy.description=Permite a un jugador ver todos los mensajes privados y de canal que de otra manera no vería.\nduration.days=<days>d<hours>h<minutes>m<seconds>s\nduration.hours=<hours>h<minutes>m<seconds>s\nparty.player_joined=<display_name><green> se ha unido a tu grupo.\nparty.player_left=<display_name><green> ha abandonado tu grupo.\nparty.cannot_use_channel=<red>Debes unirte a un grupo para usar este canal.\nparty.spy=<red>Espía <party_name> <red>[<username>\\: <white><message><red>]\nconfig.reload.failed=<red>Error al recargar la configuración\nconfig.reload.success=<green>Configuración recargada exitosamente\nerror.command.argument_parsing=<red>Argumento de comando inválido\\: <gray><throwable_message>\nerror.command.command_execution=<hover\\:show_text\\:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    Haz clic para copiar\"><click\\:copy_to_clipboard\\:''<stacktrace>''><red>Ocurrió un error interno al intentar ejecutar este comando.\nerror.command.invalid_player=No se encontró ningún jugador para la entrada ''<input>''\nerror.command.invalid_sender=<red>Remitente de comando inválido. Debes ser de tipo <gray><sender_type>\nerror.command.invalid_syntax=<red>Sintaxis de comando inválida. La sintaxis correcta es\\: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>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.\nerror.command.command_needs_player=<red>Los no jugadores deben proporcionar el argumento del jugador para ejecutar este comando.\nignore.already_ignored=<red>Ya estás ignorando a <target>\nignore.not_ignored=<red>No estás ignorando a <target>\nignore.exempt=<red>No puedes ignorar a <target>\nignore.invalid_target=<red>No se encontró el objetivo\nignore.now_ignoring=<green>Ahora estás ignorando a <target>\nignore.no_longer_ignoring=<green>Ya no estás ignorando a <target>\nmute.alert.players=<red><target> <red>ha sido silenciado\nmute.alert.players.temp=<red><target> <red>ha sido silenciado por <duration>\nmute.alert.target=<red>Has sido silenciado\nmute.alert.target.temp=<red>Has sido silenciado por <duration>\nmute.cannot_speak=<red>No puedes hablar cuando estás silenciado\nmute.exempt=<red>Ese jugador está exento de ser silenciado\nmute.info.muted=<red><target> <red>está silenciado\nmute.info.muted.duration=<red><target> <red>está silenciado durante <duration>\nmute.info.not_muted=<red><target> <gold>no está silenciado\nmute.info.self.muted=<red>Estás silenciado\nmute.info.self.not_muted=<green>No estás silenciado\nmute.no_target=<red>No se especificó ningún jugador para silenciar.\nmute.spy.prefix=<red><hover\\:show_text\\:''<red>Silenciado</red>''>S</hover></red>\nmute.unmute.alert.players=<green><target> <green>ha sido desilenciado\nmute.unmute.alert.target=<green>Has sido desilenciado\nmute.unmute.no_target=<red>No se especificó ningún jugador para desilenciar.\nnickname.reset.others=<green><target></green><gold>El apodo de <target> ha sido restablecido\nnickname.reset=<gold>Tu apodo ha sido restablecido\nnickname.set.others=<green>Has establecido el apodo de </green><target><green> a </green><nickname>\nnickname.set=<green>Tu apodo ha sido establecido a </green><nickname>\nnickname.show.others.unset=<target><red> no tiene un apodo establecido\nnickname.show.others=<target><green>El apodo de <target> es </green><nickname>\nnickname.show.unset=<red>No tienes un apodo establecido\nnickname.show=<green>Tu apodo es </green><nickname>\nnickname.error.character_limit=<red>El apodo \"<nickname>\" ha excedido el límite de caracteres. Debe tener entre <min_length>~<max_length> caracteres.\nnickname.error.blacklist=<red>El apodo \"<nickname>\" no está permitido. Por favor, elige otro nombre.\nnickname.error.filter=<red>¡Los apodos deben ser alfanuméricos\\!\nreply.target.missing=<red>No tienes a nadie a quien responder\nreply.target.self=<red>No puedes susurrarte a ti mismo\nwhisper.console=<gold>[<green><sender_display_name></green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.continue.target_missing=<red>No tienes a nadie a quien susurrar\nwhisper.error=<red>Error al enviar mensaje privado\nwhisper.from=<click\\:suggest_command\\:''/whisper <sender_username> ''><hover\\:show_text\\:''Haz clic para responder''><gold>[<green><sender_display_name></green>] -> [<green>Tú</green>] <message>\nwhisper.from.spy=<red>ESPÍA [</red><green><sender_display_name></green>]<red> -> [</red><green><recipient_display_name></green><red>]</red> <message>\nwhisper.ignored_by_target=<red><target> <red>te está ignorando\nwhisper.ignoring_target=<red>Estás ignorando a <target>\nwhisper.ignoring_all=<red>¡No puedes enviar mensajes mientras los ignoras\\!\nwhisper.no_permission.receive=<red>¡Ese jugador no tiene permiso para recibir mensajes\\!\nwhisper.no_permission.send=<red>¡No tienes permiso para enviar susurros\\!\nwhisper.to=<click\\:suggest_command\\:''/whisper <recipient_username> ''><hover\\:show_text\\:''Haz clic para enviar otro mensaje a <recipient_display_name>''><gold>[<green>Tú</green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.toggled.on=Ahora recibes mensajes privados.\nwhisper.toggled.off=Ya no recibes mensajes privados.\nchannel.cooldown=<red>¡Podrás usar el chat nuevamente en <remaining> segundos\\!\nchannel.radius.empty_recipients=<red>No estás lo suficientemente cerca de nadie para enviar un mensaje\nchannel.radius.spy=<red>Espía [<username>\\: <white><message><red>]\nchannel.joined=<green>Has vuelto a unirte al canal</green>\nchannel.left=<red>Has abandonado el canal</red>\nchannel.no_permission=<red>No tienes permiso para usar este canal</red>\nchannel.already_left=<red>Ya has abandonado este canal</red>\nchannel.not_left=<red>No has abandonado este canal</red>\nchannel.not_found=<red>Canal no encontrado</red>\npagination.page_out_of_range=<red>¡La página <page> está fuera de rango\\! Solo hay <pages> páginas.\npagination.click_for_next_page=Haz clic para la siguiente página\npagination.click_for_previous_page=Haz clic para la página anterior\npagination.footer=<gray>Página <page><white>/</white><pages> <aqua><buttons>\nintegrations.towny.cannot_use_alliance_channel=<red>Debes unirte a una alianza para usar este canal.\nintegrations.towny.cannot_use_nation_channel=<red>Debes unirte a una nación para usar este canal.\nintegrations.towny.cannot_use_town_channel=<red>Debes unirte a una ciudad para usar este canal.\nintegrations.mcmmo.cannot_use_party_channel=<red>Debes unirte a un grupo de mcMMO para usar este canal.\nintegrations.fuuid.cannot_use_faction_channel=<red>Debes unirte a una facción para usar este canal.\nintegrations.fuuid.cannot_use_alliance_channel=<red>Debes unirte a una alianza para usar este canal.\nintegrations.fuuid.cannot_use_truce_channel=<red>Debes tener una tregua con otra facción para usar este canal.\nintegrations.fuuid.cannot_use_mod_channel=<red>Debes ser un mod/admin de facción para usar este canal.\nintegrations.plotsquared.cannot_use_plot_channel=<red>Debes estar en una parcela para usar este canal.\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-fi_FI.properties",
    "content": ""
  },
  {
    "path": "common/src/main/resources/locale/messages-fr_CA.properties",
    "content": ""
  },
  {
    "path": "common/src/main/resources/locale/messages-fr_FR.properties",
    "content": "integrations.plotsquared.cannot_use_plot_channel=<red>Vous devez être dans un plot pour utiliser ce canal.\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-ja_JP.properties",
    "content": "channel.change=<green>メッセージを送信中</green><channel>\ncommand.clearchat.description=すべてのプレイヤーのチャットウィンドウをクリアします。\ncommand.continue.argument.message=送信するメッセージです。\ncommand.continue.description=最後にメッセージを送った人にメッセージを送信します。\ncommand.debug.argument.player=プレーヤーのグループをチェックする。\ncommand.debug.description=プレイヤーの権限グループを表示します。\ncommand.help.argument.query=検索クエリ。\ncommand.help.description=Carbonのコマンドリストです。\ncommand.help.misc.arguments=引数\ncommand.help.misc.available_commands=利用可能なコマンド一覧\ncommand.help.misc.click_for_next_page=クリックで次のページへ\ncommand.help.misc.click_for_previous_page=クリックで前のページへ\ncommand.help.misc.click_to_show_help=クリックしてこのコマンドのヘルプを表示\ncommand.help.misc.command=コマンド\ncommand.help.misc.description=説明\ncommand.help.misc.help=ヘルプ\ncommand.help.misc.no_description=説明なし\ncommand.help.misc.no_results_for_query=クエリの結果はありません\ncommand.help.misc.optional=オプション\ncommand.help.misc.page_out_of_range=エラー\\: ページ <page> は範囲外です。[1, <max_pages>] の範囲内でなければなりません。\ncommand.help.misc.showing_results_for_query=クエリの検索結果を表示中\ncommand.ignore.argument.player=無視するプレーヤーの名前。\ncommand.ignore.argument.uuid=無視するプレーヤーのUUID。\ncommand.ignore.description=無視したプレイヤーからのすべてのメッセージを非表示にします。\ncommand.ignorelist.description=あなたが無視しているプレイヤーのリストをページ順に表示する。\ncommand.ignorelist.none_ignored=<green>あなたは他のプレイヤーを無視していません。\ncommand.ignorelist.pagination_header=<bold>無視したプレイヤー\ncommand.ignorelist.pagination_element= - <display_name> <click\\:run_command\\:''/unignore <username>''><hover\\:show_text\\:''クリックして無視を解除する <username>''><gray>[<white>unignore</white>]</gray>\ncommand.join.description=以前に退出したチャンネルに加入する。\ncommand.leave.description=現在アクセスしているチャンネルから退出する。\ncommand.mute.argument.player=ミュートするプレーヤーの名前。\ncommand.mute.argument.uuid=ミュートするプレーヤーのUUID。\ncommand.mute.description=プレイヤーをミュートし、チャットや他のプレイヤーへのプライベートメッセージを禁止します。\ncommand.muteinfo.argument.player=プレイヤーの名前。\ncommand.muteinfo.argument.uuid=プレイヤーのUUID。\ncommand.muteinfo.description=プレイヤーがミュートかどうかを表示します。\ncommand.nickname.argument.nickname=設定するニックネーム。\ncommand.nickname.argument.player=ターゲットプレイヤーの名前。このフラグがなければ、送信者がターゲットになります。\ncommand.nickname.description=プレイヤーのニックネームを設定および表示します。\ncommand.nickname.set.description=あなたのニックネームを設定する。\ncommand.nickname.reset.description=あなたのニックネームを削除する。\ncommand.nickname.others.description=プレイヤーのニックネームを表示する。\ncommand.nickname.others.set.description=プレイヤーのニックネームを設定する。\ncommand.nickname.others.reset.description=ターゲットから設定されたニックネームを削除する。\ncommand.reload.description=Carbonの設定、チャンネル設定、翻訳をリロードします。チャンネルをロードしたり、アンロードしたりしません。\ncommand.reply.argument.message=返信するメッセージ。\ncommand.reply.description=最後にメッセージを送ったプレイヤーにメッセージを送信します。\ncommand.togglemsg.description=他のプレイヤーからあなたへのメッセージを許可/拒否する。\ncommand.unignore.argument.player=無視を解除するプレーヤーの名前。\ncommand.unignore.argument.uuid=無視を解除するプレイヤーのUUID。\ncommand.unignore.description=指定したプレイヤーからのメッセージの非表示を停止します。\ncommand.unmute.argument.player=ミュートを解除するプレーヤーの名前。\ncommand.unmute.argument.uuid=ミュートを解除するプレーヤーのUUID。\ncommand.unmute.description=プレイヤーのミュートを解除し、チャットや他のプレイヤーへのプライベートメッセージを使用できるようにします。\ncommand.updateusername.argument.player=更新するプレイヤーの名前。\ncommand.updateusername.argument.uuid=更新するプレーヤーのuuid。\ncommand.updateusername.description=プレイヤーのユーザー名をmojangの名前と一致するように更新する。\ncommand.updateusername.fetching=ユーザー名を取得中...\ncommand.updateusername.notupdated=ユーザー名を取得できません。\ncommand.updateusername.updated=<newname>のユーザー名を更新しました！\ncommand.whisper.argument.message=送信するメッセージ。\ncommand.whisper.argument.player=メッセージを送信するプレーヤーの名前。\ncommand.whisper.description=指定したプレーヤーにプライベートメッセージを送信します。\ncommand.party.pagination_header=<green>パーティーメンバー</green>\\:\ncommand.party.pagination_element=<online\\:''<green>''\\:''<gray>''> -<reset> <display_name>\ncommand.party.created=<green>パーティー「</green><party_name><green>」の作成と参加に成功しました！\ncommand.party.not_in_party=<red>あなたはパーティーに参加していません。パーティを作成するには「/party create」を、招待を承認するには「/party accept」を使用して下さい。\ncommand.party.current_party=<green>あなたが参加しているパーティー<white>\\:</white></green> <party_name>\ncommand.party.must_leave_current_first=<red>あなたはまず現在のパーティーから退出しなければなりません。\ncommand.party.name_too_long=<red>パーティー名が長すぎます。\ncommand.party.received_invite=<hover\\:show_text\\:''<green>クリックして承認''><click\\:run_command\\:''/party accept <sender_username>''><green>あなたは</green><sender_display_name><green>からパーティー「</green><party_name><green>」に招待されました。承認するにはこのメッセージをクリックして下さい。\ncommand.party.sent_invite=<recipient_display_name><green>にパーティーの招待状を送信しました。\ncommand.party.must_specify_invite=<red>あなたは誰の招待を承認するか指定しなければなりません。\ncommand.party.no_pending_invites=<red>あなたには保留中のパーティー招待状はありません。\ncommand.party.no_invite_from=<red>あなたは</red><sender_display_name><red>からの保留中の招待状はありません。\ncommand.party.joined_party=<green>パーティー「</green><party_name><green>」の参加に成功しました！\ncommand.party.left_party=<green>パーティー「</green><party_name><green>」の退出に成功しました！\ncommand.party.disbanded=<green>パーティー「</green><party_name><green>」の解散に成功しました！\ncommand.party.cannot_disband_multiple_members=<red>あなたは最後のメンバーではないため、パーティー「</red><party_name><red>」を解散できません。\ncommand.party.must_be_in_party=<red>このコマンドを使うには、パーティーに入っていなければなりません。パーティを作成するには「/party create」を、招待を承認するには「/party accept」を使用して下さい。\ncommand.party.cannot_invite_self=<red>自分を招待することはできません。\ncommand.party.description=現在参加しているパーティーのメンバー情報を確認する。\ncommand.party.create.description=新しいパーティーを作成する。\ncommand.party.invite.description=プレイヤーをあなたのパーティーに招待する。\ncommand.party.accept.description=パーティーへの招待を承認する。\ncommand.party.leave.description=現在のパーティーから退出する。\ncommand.party.already_in_party=<display_name><red>は既にあなたのパーティーに参加しています。\ncommand.party.disband.description=現在のパーティーを解散する。\nparty.player_joined=<display_name><green>があなたのパーティーに参加しました。\nparty.player_left=<display_name><green>があなたのパーティーを退出しました。\nparty.cannot_use_channel=<red>このチャンネルを使用するには、あなたはパーティーに参加する必要があります。\nconfig.reload.failed=<red>コンフィグのリロードに失敗\nconfig.reload.success=<green>コンフィグのリロードに成功\nerror.command.argument_parsing=<red>無効なコマンド引数： <gray><throwable_message>\nerror.command.command_execution=<hover\\:show_text\\:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    クリックしてコピー\"><click\\:copy_to_clipboard\\:<stacktrace>><red>このコマンドの実行中に内部エラーが発生しました。\nerror.command.invalid_player=入力したプレイヤー''<input>''が見つかりません\nerror.command.invalid_sender=<red>無効なコマンド送信者。<gray><sender_type> 型である必要があります\nerror.command.invalid_syntax=<red>無効なコマンド構文です。正しいコマンド構文\\: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>申し訳ありませんが、このコマンドを実行する権限がありません。これがエラーだと思われる場合は、サーバー管理者にお問い合わせください。\nerror.command.command_needs_player=<red>非プレイヤーがこのコマンドを実行するには、プレーヤーの引数を指定しなければなりません。\nignore.already_ignored=<red>あなたは既に<target>を無視しています\nignore.not_ignored=<red>あなたは<target>を無視していません\nignore.exempt=<red>あなたは<target>を無視する事はできません\nignore.invalid_target=<red>ターゲットが見つかりません\nignore.now_ignoring=<green>あなたは<target>を無視しています\nignore.no_longer_ignoring=<green>あなたは<target>を無視しなくなりました\nmute.alert.players=<red><target><red>がミュートされました\nmute.alert.target=<red>あなたはミュートされています\nmute.cannot_speak=<red>ミュート時は話すことができません\nmute.exempt=<red>そのプレイヤーはミュートされることを免除されています\nmute.info.muted=<red><target><red>はミュートされています。\nmute.info.not_muted=<red><target><gold>はミュートされていません\nmute.info.self.muted=<red>あなたはミュートされています\nmute.info.self.not_muted=<green>あなたはミュートされていません\nmute.no_target=<red>ミュートするプレイヤーが指定されていません。\nmute.spy.prefix=<red><hover\\:show_text\\:<red>ミュート</red>M</hover></red>\nmute.unmute.alert.players=<green><target><green>のミュートが解除されました\nmute.unmute.alert.target=<green>あなたのミュートが解除されました\nmute.unmute.no_target=<red>ミュート解除するプレイヤーが指定されていません。\nnickname.reset.others=<green><target></green>の<gold>のニックネームがリセットされました\nnickname.reset=<gold>あなたのニックネームがリセットされました\nnickname.set.others=<target><green>のニックネームを</green><nickname><green>に設定しました\nnickname.set=<green>あなたのニックネームが</green><nickname><green>に設定されました\nnickname.show.others.unset=<target><red>にはニックネームが設定されていません\nnickname.show.others=<target><green>のニックネームは</green><nickname><green>です\nnickname.show.unset=<red>あなたはニックネームが設定されていません\nnickname.show=<green>あなたのニックネームは</green><nickname><green>です\nnickname.error.character_limit=<red>ニックネーム「</red><nickname><red>」は文字数制限を超えています。 <min_length>〜<max_length> 文字に設定する必要があります。\nnickname.error.blacklist=<red>ニックネーム「</red><nickname></red>」は使用できません。他の名前を入力して下さい。\nreply.target.missing=<red>返信できる人がいません\nreply.target.self=<red>自分自身にプライベートメッセージを送信することはできません\nwhisper.console=<gold>[<green><sender_display_name></green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.continue.target_missing=<red>プライベートメッセージを送る相手がいません\nwhisper.error=<red>プライベートメッセージの送信に失敗しました。\nwhisper.from=<gold>[<green><sender_display_name></green>] -> [<green>あなた</green>] <message>\nwhisper.ignored_by_target=<red><target><red>はあなたを無視しています\nwhisper.ignoring_target=<red>あなたは<target>を無視しています\nwhisper.ignoring_all=<red>無視されている間はメッセージを送信できません！\nwhisper.to=<gold>[<green>あなた</green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.toggled.on=プライベートメッセージを受信しています。\nwhisper.toggled.off=プライベートメッセージを受信しなくなりました。\nchannel.radius.empty_recipients=<red>メッセージを送信できる人が近くには誰もいません。\nchannel.joined=<green>チャンネルに再加入しました</green>\nchannel.left=<red>あなたはチャンネルから退出しました</red>\nchannel.no_permission=<red>あなたにはこのチャンネルを使用する権限がありません</red>\nchannel.already_left=<red>あなたはすでにこのチャンネルから退出しています</red>\nchannel.not_left=<red>あなたはこのチャンネルから退出していません</red>\nchannel.not_found=<red>チャンネルが見つかりません</red>\npagination.page_out_of_range=<red><page> ページは範囲外です！<pages>ページまでです。\npagination.click_for_next_page=クリックして次のページへ\npagination.click_for_previous_page=クリックして前のページへ\npagination.footer=<gray>ページ <page><white>/</white><pages> <aqua><buttons>\nintegrations.towny.cannot_use_alliance_channel=<red>このチャンネルを利用するには、同盟に参加する必要があります。\nintegrations.towny.cannot_use_nation_channel=<red>このチャンネルを利用するには、国家に参加する必要があります。\nintegrations.towny.cannot_use_town_channel=<red>このチャンネルを利用するには、町に参加する必要があります。\nintegrations.mcmmo.cannot_use_party_channel=<red>このチャンネルを利用するには、mcMMOパーティーに参加する必要があります。\nintegrations.fuuid.cannot_use_faction_channel=<red>このチャンネルを利用するには、派閥に参加する必要があります。\nintegrations.fuuid.cannot_use_alliance_channel=<red>このチャンネルを利用するには、同盟に参加する必要があります。\nintegrations.fuuid.cannot_use_truce_channel=<red>このチャンネルを利用するには、他の派閥と休戦する必要があります。\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-nl_NL.properties",
    "content": "channel.change=<green>Je stuurt nu berichten naar </green><channel>\ncommand.clearchat.description=Wist het chatvenster voor alle spelers.\ncommand.continue.argument.message=Het te verzenden bericht.\ncommand.continue.description=Stuurt een bericht naar de laatste persoon die je een bericht hebt gestuurd.\ncommand.debug.argument.player=De speler om de groepen van te controleren.\ncommand.debug.description=Toont de groepen van de spelers.\ncommand.help.argument.query=De zoekopdracht.\ncommand.help.description=Carbon command lijst.\ncommand.help.misc.arguments=Argumenten\ncommand.help.misc.available_commands=Beschikbare command''s\ncommand.help.misc.click_for_next_page=Klik voor de volgende pagina\ncommand.help.misc.click_for_previous_page=Klik voor de vorige pagina\ncommand.help.misc.click_to_show_help=Klik om hulp voor deze command te tonen\ncommand.help.misc.description=Beschrijving\ncommand.help.misc.help=Hulp\ncommand.help.misc.no_description=Geen beschrijving\ncommand.help.misc.no_results_for_query=Geen zoekresultaten\ncommand.help.misc.optional=Optioneel\ncommand.help.misc.page_out_of_range=Fout\\: Pagina <page> is niet in bereik. Moet tussen bereik [1, <max_pages>] zijn\ncommand.help.misc.showing_results_for_query=Zoekresultaten voor query weergeven\ncommand.ignore.argument.player=De naam van de speler om te negeren.\ncommand.ignore.argument.uuid=Het UUID van de speler om te negeren.\ncommand.ignore.description=Verbergt alle inkomende berichten van genegeerde spelers.\ncommand.mute.argument.player=De naam van de speler om te negeren.\ncommand.mute.argument.uuid=Het UUID van de speler om te negeren.\ncommand.mute.description=Mute spelers, vermijd dat ze chat gebruiken of andere spelers een prive bericht sturen.\ncommand.muteinfo.argument.player=De naam van de speler om te muten.\ncommand.muteinfo.argument.uuid=Het UUID van de speler om te negeren.\ncommand.muteinfo.description=Laat zien of spelers gemute zijn of niet.\ncommand.nickname.argument.nickname=De in te stellen bijnaam.\ncommand.nickname.description=Zet en laat speler bijnaam zien.\ncommand.reload.description=Herlaadt Carbon''s configuratie, kanaalinstellingen en vertalingen. Dit zal geen kanalen laden of ontladen.\ncommand.reply.argument.message=Het bericht waarmee u wilt antwoorden.\ncommand.reply.description=Stuurt een bericht naar de laatste persoon waar je een bericht van hebt ontvangen.\ncommand.unignore.argument.player=De naam van de speler om te stoppen met negeren.\ncommand.unignore.argument.uuid=Het UUID van de speler om te stoppen met negeren.\ncommand.unignore.description=Stopt het verbergen van berichten van de opgegeven speler.\ncommand.unmute.argument.player=De naam van de speler om te stoppen met negeren.\ncommand.unmute.argument.uuid=Het UUID van de speler om te stoppen met negeren.\ncommand.unmute.description=Un-mute spelers, geef ze toegang om chat te gebruiken en andere spelers een prive bericht te sturen.\ncommand.whisper.argument.message=Het te verzenden bericht.\ncommand.whisper.argument.player=De naam van de speler om een bericht naar te sturen.\ncommand.whisper.description=Stuurt een privébericht naar de opgegeven speler.\nconfig.reload.failed=<red>Configuratie kon niet herladen worden\nconfig.reload.success=<green>Configuratie herladen geslaagd\nerror.command.argument_parsing=<red>Ongeldig command argument\\: <gray><throwable_message>\nerror.command.command_execution=<hover\\:show_text\\:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    Klik om te kopiëren \"><click\\:copy_to_clipboard\\:<stacktrace>><red>Er is een interne fout opgetreden tijdens het proberen van deze command.\nerror.command.invalid_player=Geen speler gevonden voor invoer ''<input>''\nerror.command.invalid_sender=<red>Ongeldige command zender. Je moet van type <gray><senderType> zijn\nerror.command.invalid_syntax=<red>Ongeldige command syntax. Het correcte command syntax\\: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>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.\nignore.exempt=<red>Je kan <target> niet negeren\nignore.invalid_target=<red>Geen doel gevonden\nignore.now_ignoring=<green>Je bent nu <target> aan het negeren\nignore.no_longer_ignoring=<green>Je bent <target> niet langer aan het negeren\nmute.alert.players=<red><target> <red>is gemute\nmute.alert.target=<red>Je bent gemute\nmute.cannot_speak=<red>Je kan niet praten wanneer je gemute bent\nmute.exempt=<red>Die speler is vrijgesteld van gemute te worden\nmute.info.muted=<red><target> <red>is gemute\nmute.info.not_muted=<red><target> <gold>is niet gemute\nmute.info.self.muted=<red>Je bent gemute\nmute.info.self.not_muted=<green>Je bent niet gemute\nmute.no_target=<red>Er is geen speler opgegeven om te muten.\nmute.spy.prefix=<red><hover\\:show_text\\:<red>Gemute</red>M</hover></red>\nmute.unmute.alert.players=<green><target> <green>is gemute\nmute.unmute.alert.target=<green>Je bent niet langer gemute\nmute.unmute.no_target=<red>Geen speler gespecifieerd om te umuten.\nnickname.reset.others=<green><target></green><gold>''s bijnaam is opnieuw ingesteld\nnickname.reset=<gold>Jou bijnaam is opnieuw ingesteld\nnickname.set.others=<green>Je hebt </green><target><green>''s bijnaam ingesteld op </green><nickname>\nnickname.set=<green>Je bijnaam is ingesteld op </green><nickname>\nnickname.show.others.unset=<target><red> heeft geen bijnaam ingesteld\nnickname.show.others=<target><green>''s bijnaam is </green><nickname>\nnickname.show.unset=<red>Je hebt geen bijnaam ingesteld\nnickname.show=<green>Je bijnaam is </green><nickname>\nreply.target.missing=<red>Je hebt niemand om te beantwoorden\nreply.target.self=<red>Je kan jezelf geen privebericht sturen\nwhisper.continue.target_missing=<red>Je hebt niemand om een privebericht naar te sturen\nwhisper.ignored_by_target=<red><target> <red>is je aan het negeren\nwhisper.ignoring_target=<red>Jij negeert <target>\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-nn_NO.properties",
    "content": ""
  },
  {
    "path": "common/src/main/resources/locale/messages-no_NO.properties",
    "content": ""
  },
  {
    "path": "common/src/main/resources/locale/messages-pl_PL.properties",
    "content": ""
  },
  {
    "path": "common/src/main/resources/locale/messages-pt_BR.properties",
    "content": ""
  },
  {
    "path": "common/src/main/resources/locale/messages-ru_RU.properties",
    "content": ""
  },
  {
    "path": "common/src/main/resources/locale/messages-tr_TR.properties",
    "content": "channel.change=<green>Artık <channel> ile iletişim kuruyorsunuz.</green>\ncommand.clearchat.description=Tüm oyuncuların sohbet penceresini temizler.\ncommand.continue.argument.message=Gönderilecek mesaj.\ncommand.continue.description=En son iletişim kurduğunuz kişiye bir mesaj gönderir.\ncommand.debug.argument.player=Gruplarını kontrol etmek istediğiniz oyuncu.\ncommand.debug.description=Oyuncuların izin gruplarını gösterir.\ncommand.help.argument.query=Arama sorgusu.\ncommand.help.description=Carbon komut listesi.\ncommand.help.misc.arguments=Argümanlar\ncommand.help.misc.available_commands=Kullanılabilir Komutlar\ncommand.help.misc.click_for_next_page=Sonraki sayfa için tıklayın\ncommand.help.misc.click_for_previous_page=Önceki sayfa için tıklayın\ncommand.help.misc.click_to_show_help=Bu komut için yardımı göstermek için tıklayın\ncommand.help.misc.command=Komut\ncommand.help.misc.description=Açıklama\ncommand.help.misc.help=Yardım\ncommand.help.misc.no_description=Açıklama yok\ncommand.help.misc.no_results_for_query=Sorgu için sonuç bulunamadı\ncommand.help.misc.optional=İsteğe bağlı\ncommand.help.misc.page_out_of_range=Hata\\: Sayfa <page>, aralıkta değil. Aralık [1, <max_pages>] içinde olmalıdır.\ncommand.help.misc.showing_results_for_query=Sorgu için sonuçları gösteriliyor\ncommand.ignore.argument.player=Ignore edilecek oyuncunun adı.\ncommand.ignore.argument.uuid=Ignore edilecek oyuncunun UUID''si.\ncommand.ignore.description=Ignore edilen oyunculardan gelen tüm iletileri gizler.\ncommand.ignorelist.description=Ignore listesindeki oyuncuların sayfalandırılmış bir listesini gösterir.\ncommand.ignorelist.none_ignored=<green>Hiçbir oyuncuyu ignore etmiyorsunuz.\ncommand.ignorelist.pagination_header=<bold>Ignore Edilen Oyuncular\ncommand.ignorelist.pagination_element= - <hover\\:show_text\\:''Kullanıcı Adı\\: <username>''><display_name></hover> <click\\:run_command\\:''/unignore <username>''><hover\\:show_text\\:''<username> kullanıcısını unignore etmek için tıklayın''><gray>[<white>unignore</white>]</gray>\ncommand.join.description=Daha önce ayrıldığınız bir kanala katılın.\ncommand.leave.description=Şu anda erişiminiz olan bir kanaldan ayrılın.\ncommand.mute.argument.player=Susturulacak oyuncunun adı.\ncommand.mute.argument.uuid=Susturulacak oyuncunun UUID''si.\ncommand.mute.description=Oyuncuları susturur, onların sohbeti kullanmalarını veya diğer oyunculara fısıltı göndermelerini engeller.\ncommand.muteinfo.argument.player=Oyuncunun adı.\ncommand.muteinfo.argument.uuid=Oyuncunun UUID''si.\ncommand.muteinfo.description=Oyuncuların susturulup susturulmadığını gösterir.\ncommand.nickname.argument.nickname=Ayarlanacak takma ad.\ncommand.nickname.argument.player=Hedef oyuncunun adı.\ncommand.nickname.description=Takma adınızı gösterir.\ncommand.nickname.set.description=Takma adınızı ayarlar.\ncommand.nickname.reset.description=Takma adınızı kaldırır.\ncommand.nickname.others.description=Oyuncu takma adlarını gösterir.\ncommand.nickname.others.set.description=Oyuncu takma adlarını ayarlar.\ncommand.nickname.others.reset.description=Hedeften ayarlanmış herhangi bir takma adı kaldırır.\ncommand.reload.description=Carbon''un yapılandırma, kanal ayarları ve çevirilerini yeniden yükler. Kanalları yüklemeye veya boşaltmaya gitmez.\ncommand.reply.argument.message=Yanıt olarak gönderilecek ileti.\ncommand.reply.description=Size en son mesaj gönderen oyuncuya bir ileti gönderir.\ncommand.togglemsg.description=Diğer oyuncuların size mesaj göndermesine izin verir veya engeller.\ncommand.unignore.argument.player=Ignore''un kaldırılacağı oyuncunun adı.\ncommand.unignore.argument.uuid=Ignore''un kaldırılacağı oyuncunun UUID''si.\ncommand.unignore.description=Belirtilen oyuncunun mesajlarını gizlemeyi bırakır.\ncommand.unmute.argument.player=Susturmanın kaldırılacağı oyuncunun adı.\ncommand.unmute.argument.uuid=Susturmanın kaldırılacağı oyuncunun UUID''si.\ncommand.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.\ncommand.updateusername.argument.player=Güncellenecek oyuncunun adı.\ncommand.updateusername.argument.uuid=Oyuncunun UUID''si.\ncommand.updateusername.description=Oyuncunun adını Mojang adıyla eşleşecek şekilde günceller.\ncommand.updateusername.fetching=Kullanıcı adı alınıyor...\ncommand.updateusername.notupdated=Kullanıcı adı alınamıyor.\ncommand.updateusername.updated=<newname>''nin kullanıcı adı güncellendi\\!\ncommand.whisper.argument.message=Gönderilecek ileti.\ncommand.whisper.argument.player=İletişim kurmak istediğiniz oyuncunun adı.\ncommand.whisper.description=Belirtilen oyuncuya özel bir ileti gönderir.\nconfig.reload.failed=<red>Yapılandırma yeniden yüklenemedi.\nconfig.reload.success=<green>Yapılandırma başarıyla yeniden yüklendi.\nerror.command.argument_parsing=<red>Geçersiz komut argümanı\\: <gray><throwable_message>\nerror.command.command_execution=<hover\\:show_text\\:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    Kopyalamak için tıklayın\"><click\\:copy_to_clipboard\\:<stacktrace>><red>Komutu çalıştırmaya çalışırken içsel bir hata oluştu.\nerror.command.invalid_player=''<input>'' için oyuncu bulunamadı.\nerror.command.invalid_sender=<red>Geçersiz komut gönderen. Tür <gray><sender_type> olmalıdır.\nerror.command.invalid_syntax=<red>Geçersiz komut sözdizimi. Doğru komut sözdizimi\\: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>Ü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.\nerror.command.command_needs_player=<red>Oyuncuların bu komutu çalıştırmak için oyuncu argümanı sağlaması gerekir.\nignore.already_ignored=<red>Zaten <target>''i ignore ediyorsunuz.\nignore.not_ignored=<red><target>''i ignore etmiyorsunuz.\nignore.exempt=<red>Bu hedef ignore edilemez.\nignore.invalid_target=<red>Hedef bulunamadı.\nignore.now_ignoring=<green>Artık <target>''i ignore ediyorsunuz.</green>\nignore.no_longer_ignoring=<green>Artık <target>''i ignore etmiyorsunuz.</green>\nmute.alert.players=<red><target> <red>susturuldu.\nmute.alert.target=<red>Siz susturuldunuz.\nmute.cannot_speak=<red>Susturulduğunuzda konuşamazsınız.\nmute.exempt=<red>Bu oyuncunun susturulmasını engelleyemezsiniz.\nmute.info.muted=<red><target> <red>susturulmuş durumda.\nmute.info.not_muted=<red><target> <gold>susturulmamış durumda.\nmute.info.self.muted=<red>Siz susturuldunuz.\nmute.info.self.not_muted=<green>Siz susturulmamış durumdasınız.\nmute.no_target=<red>Susturulacak belirli bir oyuncu yok.\nmute.spy.prefix=<red><hover\\:show_text\\:''<red>Susturuldu</red>''>S</hover></red>\nmute.unmute.alert.players=<green><target> <green>susturulması kaldırıldı.\nmute.unmute.alert.target=<green>Siz artık susturulmamış durumdasınız.\nmute.unmute.no_target=<red>Susturulacak belirli bir oyuncu yok.\nnickname.reset.others=<green><target></green><gold>''in takma adı sıfırlandı.\nnickname.reset=<gold>Takma adınız sıfırlandı.\nnickname.set.others=<green>Siz </green><target><green>''in takma adını </green><nickname> olarak ayarladınız.\nnickname.set=<green>Takma adınız </green><nickname> olarak ayarlandı.\nnickname.show.others.unset=<target><red>''in ayarlanmış bir takma adı yok.</red>\nnickname.show.others=<target><green>''in takma adı </green><nickname>.\nnickname.show.unset=<red>Sizin ayarlanmış bir takma adınız yok.</red>\nnickname.show=<green>Takma adınız </green><nickname>.\nreply.target.missing=<red>Cevap verecek kimse bulunmamaktadır.</red>\nreply.target.self=<red>Kendinize fısıltı gönderemezsiniz.</red>\nwhisper.continue.target_missing=<red>Yanıt verecek kimse bulunmamaktadır.</red>\nwhisper.error=<red>Özel ileti gönderme başarısız oldu.</red>\nwhisper.ignored_by_target=<red><target></red> <red>sizin mesajlarınızı görmezden geliyor.</red>\nwhisper.ignoring_target=<red><target> sizin mesajlarınızı görmezden geliyor.</red>\nwhisper.ignoring_all=<red>Ignore ediliyor durumdayken mesaj gönderemezsiniz\\!</red>\nwhisper.toggled.on=Artık özel mesajlar alıyorsunuz.\nwhisper.toggled.off=Artık özel mesajlar almıyorsunuz.\nchannel.radius.empty_recipients=<red>Bir mesaj göndermek için kimseye yeterince yakın değilsiniz.</red>\nchannel.joined=<green>Kanala tekrar katıldınız</green>.\nchannel.left=<red>Kanaldan ayrıldınız</red>.\nchannel.no_permission=<red>Bu kanalı kullanma izniniz yok</red>.\nchannel.already_left=<red>Zaten bu kanaldan ayrıldınız</red>.\nchannel.not_left=<red>Bu kanaldan ayrılmadınız</red>.\nchannel.not_found=<red>Kanal bulunamadı</red>.\npagination.page_out_of_range=<red>Sayfa <page> aralık dışında\\! Sadece <pages> sayfa var.</red>.\npagination.click_for_next_page=Sıradaki sayfa için tıklayın.\npagination.click_for_previous_page=Önceki sayfa için tıklayın.\npagination.footer=<gray>Sayfa <page><white>/</white><pages> <aqua><buttons></aqua></gray>.\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-uk_UA.properties",
    "content": "channel.change=<green>Тепер ви пишете в </green><channel>\ncommand.clearchat.description=Очищає вікно чату для всіх гравців.\ncommand.continue.argument.message=Повідомлення для відправлення.\ncommand.continue.description=Надсилає повідомлення останній особі, якій ви писали.\ncommand.debug.argument.player=The player to check the groups of.\ncommand.debug.description=Показує групи дозволів гравців.\ncommand.filter.optional.enabled=<green>>Додатковий фільтр чату увімкнено\\!\ncommand.filter.optional.disabled=<red>Додатковий фільтр чату вимкнено\\!\ncommand.filter.optional.description=Увімкнути або вимкнути додатковий фільтр чату.\ncommand.help.argument.query=Пошуковий запит.\ncommand.help.description=Список команд Carbon\ncommand.help.misc.arguments=Аргументи\ncommand.help.misc.available_commands=Доступні команди\ncommand.help.misc.click_for_next_page=Натисніть, щоб перейти на наступну сторінку\ncommand.help.misc.click_for_previous_page=Натисніть, щоб перейти на попередню сторінку\ncommand.help.misc.click_to_show_help=Натисніть, щоб показати довідку для цієї команди\ncommand.help.misc.command=Команда\ncommand.help.misc.description=Опис\ncommand.help.misc.help=Довідка\ncommand.help.misc.no_description=Немає опису\ncommand.help.misc.no_results_for_query=Немає результатів за запитом\ncommand.help.misc.optional=Додаткове\ncommand.help.misc.page_out_of_range=Помилка\\: Сторінка <page> не входить в діапазон. Вона повинна бути в діапазоні [1, <max_pages>]\ncommand.help.misc.showing_results_for_query=Показ результатів за запитом\ncommand.ignore.argument.player=Нікнейм гравця, якого буде проігноровано.\ncommand.ignore.argument.uuid=UUID гравця, якого буде проігноровано.\ncommand.ignore.description=Приховує всі вхідні повідомлення від ігнорованих гравців.\ncommand.ignorelist.description=Показує перелік гравців, яких ви ігноруєте.\ncommand.ignorelist.none_ignored=<green>Ви не ігноруєте жодних гравців.\ncommand.ignorelist.pagination_header=<bold>Ігноровані гравці\ncommand.ignorelist.pagination_element= - <display_name> <click\\:run_command\\:''/unignore <username>''><hover\\:show_text\\:''Натисніть, щоб перестати ігнорувати <username>''><gray>[<white>unignore</white>]</gray>\ncommand.join.description=Приєднатися до каналу, який ви раніше покинули.\ncommand.leave.description=Покинути канал, до якого ви маєте доступ.\ncommand.mute.argument.player=Нікнейм гравця, якого буде замучено.\ncommand.mute.argument.uuid=UUID гравця, якого буде замучено.\ncommand.mute.argument.duration=Тривалість муту.\ncommand.mute.description=Мутить гравців, забороняючи їм писати в чат або надсилати приватні повідомлення.\ncommand.muteinfo.argument.player=Нікнейм гравця.\ncommand.muteinfo.argument.uuid=UUID гравця.\ncommand.muteinfo.description=Показує, чи гравець в муті чи ні.\ncommand.nickname.argument.nickname=Нікнейм для встановлення.\ncommand.nickname.argument.player=Нікнейм цільового гравця.\ncommand.nickname.description=Показує ваш нікнейм.\ncommand.nickname.set.description=Встановлює ваш нікнейм.\ncommand.nickname.reset.description=Видаляє ваш нікнейм.\ncommand.nickname.others.description=Показує нікнейм гравця.\ncommand.nickname.others.set.description=Встановлює нікнейм гравцю.\ncommand.nickname.others.reset.description=Видаляє встановлений нікнейм цілі.\ncommand.reload.description=Перезавантажує конфігурацію Carbon, налаштування каналів і переклади. <red> Канали не будуть завантажені чи виватажені\ncommand.reply.argument.message=Повідомлення для відповіді.\ncommand.reply.description=Надсилає повідомлення останньому гравцю, який вам писав.\ncommand.togglemsg.description=Дозволяє або забороняє іншим гравцям писати вам.\ncommand.unignore.argument.player=Нікнейм гравця для зняття ігнорування.\ncommand.unignore.argument.uuid=UUID гравця для зняття ігнорування.\ncommand.unignore.description=Припиняє ігнорування зазначеного гравця.\ncommand.unmute.argument.player=Нікнейм гравця з якого буде знято мут.\ncommand.unmute.argument.uuid=UUID гравця з якого буде знято мут.\ncommand.unmute.description=Розблоковує гравців, дозволяючи їм писати.\ncommand.updateusername.argument.player=Нікнейм гравця, якому буде оновлено нікнейм.\ncommand.updateusername.argument.uuid=UUID гравця, якому буде оновлено нікнейм.\ncommand.updateusername.description=Оновлює нікнейм гравця відповідно до нікнейму Mojang.\ncommand.updateusername.fetching=Отримання нікнейму користувача...\ncommand.updateusername.notupdated=Не вдалося отримати нікнейм користувача.\ncommand.updateusername.updated=Оновлено нікнейм <newname>\\!\ncommand.whisper.argument.message=Повідомлення для відправлення.\ncommand.whisper.argument.player=Нікнейм гравця, якому надіслати повідомлення.\ncommand.whisper.description=Надсилає приватне повідомлення вказаному гравцю.\ncommand.party.pagination_header=<green>Учасники групи</green>\\:\ncommand.party.pagination_element=<online\\:''<green>''\\:''<gray>''> -<reset> <display_name>\ncommand.party.created=<green>Успішно створено й приєднано до групи ''</green><party_name><green>''\\!\ncommand.party.not_in_party=<red>Ви не в групі. Використайте ''/party create'' або ''/party accept''.\ncommand.party.current_party=<green>Ви в групі<white>\\:</white></green> <party_name>\ncommand.party.must_leave_current_first=<red>Спочатку покиньте поточну групу.\ncommand.party.name_too_long=<red>Назва групи надто довга.\ncommand.party.received_invite=<hover\\:show_text\\:''<green>Натисніть, щоб прийняти''><click\\:run_command\\:''/party accept <sender_username>''><green>Вас запросив до групи ''</green><party_name><green>'' гравець </green><sender_display_name><green>. Натисніть, щоб прийняти.\ncommand.party.sent_invite=<green>Запрошення надіслано гравцю </green><recipient_display_name><green>.\ncommand.party.must_specify_invite=<red>Вкажіть, чиє запрошення прийняти.\ncommand.party.no_pending_invites=<red>Немає активних запрошень.\ncommand.party.no_invite_from=<red>Немає запрошення від </red><sender_display_name><red>.\ncommand.party.joined_party=<green>Ви приєдналися до групи ''</green><party_name><green>''\\!\ncommand.party.left_party=<green>Ви покинули групу ''</green><party_name><green>''.\ncommand.party.disbanded=<green>Групу ''</green><party_name><green>'' розпущено.\ncommand.party.cannot_disband_multiple_members=<red>Не можна розпустити групу ''</red><party_name><red>'', ви не останній учасник.\ncommand.party.must_be_in_party=<red>Ви повинні бути в групі, щоб використовувати цю команду. Використайте ''/party create'' або ''/party accept''.\ncommand.party.cannot_invite_self=<red>Не можна запросити самого себе.\ncommand.party.description=Інформація про поточну групу.\ncommand.party.create.description=Створити нову групу.\ncommand.party.invite.description=Запросити гравця до групи.\ncommand.party.accept.description=рийняти запрошення до групи.\ncommand.party.leave.description=Покинути поточну групу.\ncommand.party.already_in_party=<display_name><red> вже у вашій групі.\ncommand.party.disband.description=Розпустити поточну групу.\ncommand.spy.enabled=<green>Режим шпигування увімкнено.\ncommand.spy.disabled=<red>Режим шпигування вимкнено.\ncommand.spy.description=Дозволяє гравцеві переглядати всі приватні повідомлення та повідомлення каналів, які він інакше не бачив би.\nduration.days=<days>д<hours>г<minutes>хв<seconds>с\nduration.hours=<hours>г<minutes>хв<seconds>с\nparty.player_joined=<display_name><green> приєднався до вашої групи.\nparty.player_left=<display_name><green> покинув вашу групу.\nparty.cannot_use_channel=<red>Ви повинні бути в групі, щоб використовувати цей канал.\nparty.spy=<red>Шпигун <party_name> <red>[<username>\\: <white><message><red>]\nconfig.reload.failed=<red>Не вдалося перезавантажити конфігурацію\nconfig.reload.success=<green>Конфігурацію успішно перезавантажено\nerror.command.argument_parsing=<red>Невірний аргумент команди\\: <gray><throwable_message>\nerror.command.command_execution=<hover\\:show_text\\:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    Натисніть, щоб скопіювати\"><click\\:copy_to_clipboard\\:''<stacktrace>''><red>Виникла внутрішня помилка під час виконання цієї команди.\nerror.command.invalid_player=Не знайдено гравця за запитом ''<input>''\nerror.command.invalid_sender=<red>Невірний відправник команди. Ви повинні бути типу <gray><sender_type>\nerror.command.invalid_syntax=<red>Невірний синтаксис команди. Правильний синтаксис\\: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>На жаль, у вас немає дозволу на виконання цієї команди.\\nЗверніться до адміністратора сервера, якщо вважаєте це помилкою.\nerror.command.command_needs_player=<red>Консоль повинна вказати аргумент player для виконання цієї команди.\nignore.already_ignored=<red>Ви вже ігноруєте <target>\nignore.not_ignored=<red>Ви не ігноруєте <target>\nignore.exempt=<red>Ви не можете ігнорувати <target>\nignore.invalid_target=<red>Ціль не знайдено\nignore.now_ignoring=<green>Ви тепер ігноруєте <target>\nignore.no_longer_ignoring=<green>Ви більше не ігноруєте <target>\nmute.alert.players=<red><target> <red>був замучений\nmute.alert.players.temp=<red><target> <red>був замучений на <duration>\nmute.alert.target=<red>Вас було замучено\nmute.alert.target.temp=<red>Вас було замучено на <duration>\nmute.cannot_speak=<red>Ви не можете писати, поки замучені\nmute.exempt=<red>Цього гравця не можна заглушити\nmute.info.muted=<red><target> <red>замучений\nmute.info.not_muted=<red><target> <gold>не замучений\nmute.info.self.muted=<red>Ви замучений\nmute.info.self.not_muted=<green>Ви не замучені\nmute.no_target=<red>Не вказано гравця для муту.\nmute.spy.prefix=red><hover\\:show_text\\:''<red>Замучений</red>''>M</hover></red>\nmute.unmute.alert.players=\\=<green><target> <green>розмучено\nmute.unmute.alert.target=<green>Вас було розмучено\nmute.unmute.no_target=<red>Не вказано гравця для розмуту.\nnickname.reset.others=<green><target></green><gold> — нікнейм скинуто\nnickname.reset=<gold>Ваш нікнейм скинуто\nnickname.set.others=<green><green>Ви встановили нікнейм </green><target><green> на </green><nickname>\nnickname.set=<green>Ваш нікнейм встановлено на </green><nickname>\nnickname.show.others.unset=<target><red> не має встановленого нікнейму\nnickname.show.others=<target><green> має нікнейм </green><nickname>\nnickname.show.unset=<red>У вас не встановлено нікнейм\nnickname.show=<green>Ваш нікнейм\\: </green><nickname>\nnickname.error.character_limit=<red>Нікнейм \"<nickname>\" перевищує допустиму довжину. Має бути від <min_length> до <max_length> символів.\nnickname.error.blacklist=<red>Нікнейм \"<nickname>\" заборонений. Будь ласка, виберіть інший.\nnickname.error.filter=<red>Нікнейм повинен складатися лише з літер та цифр\\!\nreply.target.missing=<red>Немає адресата для відповіді\nreply.target.self=<red>Ви не можете надсилати повідомлення самому собі\nwhisper.console=<gold>[<green><sender_display_name></green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.continue.target_missing=<<red>Немає адресата для приватного повідомлення\nwhisper.error=<red>Не вдалося надіслати приватне повідомлення\nwhisper.from=<click\\:suggest_command\\:''/whisper <sender_username> ''><hover\\:show_text\\:''Натисніть, щоб відповісти''><gold>[<green><sender_display_name></green>] -> [<green>Ви</green>] <message>\nwhisper.from.spy=<red>ШПИГУН [</red><green><sender_display_name></green>]<red> -> [</red><green><recipient_display_name></green><red>]</red> <message>\nwhisper.ignored_by_target=<red><target> <red>ігнорує вас\nwhisper.ignoring_target=<red>Ви ігноруєте <target>\nwhisper.ignoring_all=<red>Ви не можете надсилати повідомлення, поки ви всіх ігноруєте\\!\nwhisper.no_permission.receive=<red>Цей гравець не має дозволу отримувати повідомлення\\!\nwhisper.no_permission.send=<red>У вас немає дозволу на відправлення приватних повідомлень\\!\nwhisper.to=<click\\:suggest_command\\:''/whisper <recipient_username> ''><hover\\:show_text\\:''Натисніть, щоб написати ще повідомлення <recipient_display_name>''><gold>[<green>Ви</green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.toggled.on=Прийом приватних повідомлень увімкнено.\nwhisper.toggled.off=Прийом приватних повідомлень вимкнено.\nchannel.cooldown=<red>Ви зможете знову писати через <remaining> секунд\\!\nchannel.radius.empty_recipients=<red>Вас ніхто не почув\nchannel.radius.spy=<red>Шпигун [<username>\\: <white><message><red>]\nchannel.joined=<green>Ви знову приєдналися до каналу</green>\nchannel.left=<red>Ви покинули канал</red>\nchannel.no_permission=<red>У вас немає дозволу на використання цього каналу</red>\nchannel.already_left=<red>Ви вже покинули цей канал</red>\nchannel.not_left=<red>Ви не покидали цей канал</red>\nchannel.not_found=<red>Канал не знайдено</red>\npagination.page_out_of_range=<red>Сторінка <page> поза діапазоном\\! Всього сторінок\\: <pages>.\npagination.click_for_next_page=Натисніть, щоб перейти на наступну сторінку\npagination.click_for_previous_page=Натисніть, щоб перейти на попередню сторінку\npagination.footer=<gray>Сторінка <page><white>/</white><pages> <aqua><buttons>\nintegrations.towny.cannot_use_alliance_channel=<red>Ви повинні приєднатися до альянсу, щоб використовувати цей канал.\nintegrations.towny.cannot_use_nation_channel=<red>Ви повинні приєднатися до нації, щоб використовувати цей канал.\nintegrations.towny.cannot_use_town_channel=<red>Ви повинні приєднатися до міста, щоб використовувати цей канал.\nintegrations.mcmmo.cannot_use_party_channel=<red>Ви повинні приєднатися до групи mcMMO, щоб використовувати цей канал.\nintegrations.fuuid.cannot_use_faction_channel=<red>Ви повинні приєднатися до фракції, щоб використовувати цей канал.\nintegrations.fuuid.cannot_use_alliance_channel=<red>Ви повинні приєднатися до альянсу, щоб використовувати цей канал.\nintegrations.fuuid.cannot_use_truce_channel=<red>Ви повинні мати перемир’я з іншою фракцією, щоб використовувати цей канал.\nintegrations.fuuid.cannot_use_mod_channel=<red>Ви повинні бути модератором або адміністратором фракції, щоб використовувати цей канал.\nintegrations.plotsquared.cannot_use_plot_channel=<red>Ви повинні бути на ділянці, щоб використовувати цей канал.\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-zh_CN.properties",
    "content": "channel.change=<green>你正在 </green><channel><green> 频道上聊天</green>\ncommand.clearchat.description=清空所有玩家的聊天框.\ncommand.continue.argument.message=要发送的消息.\ncommand.continue.description=向你上次私信你的人发送消息.\ncommand.debug.argument.player=检查玩家的权限组.\ncommand.debug.description=显示玩家的权限组.\ncommand.help.argument.query=搜索查询.\ncommand.help.description=Carbon 命令列表.\ncommand.help.misc.arguments=参数\ncommand.help.misc.available_commands=可用命令\ncommand.help.misc.click_for_next_page=下一页\ncommand.help.misc.click_for_previous_page=上一页\ncommand.help.misc.click_to_show_help=点击显示此命令的帮助\ncommand.help.misc.command=命令\ncommand.help.misc.description=描述\ncommand.help.misc.help=帮助\ncommand.help.misc.no_description=无描述\ncommand.help.misc.no_results_for_query=没有结果\ncommand.help.misc.optional=可选\ncommand.help.misc.page_out_of_range=错误\\: 页面 <page> 不在范围内.必须在范围 [1, <max_pages>] 内\ncommand.help.misc.showing_results_for_query=显示搜索结果\ncommand.ignore.argument.player=要屏蔽的玩家名字.\ncommand.ignore.argument.uuid=要屏蔽的UUID.\ncommand.ignore.description=屏蔽指定玩家的消息.\ncommand.ignorelist.description=显示你屏蔽的玩家列表.\ncommand.ignorelist.none_ignored=<green>你没有屏蔽任何玩家.\ncommand.ignorelist.pagination_header=<bold>屏蔽列表\ncommand.ignorelist.pagination_element= - <display_name> <click\\:run_command\\:''/unignore <username>''><hover\\:show_text\\:''单击取消屏蔽<username>''><gray>[<white>取消屏蔽</white>]</gray>\ncommand.join.description=加入你上次离开的频道.\ncommand.leave.description=离开你当前的频道.\ncommand.mute.argument.player=要禁言的玩家.\ncommand.mute.argument.uuid=要禁言的UUID.\ncommand.mute.description=禁言指定玩家, 阻止指定玩家发送消息.\ncommand.muteinfo.argument.player=玩家.\ncommand.muteinfo.argument.uuid=UUID.\ncommand.muteinfo.description=显示玩家是否被禁言.\ncommand.nickname.argument.nickname=昵称.\ncommand.nickname.argument.player=玩家.\ncommand.nickname.description=显示你的昵称.\ncommand.nickname.set.description=设置你的昵称.\ncommand.nickname.reset.description=删除你的昵称.\ncommand.nickname.others.description=显示玩家的昵称.\ncommand.nickname.others.set.description=设置玩家的昵称.\ncommand.nickname.others.reset.description=重置玩家的昵称.\ncommand.reload.description=重载配置.\ncommand.reply.argument.message=要回复的消息.\ncommand.reply.description=向最近私信你的玩家发送消息.\ncommand.togglemsg.description=允许或禁止其他玩家向你发送消息.\ncommand.unignore.argument.player=要取消屏蔽的玩家.\ncommand.unignore.argument.uuid=要取消屏蔽的UUID.\ncommand.unignore.description=取消屏蔽指定玩家的消息.\ncommand.unmute.argument.player=要取消禁言的玩家.\ncommand.unmute.argument.uuid=要取消禁言的UUID.\ncommand.unmute.description=取消禁言指定玩家.\ncommand.updateusername.argument.player=要更新的玩家名称.\ncommand.updateusername.argument.uuid=要更新的玩家的 UUID.\ncommand.updateusername.description=更新玩家的用户名以匹配其正版名称.\ncommand.updateusername.fetching=获取用户名中...\ncommand.updateusername.notupdated=无法获取用户名.\ncommand.updateusername.updated=已更新 <newname> 的用户名\\!\ncommand.whisper.argument.message=要发送的私聊消息.\ncommand.whisper.argument.player=要发送消息的玩家名称.\ncommand.whisper.description=向指定玩家发送私聊消息.\ncommand.party.pagination_header=<green>队伍成员</green>\\: \ncommand.party.pagination_element=<online\\:''<green>''\\:''<gray>''> -<reset> <display_name>\ncommand.party.created=<green>成功创建并加入队伍''</green><party_name><green>''\\!\ncommand.party.not_in_party=<red>你不在任何队伍中.使用''/party create''创建一个队伍, 或使用''/party accept''接受邀请.\ncommand.party.current_party=<green>你在队伍<white>\\: </white></green><party_name>\ncommand.party.must_leave_current_first=<red>你必须先离开当前的队伍.\ncommand.party.name_too_long=<red>队伍名称太长.\ncommand.party.received_invite=<hover\\:show_text\\:''<green>单击接受''><click\\:run_command\\:''/party accept <sender_username>''><green>你收到来自队伍''</green><party_name><green>''的邀请, 由</green><sender_display_name><green>发送.点击此消息以接受.\ncommand.party.sent_invite=<green>已向 </green><recipient_display_name><green> 发送队伍邀请.\ncommand.party.must_specify_invite=<red>你必须指定邀请的人.\ncommand.party.no_pending_invites=<red>你没有任何待处理的组队邀请.\ncommand.party.no_invite_from=<red>你没有来自 </red><sender_display_name><red> 的待处理邀请.\ncommand.party.joined_party=<green>成功加入了队伍 ''</green><party_name><green>'' \\!\ncommand.party.left_party=<green>成功离开了队伍 ''</green><party_name><green>'' .\ncommand.party.disbanded=<green>成功解散了队伍 ''</green><party_name><green>'' .\ncommand.party.cannot_disband_multiple_members=<red>无法解散队伍 ''</red><party_name><red>'' , 因为你不是最后一个成员.\ncommand.party.must_be_in_party=<red>你必须加入一个队伍才能使用该指令.使用 ''/party create'' 创建一个队伍, 或使用 ''/party accept'' 接受邀请.\ncommand.party.cannot_invite_self=<red>你不能邀请自己.\ncommand.party.description=查看当前队伍的信息.\ncommand.party.create.description=创建一个新的队伍.\ncommand.party.invite.description=邀请玩家加入你的队伍.\ncommand.party.accept.description=接受队伍邀请.\ncommand.party.leave.description=离开当前队伍.\ncommand.party.already_in_party=<display_name> <red>已经在你的队伍中.\ncommand.party.disband.description=解散当前队伍.\nparty.player_joined=<display_name><green>加入了你的队伍.\nparty.player_left=<display_name><green>离开了你的队伍.\nparty.cannot_use_channel=<red>你必须加入一个队伍才能使用该频道.\nconfig.reload.failed=<red>配置加载失败\nconfig.reload.success=<green>配置加载成功\nerror.command.argument_parsing=<red>无效的命令参数\\: <gray><throwable_message>\nerror.command.command_execution=<hover\\:show_text\\:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    点击复制\"><click\\:copy_to_clipboard\\:''<stacktrace>''><red>执行此命令时发生内部错误.\nerror.command.invalid_player=未找到名为 ''<input>'' 的玩家\nerror.command.invalid_sender=<red>无效的命令发送者.你必须是类型为 <gray><sender_type> <red>的用户\nerror.command.invalid_syntax=<red>无效命令, 正确用法\\: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>你没有执行此命令的权限.\nerror.command.command_needs_player=<red>需要提供玩家参数才能执行此命令.\nignore.already_ignored=<red>你已经在屏蔽<target>\nignore.not_ignored=<red>你没有屏蔽<target>\nignore.exempt=<red>你不能屏蔽<target>\nignore.invalid_target=<red>未找到玩家\nignore.now_ignoring=<green>你现在正在屏蔽<target>\nignore.no_longer_ignoring=<green>你不再屏蔽<target>\nmute.alert.players=<red><target> <red>已被禁言\nmute.alert.target=<red>你已被禁言\nmute.cannot_speak=<red>你被禁言时不能发言\nmute.exempt=<red>该玩家不受禁言限制\nmute.info.muted=<red><target> <red>被禁言了\nmute.info.not_muted=<red><target> <gold>没有被禁言\nmute.info.self.muted=<red>你被禁言了\nmute.info.self.not_muted=<green>你没有被禁言\nmute.no_target=<red>没有指定要禁言的玩家.\nmute.spy.prefix=<red><hover\\:show_text\\:''<red>禁言</red>''>M</hover></red>\nmute.unmute.alert.players=<green><target> <green>已被取消禁言\nmute.unmute.alert.target=<green>你已被取消禁言\nmute.unmute.no_target=<red>没有指定要解除禁言的玩家.\nnickname.reset.others=<green>你已重置 <target> 的昵称</green>\nnickname.reset=<gold>你已重置自己的昵称\nnickname.set.others=<green>你将 <target> 的昵称设置为 <nickname></green>\nnickname.set=<green>你的昵称已设置为 </green><nickname>\nnickname.show.others.unset=<target> <red>未设置昵称\nnickname.show.others=<target> <green>的昵称是 </green><nickname>\nnickname.show.unset=<red>你未设置昵称\nnickname.show=<green>你的昵称是</green> <nickname>\nnickname.error.character_limit=<red>昵称 ''<nickname>'' 超过了字符限制, 必须设置为 <min_length>~<max_length> 个字符.\nnickname.error.blacklist=<red>昵称\"<nickname>\" 不允许使用, 请选择其他昵称.\nnickname.error.filter=<red>昵称必须由字母或数字组成\\!\nreply.target.missing=<red>没有回复\nreply.target.self=<red>你不能给自己发送私信\nwhisper.console=<gold>[<green><sender_display_name></green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.continue.target_missing=<red>无法继续发送私信\nwhisper.error=<red>发送私信失败\nwhisper.from=<click\\:suggest_command\\:''/whisper <sender_username> ''>hover\\:show_text\\:''单击开始回复''<gold>[<green><sender_display_name></green>] -> [<green>你</green>] <message>\nwhisper.ignored_by_target=<red><target> <red>已经屏蔽你的私信\nwhisper.ignoring_target=<red>你正在屏蔽 <target>\nwhisper.ignoring_all=<red>无法在他们被你屏蔽时发送消息\\!\nwhisper.to=<click\\:suggest_command\\:''/whisper <recipient_username> ''><hover\\:show_text\\:''点击开始给<recipient_display_name>发送消息''><gold>[<green>你</green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.toggled.on=你开启了私信的接收.\nwhisper.toggled.off=你关闭了私信的接收.\nchannel.cooldown=<red>请等待 <remaining> 秒后再聊天\\!\nchannel.radius.empty_recipients=<red>你附近没有人, 无法发送消息.\nchannel.joined=<green>你已重新加入该频道</green>\nchannel.left=<red>你已离开该频道</red>\nchannel.no_permission=<red>你没有权限进入该频道</red>\nchannel.already_left=<red>你已经离开了该频道</red>\nchannel.not_left=<red>你还未离开该频道</red>\nchannel.not_found=<red>找不到该频道</red>\npagination.page_out_of_range=<red>第 <page> 页超出范围, 总共只有 <pages> 页.\npagination.click_for_next_page=下一页\npagination.click_for_previous_page=上一页\npagination.footer=<gray>第 <page><white>/</white><pages> 页 <aqua><buttons>\nintegrations.towny.cannot_use_alliance_channel=<red>你必须加入一个联盟才能使用该频道.\nintegrations.towny.cannot_use_nation_channel=<red>你必须加入一个国家才能使用该频道.\nintegrations.towny.cannot_use_town_channel=<red>你必须加入一个城镇才能使用该频道.\nintegrations.mcmmo.cannot_use_party_channel=<red>你必须加入一个 mcMMO 队伍才能使用该频道.\nintegrations.fuuid.cannot_use_faction_channel=<red>你必须加入一个派系才能使用该频道.\nintegrations.fuuid.cannot_use_alliance_channel=<red>你必须加入一个联盟才能使用该频道.\nintegrations.fuuid.cannot_use_truce_channel=<red>你必须与另一个派系结成停战协议才能使用该频道.\n"
  },
  {
    "path": "common/src/main/resources/locale/messages-zh_TW.properties",
    "content": "channel.change=<green>你現在正在屏蔽 </green><channel>\ncommand.clearchat.description=清空所有玩家的聊天框.\ncommand.continue.argument.message=要發送的消息.\ncommand.continue.description=嚮你上次私信你的人發送消息.\ncommand.debug.argument.player=檢查玩家的權限組.\ncommand.debug.description=顯示玩家的權限組.\ncommand.help.argument.query=搜索查詢.\ncommand.help.description=Carbon 命令列錶.\ncommand.help.misc.arguments=參數\ncommand.help.misc.available_commands=可用命令\ncommand.help.misc.click_for_next_page=下一頁\ncommand.help.misc.click_for_previous_page=上一頁\ncommand.help.misc.click_to_show_help=點選顯示此命令的幫助\ncommand.help.misc.command=命令\ncommand.help.misc.description=描述\ncommand.help.misc.help=幫助\ncommand.help.misc.no_description=無描述\ncommand.help.misc.no_results_for_query=冇有結果\ncommand.help.misc.optional=可選\ncommand.help.misc.page_out_of_range=錯誤\\: 頁麵 <page> 不在範圍內.必須在範圍 [1, <max_pages>] 內\ncommand.help.misc.showing_results_for_query=顯示搜索結果\\: <query>\ncommand.ignore.argument.player=要屏蔽的玩家名字.\ncommand.ignore.argument.uuid=要屏蔽的UUID.\ncommand.ignore.description=屏蔽指定玩家的消息.\ncommand.ignorelist.description=顯示你屏蔽的玩家列錶.\ncommand.ignorelist.none_ignored=<green>你冇有屏蔽任何玩家.\ncommand.ignorelist.pagination_header=<bold>屏蔽列錶\ncommand.ignorelist.pagination_element= - <display_name> <click\\:run_command\\:''/unignore <username>''><hover\\:show_text\\:''單擊取消屏蔽<username>''><gray>[<white>取消屏蔽</white>]</gray>\ncommand.join.description=加入你上次離開的頻道.\ncommand.leave.description=離開你當前的頻道.\ncommand.mute.argument.player=要禁言的玩家.\ncommand.mute.argument.uuid=要禁言的UUID.\ncommand.mute.description=禁言指定玩家, 阻止指定玩家發送消息.\ncommand.muteinfo.argument.player=玩家.\ncommand.muteinfo.argument.uuid=UUID.\ncommand.muteinfo.description=顯示玩家是否被禁言.\ncommand.nickname.argument.nickname=昵稱.\ncommand.nickname.argument.player=玩家.\ncommand.nickname.description=顯示你的昵稱.\ncommand.nickname.set.description=設定你的昵稱.\ncommand.nickname.reset.description=刪除你的昵稱.\ncommand.nickname.others.description=顯示玩家的昵稱.\ncommand.nickname.others.set.description=設定玩家的昵稱.\ncommand.nickname.others.reset.description=重置玩家的昵稱.\ncommand.reload.description=重載配置.\ncommand.reply.argument.message=要回複的消息.\ncommand.reply.description=嚮最近私信你的玩家發送消息.\ncommand.togglemsg.description=允許或禁止其他玩家嚮你發送消息.\ncommand.unignore.argument.player=要取消屏蔽的玩家.\ncommand.unignore.argument.uuid=要取消屏蔽的UUID.\ncommand.unignore.description=取消屏蔽指定玩家的消息.\ncommand.unmute.argument.player=要取消禁言的玩家.\ncommand.unmute.argument.uuid=要取消禁言的UUID.\ncommand.unmute.description=取消禁言指定玩家.\ncommand.updateusername.argument.player=要更新的玩家名稱.\ncommand.updateusername.argument.uuid=要更新的玩家的 UUID.\ncommand.updateusername.description=更新玩家的用戶名以匹配其正版名稱.\ncommand.updateusername.fetching=獲取用戶名中...\ncommand.updateusername.notupdated=無法獲取用戶名.\ncommand.updateusername.updated=已更新 <newname> 的用戶名\\!\ncommand.whisper.argument.message=要發送的私聊消息.\ncommand.whisper.argument.player=要發送消息的玩家名稱.\ncommand.whisper.description=嚮指定玩家發送私聊消息.\ncommand.party.pagination_header=<green>隊伍成員</green>\\: \ncommand.party.pagination_element=<online\\:''<green>''\\:''<gray>''> -<reset> <display_name>\ncommand.party.created=<green>成功創建並加入隊伍''</green><party_name><green>''\\!\ncommand.party.not_in_party=<red>你不在任何隊伍中.使用''/party create''創建一個隊伍, 或使用''/party accept''接受邀請.\ncommand.party.current_party=<green>你在隊伍<white>\\: </white></green><party_name>\ncommand.party.must_leave_current_first=<red>你必須先離開當前的隊伍.\ncommand.party.name_too_long=<red>隊伍名稱太長.\ncommand.party.received_invite=<hover\\:show_text\\:''<green>單擊接受''><click\\:run_command\\:''/party accept <sender_username>''><green>你收到來自隊伍''</green><party_name><green>''的邀請, 由</green><sender_display_name><green>發送.點選此消息以接受.\ncommand.party.sent_invite=<green>已嚮 </green><recipient_display_name><green> 發送隊伍邀請.\ncommand.party.must_specify_invite=<red>你必須指定邀請的人.\ncommand.party.no_pending_invites=<red>你冇有任何待處理的組隊邀請.\ncommand.party.no_invite_from=<red>你冇有來自 </red><sender_display_name><red> 的待處理邀請.\ncommand.party.joined_party=<green>成功加入了隊伍 ''</green><party_name><green>'' \\!\ncommand.party.left_party=<green>成功離開了隊伍 ''</green><party_name><green>'' .\ncommand.party.disbanded=<green>成功解散了隊伍 ''</green><party_name><green>'' .\ncommand.party.cannot_disband_multiple_members=<red>無法解散隊伍 ''</red><party_name><red>'' , 因為你不是最後一個成員.\ncommand.party.must_be_in_party=<red>你必須加入一個隊伍才能使用該指令.使用 ''/party create'' 創建一個隊伍, 或使用 ''/party accept'' 接受邀請.\ncommand.party.cannot_invite_self=<red>你不能邀請自己.\ncommand.party.description=檢視當前隊伍的信息.\ncommand.party.create.description=創建一個新的隊伍.\ncommand.party.invite.description=邀請玩家加入你的隊伍.\ncommand.party.accept.description=接受隊伍邀請.\ncommand.party.leave.description=離開當前隊伍.\ncommand.party.already_in_party=<display_name> <red>已經在你的隊伍中.\ncommand.party.disband.description=解散當前隊伍.\nparty.player_joined=<display_name><green>加入了你的隊伍.\nparty.player_left=<display_name><green>離開了你的隊伍.\nparty.cannot_use_channel=<red>你必須加入一個隊伍才能使用該頻道.\nconfig.reload.failed=<red>配置加載失敗\nconfig.reload.success=<green>配置加載成功\nerror.command.argument_parsing=<red>無效的命令參數\\: <gray><throwable_message>\nerror.command.command_execution=<hover\\:show_text\\:\"<throwable_message>\\n<stacktrace>\\n<gray><italic>    點選複製\"><click\\:copy_to_clipboard\\:''<stacktrace>''><red>執行此命令時發生內部錯誤.\nerror.command.invalid_player=未找到名為 ''<input>'' 的玩家\nerror.command.invalid_sender=<red>無效的命令發送者.你必須是類型為 <gray><sender_type> <red>的用戶\nerror.command.invalid_syntax=<red>無效命令, 正確用法\\: <white>/</white><gray><syntax></gray>\nerror.command.no_permission=<red>你冇有執行此命令的權限.\nerror.command.command_needs_player=<red>需要提供玩家參數才能執行此命令.\nignore.already_ignored=<red>你已經在屏蔽<target>\nignore.not_ignored=<red>你冇有屏蔽<target>\nignore.exempt=<red>你不能屏蔽<target>\nignore.invalid_target=<red>未找到玩家\nignore.now_ignoring=<green>你現在正在屏蔽<target>\nignore.no_longer_ignoring=<green>你不再屏蔽<target>\nmute.alert.players=<red><target> <red>已被禁言\nmute.alert.target=<red>你已被禁言\nmute.cannot_speak=<red>你被禁言時不能發言\nmute.exempt=<red>該玩家不受禁言限製\nmute.info.muted=<red><target> <red>被禁言了\nmute.info.not_muted=<red><target> <gold>冇有被禁言\nmute.info.self.muted=<red>你被禁言了\nmute.info.self.not_muted=<green>你冇有被禁言\nmute.no_target=<red>冇有指定要禁言的玩家.\nmute.spy.prefix=<red><hover\\:show_text\\:''<red>禁言</red>''>M</hover></red>\nmute.unmute.alert.players=<green><target> <green>已被取消禁言\nmute.unmute.alert.target=<green>你已被取消禁言\nmute.unmute.no_target=<red>冇有指定要解除禁言的玩家.\nnickname.reset.others=<green>你已重置 <target> 的昵稱</green>\nnickname.reset=<gold>你已重置自己的昵稱\nnickname.set.others=<green>你將 <target> 的昵稱設定為 <nickname></green>\nnickname.set=<green>你的昵稱已設定為 </green><nickname>\nnickname.show.others.unset=<target> <red>未設定昵稱\nnickname.show.others=<target> <green>的昵稱是 </green><nickname>\nnickname.show.unset=<red>你未設定昵稱\nnickname.show=<green>你的昵稱是</green> <nickname>\nnickname.error.character_limit=<red>昵稱 ''<nickname>'' 超過了字符限製, 必須設定為 <min_length>~<max_length> 個字符.\nnickname.error.blacklist=<red>昵稱\"<nickname>\" 不允許使用, 請選擇其他昵稱.\nreply.target.missing=<red>冇有回複\nreply.target.self=<red>你不能給自己發送私信\nwhisper.console=<gold>[<green><sender_display_name></green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.continue.target_missing=<red>無法繼續發送私信\nwhisper.error=<red>發送私信失敗\nwhisper.from=<click\\:suggest_command\\:''/whisper <sender_username> ''>hover\\:show_text\\:''單擊開始回複''<gold>[<green><sender_display_name></green>] -> [<green>你</green>] <message>\nwhisper.ignored_by_target=<red><target> <red>已經屏蔽你的私信\nwhisper.ignoring_target=<red>你正在屏蔽 <target>\nwhisper.ignoring_all=<red>無法在他們被你屏蔽時發送消息\\!\nwhisper.to=<click\\:suggest_command\\:''/whisper <recipient_username> ''><hover\\:show_text\\:''點選開始給<recipient_display_name>發送消息''><gold>[<green>你</green>] -> [<green><recipient_display_name></green>] <message>\nwhisper.toggled.on=你開啓了私信的接收.\nwhisper.toggled.off=你關閉了私信的接收.\nchannel.radius.empty_recipients=<red>你附近冇有人, 無法發送消息.\nchannel.joined=<green>你已重新加入該頻道</green>\nchannel.left=<red>你已離開該頻道</red>\nchannel.no_permission=<red>你冇有權限進入該頻道</red>\nchannel.already_left=<red>你已經離開了該頻道</red>\nchannel.not_left=<red>你還未離開該頻道</red>\nchannel.not_found=<red>找不到該頻道</red>\npagination.page_out_of_range=<red>第 <page> 頁超出範圍, 總共隻有 <pages> 頁.\npagination.click_for_next_page=下一頁\npagination.click_for_previous_page=上一頁\npagination.footer=<gray>第 <page><white>/</white><pages> 頁 <aqua><buttons>\nintegrations.towny.cannot_use_alliance_channel=<red>你必須加入一個聯盟才能使用該頻道.\nintegrations.towny.cannot_use_nation_channel=<red>你必須加入一個國家才能使用該頻道.\nintegrations.towny.cannot_use_town_channel=<red>你必須加入一個城鎮才能使用該頻道.\nintegrations.mcmmo.cannot_use_party_channel=<red>你必須加入一個 mcMMO 隊伍才能使用該頻道.\nintegrations.fuuid.cannot_use_faction_channel=<red>你必須加入一個派係才能使用該頻道.\nintegrations.fuuid.cannot_use_alliance_channel=<red>你必須加入一個聯盟才能使用該頻道.\nintegrations.fuuid.cannot_use_truce_channel=<red>你必須與另一個派係結成停戰協議才能使用該頻道.\n"
  },
  {
    "path": "common/src/main/resources/queries/clear-ignores.sql",
    "content": "DELETE FROM carbon_ignores WHERE (id = :id);\n"
  },
  {
    "path": "common/src/main/resources/queries/clear-leftchannels.sql",
    "content": "DELETE FROM carbon_leftchannels WHERE (id = :id);\n"
  },
  {
    "path": "common/src/main/resources/queries/clear-party-members.sql",
    "content": "DELETE FROM carbon_party_members WHERE (partyid = :partyid);\n"
  },
  {
    "path": "common/src/main/resources/queries/drop-party-member.sql",
    "content": "DELETE FROM carbon_party_members WHERE (playerid = :playerid);\n"
  },
  {
    "path": "common/src/main/resources/queries/drop-party.sql",
    "content": "DELETE FROM carbon_parties WHERE (partyid = :partyid);\n"
  },
  {
    "path": "common/src/main/resources/queries/insert-party-member.sql",
    "content": "INSERT{!PSQL: IGNORE} INTO carbon_party_members (partyid, playerid) VALUES(:partyid, :playerid){PSQL: ON CONFLICT DO NOTHING};\n"
  },
  {
    "path": "common/src/main/resources/queries/insert-party.sql",
    "content": "INSERT INTO carbon_parties(\n    partyid,\n    name\n) VALUES (\n    :partyid,\n    :name\n);\n"
  },
  {
    "path": "common/src/main/resources/queries/insert-player.sql",
    "content": "INSERT{!PSQL: IGNORE} INTO carbon_users(\n    id,\n    muted,\n    muteexpiration,\n    deafened,\n    selectedchannel,\n    displayname,\n    lastwhispertarget,\n    whisperreplytarget,\n    spying,\n    ignoringdms,\n    party,\n    applycustomfilters\n) VALUES (\n    :id,\n    :muted,\n    :muteexpiration,\n    :deafened,\n    :selectedchannel,\n    :displayname,\n    :lastwhispertarget,\n    :whisperreplytarget,\n    :spying,\n    :ignoringdms,\n    :party,\n    :applycustomfilters\n){PSQL: ON CONFLICT DO NOTHING};\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/h2/V1__create_tables.sql",
    "content": "CREATE TABLE carbon_users (\n    `id` UUID NOT NULL PRIMARY KEY,\n    `muted` BOOLEAN,\n    `deafened` BOOLEAN,\n    `selectedchannel` VARCHAR(256),\n    `displayname` VARCHAR(1024),\n    `lastwhispertarget` UUID,\n    `whisperreplytarget` UUID,\n    `spying` BOOLEAN,\n    `ignoringdms` BOOLEAN\n);\n\nCREATE TABLE carbon_ignores (\n    `id` UUID NOT NULL,\n    `ignoredplayer` UUID NOT NULL,\n    PRIMARY KEY (id, ignoredplayer)\n);\n\nCREATE TABLE carbon_leftchannels (\n    `id` UUID NOT NULL,\n    `channel` VARCHAR(256) NOT NULL,\n    PRIMARY KEY (id, channel)\n);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/h2/V2__increase_nickname_size.sql",
    "content": "ALTER TABLE carbon_users MODIFY displayname VARCHAR(8192);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/h2/V3__parties.sql",
    "content": "CREATE TABLE carbon_party_members (\n    `partyid` UUID NOT NULL,\n    `playerid` UUID NOT NULL,\n    PRIMARY KEY (partyid, playerid)\n);\n\nCREATE TABLE carbon_parties (\n    `partyid` UUID NOT NULL PRIMARY KEY,\n    `name` VARCHAR(8192)\n);\n\nALTER TABLE carbon_users ADD COLUMN party UUID;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/h2/V4__filters.sql",
    "content": "ALTER TABLE carbon_users ADD COLUMN applycustomfilters BOOLEAN;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/h2/V5__tempmute.sql",
    "content": "ALTER TABLE carbon_users ADD COLUMN muteexpiration BOOLEAN AFTER muted;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/h2/V6__tempmute.sql",
    "content": "ALTER TABLE carbon_users ALTER COLUMN muteexpiration BIGINT;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/mysql/V10__tempmute.sql",
    "content": "ALTER TABLE carbon_users MODIFY COLUMN muteexpiration BIGINT;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/mysql/V1__create_tables.sql",
    "content": "CREATE TABLE carbon_users (\n    `id` BINARY(16) NOT NULL PRIMARY KEY,\n    `muted` BOOLEAN,\n    `deafened` BOOLEAN,\n    `selectedchannel` VARCHAR(256),\n    `username` VARCHAR(20),\n    `displayname` VARCHAR(1024),\n    `lastwhispertarget` BINARY(16),\n    `whisperreplytarget` BINARY(16),\n    `spying` BOOLEAN\n);\n\nCREATE TABLE carbon_ignores (\n    `id` BINARY(16) NOT NULL,\n    `ignoredplayer` BINARY(16) NOT NULL,\n    PRIMARY KEY (id, ignoredplayer)\n);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/mysql/V2__create_tables.sql",
    "content": "CREATE TABLE carbon_leftchannels (\n    `id` BINARY(16) NOT NULL,\n    `channel` BINARY(16) NOT NULL,\n    PRIMARY KEY (id, channel)\n);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/mysql/V3__fix_leftchannels.sql",
    "content": "DROP TABLE carbon_leftchannels;\nCREATE TABLE carbon_leftchannels (\n    `id` BINARY(16) NOT NULL,\n    `channel` VARCHAR(256) NOT NULL,\n    PRIMARY KEY (id, channel)\n);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/mysql/V4__drop_usernames.sql",
    "content": "ALTER TABLE carbon_users DROP COLUMN username;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/mysql/V5__add_dmtoggle.sql",
    "content": "ALTER TABLE carbon_users ADD COLUMN ignoringdms BOOLEAN;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/mysql/V6__increase_nickname_size.sql",
    "content": "ALTER TABLE carbon_users MODIFY displayname VARCHAR(8192);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/mysql/V7__parties.sql",
    "content": "CREATE TABLE carbon_party_members (\n    `partyid` BINARY(16) NOT NULL,\n    `playerid` BINARY(16) NOT NULL,\n    PRIMARY KEY (partyid, playerid)\n);\n\nCREATE TABLE carbon_parties (\n    `partyid` BINARY(16) NOT NULL PRIMARY KEY,\n    `name` VARCHAR(8192)\n);\n\nALTER TABLE carbon_users ADD COLUMN party BINARY(16);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/mysql/V8__filters.sql",
    "content": "ALTER TABLE carbon_users ADD COLUMN applycustomfilters BOOLEAN;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/mysql/V9__tempmute.sql",
    "content": "ALTER TABLE carbon_users ADD COLUMN muteexpiration BOOLEAN AFTER muted;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/postgresql/V10__tempmute.sql",
    "content": "ALTER TABLE carbon_users ALTER COLUMN muteexpiration TYPE BIGINT USING (CASE WHEN muteexpiration THEN 1 ELSE 0 END);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/postgresql/V1__create_tables.sql",
    "content": "CREATE TABLE carbon_users (\n    id UUID NOT NULL PRIMARY KEY,\n    muted BOOLEAN,\n    deafened BOOLEAN,\n    selectedchannel VARCHAR(256),\n    username VARCHAR(20),\n    displayname VARCHAR(1024),\n    lastwhispertarget UUID,\n    whisperreplytarget UUID,\n    spying BOOLEAN\n);\n\nCREATE TABLE carbon_ignores (\n    id UUID NOT NULL,\n    ignoredplayer UUID NOT NULL,\n    PRIMARY KEY (id, ignoredplayer)\n);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/postgresql/V2__create_tables.sql",
    "content": "CREATE TABLE carbon_leftchannels (\n    id UUID NOT NULL,\n    channel VARCHAR(100) NOT NULL,\n    PRIMARY KEY (id, channel)\n);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/postgresql/V3__fix_leftchannels.sql",
    "content": "ALTER TABLE carbon_leftchannels\n    ALTER COLUMN channel TYPE VARCHAR(256),\n    ALTER COLUMN channel SET NOT NULL;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/postgresql/V4__drop_usernames.sql",
    "content": "ALTER TABLE carbon_users DROP COLUMN username;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/postgresql/V5__add_dmtoggle.sql",
    "content": "ALTER TABLE carbon_users ADD COLUMN ignoringdms BOOLEAN;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/postgresql/V6__increase_nickname_size.sql",
    "content": "ALTER TABLE carbon_users ALTER COLUMN displayname TYPE VARCHAR(8192);\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/postgresql/V7__parties.sql",
    "content": "CREATE TABLE carbon_party_members (\n    partyid UUID NOT NULL,\n    playerid UUID NOT NULL,\n    PRIMARY KEY (partyid, playerid)\n);\n\nCREATE TABLE carbon_parties (\n    partyid UUID NOT NULL PRIMARY KEY,\n    name VARCHAR(8192)\n);\n\nALTER TABLE carbon_users ADD COLUMN party UUID;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/postgresql/V8__filters.sql",
    "content": "ALTER TABLE carbon_users ADD COLUMN applycustomfilters BOOLEAN;\n"
  },
  {
    "path": "common/src/main/resources/queries/migrations/postgresql/V9__tempmute.sql",
    "content": "ALTER TABLE carbon_users ADD COLUMN muteexpiration BOOLEAN;\n"
  },
  {
    "path": "common/src/main/resources/queries/save-ignores.sql",
    "content": "INSERT{!PSQL: IGNORE} INTO carbon_ignores (id, ignoredplayer) VALUES(:id, :ignoredplayer){PSQL: ON CONFLICT DO NOTHING};\n"
  },
  {
    "path": "common/src/main/resources/queries/save-leftchannels.sql",
    "content": "INSERT{!PSQL: IGNORE} INTO carbon_leftchannels (id, channel) VALUES(:id, :channel){PSQL: ON CONFLICT DO NOTHING};\n"
  },
  {
    "path": "common/src/main/resources/queries/select-ignores.sql",
    "content": "SELECT ignoredplayer FROM carbon_ignores WHERE (id = :id);\n"
  },
  {
    "path": "common/src/main/resources/queries/select-leftchannels.sql",
    "content": "SELECT channel FROM carbon_leftchannels WHERE (id = :id);\n"
  },
  {
    "path": "common/src/main/resources/queries/select-party-members.sql",
    "content": "SELECT playerid FROM carbon_party_members WHERE (partyid = :partyid);\n"
  },
  {
    "path": "common/src/main/resources/queries/select-party.sql",
    "content": "SELECT\n    partyid,\n    name\nFROM carbon_parties WHERE (partyid = :partyid);\n"
  },
  {
    "path": "common/src/main/resources/queries/select-player.sql",
    "content": "SELECT\n    id,\n    muted,\n    muteexpiration,\n    deafened,\n    selectedchannel,\n    displayname,\n    lastwhispertarget,\n    whisperreplytarget,\n    spying,\n    ignoringdms,\n    party,\n    applycustomfilters\nFROM carbon_users WHERE (id = :id);\n"
  },
  {
    "path": "common/src/main/resources/queries/update-player.sql",
    "content": "UPDATE carbon_users SET\n    muted = :muted,\n    muteexpiration = :muteexpiration,\n    deafened = :deafened,\n    selectedchannel = :selectedchannel,\n    displayname = :displayname,\n    lastwhispertarget = :lastwhispertarget,\n    whisperreplytarget = :whisperreplytarget,\n    spying = :spying,\n    ignoringdms = :ignoringdms,\n    party = :party,\n    applycustomfilters = :applycustomfilters\nWHERE (id = :id);\n"
  },
  {
    "path": "crowdin.yml",
    "content": "files:\n  - source: common/src/main/resources/locale/messages-en_US.properties\n    translation: /common/src/main/resources/locale/messages-%locale_with_underscore%.properties\nbundles:\n  - 1\n"
  },
  {
    "path": "fabric/build.gradle.kts",
    "content": "import xyz.jpenilla.resourcefactory.fabric.Environment\nimport java.util.function.Predicate\nimport kotlin.io.path.invariantSeparatorsPathString\n\nplugins {\n  id(\"carbon.shadow-platform\")\n  id(\"quiet-fabric-loom\")\n  alias(libs.plugins.resource.factory.fabric.convention)\n}\n\nval shade: Configuration by configurations.creating\n\nconfigurations.implementation {\n  extendsFrom(shade)\n}\n\ndependencies {\n  minecraft(libs.fabricMinecraft)\n  mappings(loom.officialMojangMappings())\n  modImplementation(libs.fabricLoader)\n  modImplementation(libs.fabricApi)\n  modRuntimeOnly(libs.fabricApiDeprecated) // LuckPerms needs to work at dev time\n\n  shade(projects.carbonchatCommon) {\n    exclude(\"net.kyori\", \"adventure-api\")\n    exclude(\"net.kyori\", \"adventure-text-serializer-gson\")\n    exclude(\"net.kyori\", \"adventure-text-serializer-plain\")\n    exclude(\"org.incendo\", \"cloud-core\")\n    exclude(\"org.incendo\", \"cloud-services\")\n    exclude(\"org.incendo\", \"cloud-brigadier\")\n    exclude(\"org.incendo\", \"cloud-minecraft-signed-arguments\")\n    exclude(\"io.leangen.geantyref\")\n  }\n\n  modImplementation(libs.cloudFabric) {\n    exclude(\"net.fabricmc.fabric-api\")\n  }\n  include(libs.cloudFabric)\n  implementation(libs.cloudSigned)\n  include(libs.cloudSigned)\n  modImplementation(libs.fabricPermissionsApi)\n  include(libs.fabricPermissionsApi)\n\n  modImplementation(libs.adventurePlatformFabric)\n\n  modImplementation(libs.miniplaceholders)\n\n  runtimeDownload(libs.mysql)\n  include(libs.jarRelocator)\n  runtimeOnly(libs.jarRelocator) {\n    isTransitive = false\n  }\n  runtimeDownload(libs.checkerQual)\n}\n\nfabricModJson {\n  id = rootProject.name.lowercase()\n  name = rootProject.name\n  version = project.version.toString()\n  description = project.description\n  author(\"Draycia\")\n  author(\"jmp\")\n  contact {\n    homepage = GITHUB_REPO_URL\n    sources = GITHUB_REPO_URL\n    issues = \"$GITHUB_REPO_URL/issues\"\n  }\n  license(\"GPLv3\")\n  environment = Environment.ANY\n  mainEntrypoint(\"net.draycia.carbon.fabric.CarbonFabricBootstrap\")\n  mixin(\"carbonchat.mixins.json\")\n  depends(\"fabricloader\", \">=\" + libs.versions.fabricLoader.get())\n  depends(\"fabric-api\", \"*\")\n  depends(\"cloud\", \"*\")\n  depends(\"adventure-platform-fabric\", \"*\")\n  depends(\"minecraft\", \">=${libs.versions.minecraft.get()}\")\n  depends(\"luckperms\", \">=5.0.0\")\n  suggests(\"miniplaceholders\", \"*\")\n}\n\ncarbonPlatform {\n  productionJar = tasks.remapJar.flatMap { it.archiveFile }\n}\n\ntasks {\n  shadowJar {\n    configurations = listOf(shade)\n    relocateDependency(\"org.incendo.cloud.minecraft.extras\")\n    standardRuntimeRelocations()\n    relocateGuice()\n    relocateDependency(\"org.checkerframework\")\n  }\n  writeDependencies {\n    standardRuntimeRelocations()\n    relocateGuice()\n    relocateDependency(\"org.checkerframework\")\n  }\n\n  runServer {\n    dependsOn(shadowJar)\n    classpathFilter = Predicate {\n      val s = it.toPath().toAbsolutePath().invariantSeparatorsPathString\n      !s.contains(\"build/libs\") && !s.contains(\"build/classes\") && !s.contains(\"build/resources\")\n    }\n    doFirst {\n      val jar = shadowJar.get().archiveFile.get().asFile\n      val mods = file(\"run/mods\")\n      mods.mkdirs()\n      jar.copyTo(mods.resolve(\"carbonchat-dev.jar\"), overwrite = true)\n    }\n  }\n}\n\npublishMods.modrinth {\n  minecraftVersions.set(listOf(libs.versions.minecraft.get()))\n  modLoaders.addAll(\"fabric\")\n  requires(\"fabric-api\")\n  requires(\"adventure-platform-mod\")\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/CarbonChatFabric.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.google.inject.Provider;\nimport com.google.inject.Singleton;\nimport java.util.List;\nimport java.util.concurrent.ScheduledExecutorService;\nimport java.util.function.Consumer;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.common.CarbonChatInternal;\nimport net.draycia.carbon.common.PeriodicTasks;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.command.ExecutionCoordinatorHolder;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.users.PlatformUserManager;\nimport net.draycia.carbon.common.users.ProfileCache;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.fabric.listeners.FabricChatHandler;\nimport net.draycia.carbon.fabric.listeners.FabricJoinQuitListener;\nimport net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;\nimport net.fabricmc.fabric.api.message.v1.ServerMessageEvents;\nimport net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.fabricmc.loader.api.entrypoint.EntrypointContainer;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic final class CarbonChatFabric extends CarbonChatInternal {\n\n    @Inject\n    private CarbonChatFabric(\n        final Injector injector,\n        final Logger logger,\n        final @PeriodicTasks ScheduledExecutorService periodicTasks,\n        final ProfileCache profileCache,\n        final ProfileResolver profileResolver,\n        final CarbonMessages carbonMessages,\n        final PlatformUserManager userManager,\n        final ExecutionCoordinatorHolder commandExecutor,\n        final CarbonServer carbonServer,\n        final CarbonEventHandler eventHandler,\n        final CarbonChannelRegistry channelRegistry,\n        final Provider<MessagingManager> messagingManagerProvider,\n        @SuppressWarnings(\"unused\") // Make sure it initializes now\n        final MinecraftServerHolder minecraftServerHolder\n    ) {\n        super(\n            injector,\n            logger,\n            periodicTasks,\n            profileCache,\n            profileResolver,\n            userManager,\n            commandExecutor,\n            carbonServer,\n            carbonMessages,\n            eventHandler,\n            channelRegistry,\n            messagingManagerProvider\n        );\n    }\n\n    public void onInitialize() {\n        this.init();\n\n        // Platform Listeners\n        this.registerChatListener();\n        this.registerServerLifecycleListeners();\n        this.registerPlayerStatusListeners();\n\n        this.loadAddonEntrypoints();\n    }\n\n    @SuppressWarnings({\"unchecked\", \"rawtypes\"})\n    private void loadAddonEntrypoints() {\n        final List<EntrypointContainer<Consumer>> containers = FabricLoader.getInstance().getEntrypointContainers(\"carbonchat\", Consumer.class);\n        for (final EntrypointContainer<Consumer> container : containers) {\n            try {\n                final Consumer<CarbonChat> entrypoint = container.getEntrypoint();\n                entrypoint.accept(this);\n            } catch (final Throwable t) {\n                this.logger().error(\"Failed to invoke 'carbonchat' entrypoint for addon mod '{}'\", container.getProvider().getMetadata().getId(), t);\n            }\n        }\n    }\n\n    private void registerChatListener() {\n        ServerMessageEvents.ALLOW_CHAT_MESSAGE.register(this.injector().getInstance(FabricChatHandler.class));\n    }\n\n    private void registerServerLifecycleListeners() {\n        ServerLifecycleEvents.SERVER_STARTED.register(server -> this.checkVersion());\n        ServerLifecycleEvents.SERVER_STOPPED.register(server -> this.shutdown());\n    }\n\n    private void registerPlayerStatusListeners() {\n        final FabricJoinQuitListener listener = this.injector().getInstance(FabricJoinQuitListener.class);\n        ServerPlayConnectionEvents.DISCONNECT.register(listener);\n        ServerPlayConnectionEvents.JOIN.register(listener);\n    }\n\n    public boolean luckPermsLoaded() {\n        return FabricLoader.getInstance().isModLoaded(\"luckperms\");\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/CarbonChatFabricModule.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric;\n\nimport com.google.inject.Provider;\nimport com.google.inject.Provides;\nimport com.google.inject.Singleton;\nimport com.mojang.brigadier.tree.CommandNode;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.Map;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.common.CarbonCommonModule;\nimport net.draycia.carbon.common.CarbonPlatformModule;\nimport net.draycia.carbon.common.DataDirectory;\nimport net.draycia.carbon.common.PlatformScheduler;\nimport net.draycia.carbon.common.RawChat;\nimport net.draycia.carbon.common.command.CommandSettings;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ExecutionCoordinatorHolder;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.messages.CarbonMessageRenderer;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.users.PlatformUserManager;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.common.util.CloudUtils;\nimport net.draycia.carbon.fabric.command.FabricCommander;\nimport net.draycia.carbon.fabric.command.FabricPlayerCommander;\nimport net.draycia.carbon.fabric.listeners.FabricChatHandler;\nimport net.draycia.carbon.fabric.users.CarbonPlayerFabric;\nimport net.draycia.carbon.fabric.users.FabricProfileResolver;\nimport net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;\nimport net.fabricmc.loader.api.FabricLoader;\nimport net.fabricmc.loader.api.ModContainer;\nimport net.kyori.adventure.key.Key;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.server.level.ServerPlayer;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.SenderMapper;\nimport org.incendo.cloud.fabric.FabricServerCommandManager;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonChatFabricModule extends CarbonPlatformModule {\n\n    private final Logger logger;\n    private final ModContainer modContainer;\n\n    CarbonChatFabricModule() {\n        final ModContainer modContainer = FabricLoader.getInstance().getModContainer(\"carbonchat\")\n            .orElseThrow(() -> new IllegalStateException(\"Could not find ModContainer for carbonchat.\"));\n        this.modContainer = modContainer;\n        this.logger = LogManager.getLogger(modContainer.getMetadata().getName());\n    }\n\n    @Provides\n    @Singleton\n    public CommandManager<Commander> commandManager(\n        final ExecutionCoordinatorHolder executionCoordinatorHolder,\n        final Provider<CarbonChatFabric> carbonChat,\n        final CarbonMessages carbonMessages\n    ) {\n        // Remove existing commands matching our commands or aliases\n        CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {\n            final Map<Key, CommandSettings> settings = carbonChat.get().injector().getInstance(ConfigManager.class).loadCommandSettings();\n            final Iterator<CommandNode<CommandSourceStack>> it = dispatcher.getRoot().getChildren().iterator();\n            while (it.hasNext()) {\n                final CommandNode<CommandSourceStack> next = it.next();\n                final String name = next.getName();\n                if (settings.values().stream().anyMatch(s -> s.name().equals(name) || Arrays.asList(s.aliases()).contains(name))) {\n                    it.remove();\n                }\n            }\n        });\n\n        final FabricServerCommandManager<Commander> commandManager = new FabricServerCommandManager<>(\n            executionCoordinatorHolder.executionCoordinator(),\n            SenderMapper.create(\n                commandSourceStack -> {\n                    if (commandSourceStack.getEntity() instanceof ServerPlayer) {\n                        return new FabricPlayerCommander(carbonChat.get(), commandSourceStack);\n                    }\n                    return FabricCommander.from(commandSourceStack);\n                },\n                commander -> ((FabricCommander) commander).commandSourceStack()\n            )\n        );\n\n        CloudUtils.decorateCommandManager(commandManager, carbonMessages, this.logger);\n\n        return commandManager;\n    }\n\n    @Override\n    protected void configurePlatform() {\n        this.install(new CarbonCommonModule());\n\n        this.bind(ModContainer.class).toInstance(this.modContainer);\n        this.bind(CarbonChat.class).to(CarbonChatFabric.class);\n        this.bind(Logger.class).toInstance(this.logger);\n        this.bind(Path.class).annotatedWith(DataDirectory.class).toInstance(FabricLoader.getInstance().getConfigDir().resolve(this.modContainer.getMetadata().getId()));\n        this.bind(CarbonServer.class).to(CarbonServerFabric.class);\n        this.bind(ProfileResolver.class).to(FabricProfileResolver.class);\n        this.bind(PlatformScheduler.class).to(FabricScheduler.class);\n        this.install(PlatformUserManager.PlayerFactory.moduleFor(CarbonPlayerFabric.class));\n        this.bind(CarbonMessageRenderer.class).to(FabricMessageRenderer.class);\n        this.bind(Key.class).annotatedWith(RawChat.class).toProvider(() -> FabricChatHandler.CHAT_TYPE_KEY);\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/CarbonFabricBootstrap.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric;\n\nimport com.google.inject.Guice;\nimport net.draycia.carbon.api.CarbonChatProvider;\nimport net.draycia.carbon.common.util.CarbonDependencies;\nimport net.fabricmc.api.ModInitializer;\nimport net.fabricmc.loader.api.FabricLoader;\nimport xyz.jpenilla.gremlin.runtime.platformsupport.FabricClasspathAppender;\n\npublic class CarbonFabricBootstrap implements ModInitializer {\n\n    @Override\n    public void onInitialize() {\n        new FabricClasspathAppender().append(\n            CarbonDependencies.resolve(\n                FabricLoader.getInstance().getConfigDir().resolve(\"carbonchat\").resolve(\"libraries\")\n            )\n        );\n\n        final CarbonChatFabric carbonChat = Guice.createInjector(new CarbonChatFabricModule())\n            .getInstance(CarbonChatFabric.class);\n        CarbonChatProvider.register(carbonChat);\n        carbonChat.onInitialize();\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/CarbonServerFabric.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.util.List;\nimport java.util.Objects;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport net.kyori.adventure.platform.modcommon.MinecraftServerAudiences;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic final class CarbonServerFabric implements CarbonServer, ForwardingAudience.Single {\n\n    private final MinecraftServerHolder serverHolder;\n    private final UserManager<?> userManager;\n\n    @Inject\n    private CarbonServerFabric(final MinecraftServerHolder serverHolder, final UserManager<?> userManager) {\n        this.serverHolder = serverHolder;\n        this.userManager = userManager;\n    }\n\n    @Override\n    public @NotNull Audience audience() {\n        return MinecraftServerAudiences.of(this.serverHolder.requireServer()).all();\n    }\n\n    @Override\n    public Audience console() {\n        return new ConsoleCarbonPlayer(this.serverHolder.requireServer().createCommandSourceStack());\n    }\n\n    @Override\n    public List<? extends CarbonPlayer> players() {\n        return this.serverHolder.requireServer().getPlayerList().getPlayers().stream()\n            .map(serverPlayer -> this.userManager.user(serverPlayer.getUUID()).getNow(null))\n            .filter(Objects::nonNull)\n            .toList();\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/FabricMessageRenderer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport io.github.miniplaceholders.api.MiniPlaceholders;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersIntegration;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil;\nimport net.draycia.carbon.common.messages.CarbonMessageRenderer;\nimport net.draycia.carbon.common.messages.RenderForTagResolver;\nimport net.draycia.carbon.common.messages.SourcedAudience;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic class FabricMessageRenderer extends CarbonMessageRenderer {\n\n    private final ConfigManager configManager;\n\n    @Inject\n    public FabricMessageRenderer(final ConfigManager configManager, final RenderForTagResolver.Factory renderForTagResolver) {\n        super(renderForTagResolver);\n        this.configManager = configManager;\n    }\n\n    @Override\n    public Component render(\n        final Audience receiver,\n        final String intermediateMessage,\n        final TagResolver.Builder tagResolver\n    ) {\n        final String placeholderResolvedMessage = this.configManager.primaryConfig().applyCustomPlaceholders(intermediateMessage);\n\n        final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig = MiniPlaceholdersUtil.miniPlaceholdersLoaded()\n            ? this.configManager.primaryConfig().integrations().config(MiniPlaceholdersIntegration.configMeta())\n            : null;\n\n        if (miniplaceholdersConfig != null) {\n            tagResolver.resolver(MiniPlaceholders.globalPlaceholders());\n\n            if (receiver instanceof SourcedAudience) {\n                tagResolver.resolver(MiniPlaceholders.audiencePlaceholders());\n                if (miniplaceholdersConfig.relationalPlaceholders) {\n                    tagResolver.resolver(MiniPlaceholders.relationalPlaceholders());\n                }\n            }\n        }\n\n        final Audience parseAudience = receiver instanceof SourcedAudience sourced\n            ? MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, sourced.recipient(), sourced.sender())\n            : receiver;\n\n        return MiniMessage.miniMessage().deserialize(placeholderResolvedMessage, parseAudience, tagResolver.build());\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/FabricScheduler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.PlatformScheduler;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic final class FabricScheduler implements PlatformScheduler {\n\n    private final MinecraftServerHolder serverHolder;\n\n    @Inject\n    private FabricScheduler(final MinecraftServerHolder serverHolder) {\n        this.serverHolder = serverHolder;\n    }\n\n    @Override\n    public void scheduleForPlayer(final CarbonPlayer carbonPlayer, final Runnable runnable) {\n        this.serverHolder.requireServer().execute(runnable);\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/MinecraftServerHolder.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.util.Objects;\nimport net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;\nimport net.minecraft.server.MinecraftServer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic final class MinecraftServerHolder {\n\n    private volatile @Nullable MinecraftServer server;\n\n    @Inject\n    private MinecraftServerHolder() {\n        ServerLifecycleEvents.SERVER_STARTING.register(server -> this.server = server);\n        ServerLifecycleEvents.SERVER_STOPPED.register(server -> this.server = null);\n    }\n\n    public MinecraftServer requireServer() {\n        return Objects.requireNonNull(this.server, \"server requested when not active\");\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/command/FabricCommander.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric.command;\n\nimport me.lucko.fabric.api.permissions.v0.Permissions;\nimport net.draycia.carbon.common.command.Commander;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport net.minecraft.commands.CommandSourceStack;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic interface FabricCommander extends Commander, ForwardingAudience.Single {\n\n    static FabricCommander from(final CommandSourceStack commandSourceStack) {\n        return new FabricCommanderImpl(commandSourceStack);\n    }\n\n    CommandSourceStack commandSourceStack();\n\n    @Override\n    default Audience audience() {\n        return this.commandSourceStack();\n    }\n\n    record FabricCommanderImpl(CommandSourceStack commandSourceStack) implements FabricCommander {\n\n        @Override\n        public boolean hasPermission(final String permission) {\n            return Permissions.check(this.commandSourceStack, permission, this.commandSourceStack.getServer().operatorUserPermissions().level());\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/command/FabricPlayerCommander.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric.command;\n\nimport com.mojang.brigadier.exceptions.CommandSyntaxException;\nimport me.lucko.fabric.api.permissions.v0.Permissions;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.server.level.ServerPlayer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static java.util.Objects.requireNonNull;\n\n@DefaultQualifier(NonNull.class)\npublic record FabricPlayerCommander(\n    CarbonChat carbon,\n    CommandSourceStack commandSourceStack\n) implements PlayerCommander, FabricCommander {\n\n    public ServerPlayer player() {\n        try {\n            return this.commandSourceStack.getPlayerOrException();\n        } catch (final CommandSyntaxException e) {\n            throw new IllegalStateException(\"FabricPlayerCommander was created for non-player CommandSourceStack!\", e);\n        }\n    }\n\n    @Override\n    public CarbonPlayer carbonPlayer() {\n        return requireNonNull(\n            this.carbon.userManager().user(this.player().getUUID()).join(),\n            \"No CarbonPlayer for logged in Player!\"\n        );\n    }\n\n    @Override\n    public boolean hasPermission(final String permission) {\n        return Permissions.check(this.commandSourceStack, permission, this.commandSourceStack.getServer().operatorUserPermissions().level());\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/listeners/FabricChatHandler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric.listeners;\n\nimport com.google.inject.Inject;\nimport java.util.Objects;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.event.events.CarbonChatEventImpl;\nimport net.draycia.carbon.common.event.events.CarbonEarlyChatEvent;\nimport net.draycia.carbon.common.listeners.ChatListenerInternal;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.fabric.CarbonChatFabric;\nimport net.draycia.carbon.fabric.users.CarbonPlayerFabric;\nimport net.fabricmc.fabric.api.message.v1.ServerMessageEvents;\nimport net.kyori.adventure.chat.SignedMessage;\nimport net.kyori.adventure.platform.modcommon.MinecraftServerAudiences;\nimport net.kyori.adventure.text.Component;\nimport net.minecraft.commands.CommandSourceStack;\nimport net.minecraft.core.Holder;\nimport net.minecraft.core.RegistryAccess;\nimport net.minecraft.core.registries.Registries;\nimport net.minecraft.network.chat.ChatType;\nimport net.minecraft.network.chat.FilterMask;\nimport net.minecraft.network.chat.OutgoingChatMessage;\nimport net.minecraft.network.chat.PlayerChatMessage;\nimport net.minecraft.resources.Identifier;\nimport net.minecraft.resources.ResourceKey;\nimport net.minecraft.server.level.ServerPlayer;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.jetbrains.annotations.Nullable;\n\npublic class FabricChatHandler extends ChatListenerInternal implements ServerMessageEvents.AllowChatMessage {\n\n    public static final Identifier CHAT_TYPE_KEY = Identifier.fromNamespaceAndPath(\"carbonchat\", \"chat\");\n\n    private final CarbonChatFabric carbonChat;\n    private @MonotonicNonNull ResourceKey<ChatType> chatTypeResourceKey;\n\n    @Inject\n    public FabricChatHandler(\n        final ConfigManager configManager,\n        final CarbonChatFabric carbonChat,\n        final CarbonMessages carbonMessages\n    ) {\n        super(carbonChat.eventHandler(), carbonMessages, configManager);\n        this.carbonChat = carbonChat;\n    }\n\n    @Override\n    public boolean allowChatMessage(final PlayerChatMessage chatMessage, final ServerPlayer serverPlayer, final ChatType.Bound bound) {\n        if (serverPlayer == null) {\n            return false;\n        }\n\n        final @Nullable CarbonPlayer sender = this.carbonChat.userManager().user(serverPlayer.getUUID()).join();\n\n        final MinecraftServerAudiences audiences = MinecraftServerAudiences.of(serverPlayer.level().getServer());\n        final SignedMessage signedMessage = audiences.asAdventure(chatMessage);\n        final Component originalMessage = Objects.requireNonNullElse(signedMessage.unsignedContent(), Component.text(signedMessage.message()));\n\n        final @Nullable CarbonEarlyChatEvent earlyChatEvent = this.prepareAndEmitPreChatEvent(sender, originalMessage);\n\n        if (earlyChatEvent == null || earlyChatEvent.cancelled()) {\n            return false;\n        }\n\n        final @Nullable Component message = this.parseTags(sender, earlyChatEvent.message());\n        final @Nullable CarbonChatEventImpl chatEvent = this.prepareAndEmitChatEvent(sender, message, audiences.asAdventure(chatMessage));\n\n        if (chatEvent == null || chatEvent.cancelled()) {\n            return false;\n        }\n\n        for (final var recipient : chatEvent.recipients()) {\n            final Component finishedMessage = chatEvent.renderFor(recipient);\n\n            final net.minecraft.network.chat.Component nativeMessage = audiences.nonWrappingSerializer().serialize(finishedMessage);\n            final PlayerChatMessage customChatMessage = new PlayerChatMessage(chatMessage.link(), chatMessage.signature(), chatMessage.signedBody(), nativeMessage, FilterMask.FULLY_FILTERED);\n            final RegistryAccess registryAccess = serverPlayer.level().registryAccess();\n            if (this.chatTypeResourceKey == null) {\n                this.chatTypeResourceKey = registryAccess.lookupOrThrow(Registries.CHAT_TYPE)\n                    .get(CHAT_TYPE_KEY)\n                    .flatMap(Holder.Reference::unwrapKey)\n                    .orElseThrow();\n            }\n            final ChatType.Bound customBound = ChatType.bind(this.chatTypeResourceKey, registryAccess, nativeMessage);\n\n            if (recipient instanceof CommandSourceStack recipientSource) {\n                recipientSource.sendChatMessage(new OutgoingChatMessage.Player(customChatMessage), false, customBound);\n            } else if (recipient instanceof CarbonPlayerFabric carbonPlayerFabric) {\n                carbonPlayerFabric.player().ifPresent(fabricPlayer -> {\n                    fabricPlayer.sendChatMessage(new OutgoingChatMessage.Player(customChatMessage), false, customBound);\n                });\n            }\n        }\n\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/listeners/FabricJoinQuitListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric.listeners;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Provider;\nimport java.util.List;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.messaging.packets.PacketFactory;\nimport net.draycia.carbon.common.users.ProfileCache;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport net.fabricmc.fabric.api.networking.v1.PacketSender;\nimport net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;\nimport net.minecraft.network.protocol.game.ClientboundCustomChatCompletionsPacket;\nimport net.minecraft.server.MinecraftServer;\nimport net.minecraft.server.network.ServerGamePacketListenerImpl;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static net.draycia.carbon.common.users.PlayerUtils.saveExceptionHandler;\n\n@DefaultQualifier(NonNull.class)\npublic class FabricJoinQuitListener implements ServerPlayConnectionEvents.Join, ServerPlayConnectionEvents.Disconnect {\n\n    private final ProfileCache profileCache;\n    private final Logger logger;\n    private final ConfigManager configManager;\n    private final UserManagerInternal<?> userManager;\n    private final Provider<MessagingManager> messaging;\n    private final PacketFactory packetFactory;\n\n    @Inject\n    public FabricJoinQuitListener(\n        final Logger logger,\n        final ConfigManager configManager,\n        final ProfileCache profileCache,\n        final UserManagerInternal<?> userManager,\n        final Provider<MessagingManager> messaging,\n        final PacketFactory packetFactory\n    ) {\n        this.logger = logger;\n        this.configManager = configManager;\n        this.profileCache = profileCache;\n        this.userManager = userManager;\n        this.messaging = messaging;\n        this.packetFactory = packetFactory;\n    }\n\n    @Override\n    public void onPlayReady(final ServerGamePacketListenerImpl handler, final PacketSender sender, final MinecraftServer server) {\n        this.profileCache.cache(handler.getPlayer().getUUID(), handler.getPlayer().getGameProfile().name());\n        this.messaging.get().queuePacket(() -> this.packetFactory.addLocalPlayerPacket(handler.getPlayer().getUUID(), handler.getPlayer().getGameProfile().name()));\n\n        final @Nullable List<String> suggestions = this.configManager.primaryConfig().customChatSuggestions();\n\n        if (suggestions == null || suggestions.isEmpty()) {\n            return;\n        }\n\n        sender.sendPacket(new ClientboundCustomChatCompletionsPacket(ClientboundCustomChatCompletionsPacket.Action.SET, suggestions));\n    }\n\n    @Override\n    public void onPlayDisconnect(final ServerGamePacketListenerImpl handler, final MinecraftServer server) {\n        this.userManager.loggedOut(handler.getPlayer().getGameProfile().id())\n            .exceptionally(saveExceptionHandler(this.logger, handler.getPlayer().getGameProfile().name(), handler.getPlayer().getGameProfile().id()));\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/users/CarbonPlayerFabric.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric.users;\n\nimport com.google.inject.Provider;\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Optional;\nimport me.lucko.fabric.api.permissions.v0.Permissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.InventorySlot;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.users.WrappedCarbonPlayer;\nimport net.draycia.carbon.common.util.EmptyAudienceWithPointers;\nimport net.draycia.carbon.fabric.CarbonChatFabric;\nimport net.draycia.carbon.fabric.MinecraftServerHolder;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport net.kyori.adventure.identity.Identity;\nimport net.kyori.adventure.platform.modcommon.MinecraftServerAudiences;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.format.TextColor;\nimport net.minecraft.server.level.ServerPlayer;\nimport net.minecraft.world.entity.EquipmentSlot;\nimport net.minecraft.world.item.ItemStack;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class CarbonPlayerFabric extends WrappedCarbonPlayer implements ForwardingAudience.Single {\n\n    private final MinecraftServerHolder serverHolder;\n    private final Provider<CarbonChatFabric> carbonChatFabric;\n\n    @AssistedInject\n    public CarbonPlayerFabric(final @Assisted CarbonPlayerCommon carbonPlayerCommon, final MinecraftServerHolder serverHolder, final Provider<CarbonChatFabric> carbonChatFabric) {\n        super(carbonPlayerCommon);\n        this.serverHolder = serverHolder;\n        this.carbonChatFabric = carbonChatFabric;\n    }\n\n    @Override\n    public @NonNull Audience audience() {\n        return this.player().map(p -> (Audience) p).orElseGet(() -> EmptyAudienceWithPointers.forCarbonPlayer(this));\n    }\n\n    public Optional<ServerPlayer> player() {\n        return Optional.ofNullable(\n            this.serverHolder.requireServer().getPlayerList()\n                .getPlayer(this.carbonPlayerCommon.uuid())\n        );\n    }\n\n    @Override\n    public boolean vanished() {\n        return false;\n    }\n\n    @Override\n    public @Nullable Locale locale() {\n        return this.player()\n            .flatMap(player -> player.get(Identity.LOCALE))\n            .orElseGet(Locale::getDefault);\n    }\n\n    @Override\n    public boolean online() {\n        return this.player().isPresent();\n    }\n\n    @Override\n    public double distanceSquaredFrom(final CarbonPlayer other) {\n        if (this.player().isEmpty()) {\n            return -1;\n        }\n\n        final @Nullable ServerPlayer player = this.player().orElse(null);\n        final @Nullable ServerPlayer otherPlayer = this.serverHolder.requireServer()\n            .getPlayerList().getPlayer(other.uuid());\n\n        if (player == null || otherPlayer == null) {\n            return -1;\n        }\n\n        final double deltaX = player.position().x() - otherPlayer.position().x();\n        final double deltaY = player.position().y() - otherPlayer.position().y();\n        final double deltaZ = player.position().z() - otherPlayer.position().z();\n\n        return (deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ);\n    }\n\n    @Override\n    public boolean sameWorldAs(final CarbonPlayer other) {\n        if (this.player().isEmpty()) {\n            return false;\n        }\n\n        final Optional<ServerPlayer> player = this.player();\n        final @Nullable ServerPlayer otherPlayer = this.serverHolder.requireServer()\n            .getPlayerList().getPlayer(other.uuid());\n\n        if (player.isEmpty() || otherPlayer == null) {\n            return false;\n        }\n\n        return player.get().level().equals(otherPlayer.level());\n    }\n\n    @Override\n    public @Nullable Component createItemHoverComponent(final InventorySlot slot) {\n        final Optional<ServerPlayer> playerOptional = this.player();\n        if (playerOptional.isEmpty()) {\n            return null;\n        }\n        final ServerPlayer player = playerOptional.get();\n\n        final EquipmentSlot equipmentSlot;\n\n        if (slot.equals(InventorySlot.MAIN_HAND)) {\n            equipmentSlot = EquipmentSlot.MAINHAND;\n        } else if (slot.equals(InventorySlot.OFF_HAND)) {\n            equipmentSlot = EquipmentSlot.OFFHAND;\n        } else if (slot.equals(InventorySlot.HELMET)) {\n            equipmentSlot = EquipmentSlot.HEAD;\n        } else if (slot.equals(InventorySlot.CHEST)) {\n            equipmentSlot = EquipmentSlot.CHEST;\n        } else if (slot.equals(InventorySlot.LEGS)) {\n            equipmentSlot = EquipmentSlot.LEGS;\n        } else if (slot.equals(InventorySlot.BOOTS)) {\n            equipmentSlot = EquipmentSlot.FEET;\n        } else {\n            return null;\n        }\n\n        final @Nullable ItemStack item = player.getItemBySlot(equipmentSlot);\n\n        if (item == null || item.isEmpty()) {\n            return null;\n        }\n\n        final int amount = Math.min(item.getCount(), 99);\n        final Component quantity = amount <= 1 ? Component.empty() : Component.text(\" x\" + amount);\n        final Component interim = MinecraftServerAudiences.of(player.level().getServer()).asAdventure(item.getDisplayName());\n\n        return Component.empty().append(\n                Component.text(\"[\"),\n                interim,\n                quantity,\n                Component.text(\"]\")\n            )\n            .hoverEvent(item)\n            .colorIfAbsent(TextColor.color(item.getRarity().color().getColor()));\n    }\n\n    @Override\n    public boolean hasPermission(final String permission) {\n        return this.player()\n            .map(player -> Permissions.check(player, permission, player.level().getServer().operatorUserPermissions().level()))\n            .orElse(false);\n    }\n\n    @Override\n    public String primaryGroup() {\n        if (!this.carbonChatFabric.get().luckPermsLoaded()) {\n            return \"default\";\n        }\n\n        return super.primaryGroup();\n    }\n\n    @Override\n    public List<String> groups() {\n        if (!this.carbonChatFabric.get().luckPermsLoaded()) {\n            return List.of(\"default\");\n        }\n\n        return super.groups();\n    }\n\n    @Override\n    protected Optional<Component> platformDisplayName() {\n        return this.player().flatMap(p -> p.get(Identity.DISPLAY_NAME));\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/java/net/draycia/carbon/fabric/users/FabricProfileResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.fabric.users;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.common.users.MojangProfileResolver;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.fabric.MinecraftServerHolder;\nimport net.minecraft.server.level.ServerPlayer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic final class FabricProfileResolver implements ProfileResolver {\n\n    private final MinecraftServerHolder serverHolder;\n    private final ProfileResolver mojang;\n\n    @Inject\n    private FabricProfileResolver(final MinecraftServerHolder serverHolder, final MojangProfileResolver mojang) {\n        this.serverHolder = serverHolder;\n        this.mojang = mojang;\n    }\n\n    @Override\n    public CompletableFuture<@Nullable UUID> resolveUUID(final String username, final boolean cacheOnly) {\n        final @Nullable ServerPlayer online = this.serverHolder.requireServer().getPlayerList().getPlayerByName(username);\n        if (online != null) {\n            return CompletableFuture.completedFuture(online.getUUID());\n        }\n\n        return this.mojang.resolveUUID(username, cacheOnly);\n    }\n\n    @Override\n    public CompletableFuture<@Nullable String> resolveName(final UUID uuid, final boolean cacheOnly) {\n        final @Nullable ServerPlayer online = this.serverHolder.requireServer().getPlayerList().getPlayer(uuid);\n        if (online != null) {\n            return CompletableFuture.completedFuture(online.getGameProfile().name());\n        }\n\n        return this.mojang.resolveName(uuid, cacheOnly);\n    }\n\n    @Override\n    public void shutdown() {\n        this.mojang.shutdown();\n    }\n\n}\n"
  },
  {
    "path": "fabric/src/main/resources/carbonchat.mixins.json",
    "content": "{\n  \"required\": true,\n  \"minVersion\": \"0.8.4\",\n  \"package\": \"net.draycia.carbon.fabric.mixin\",\n  \"compatibilityLevel\": \"JAVA_21\",\n  \"mixins\": [],\n  \"client\": [\n  ],\n  \"injectors\": {\n    \"defaultRequire\": 1\n  }\n}\n"
  },
  {
    "path": "fabric/src/main/resources/data/carbonchat/chat_type/chat.json",
    "content": "{\n    \"chat\": {\n        \"parameters\": [\n            \"content\"\n        ],\n        \"translation_key\": \"%s\"\n    },\n    \"narration\": {\n        \"parameters\": [\n            \"content\"\n        ],\n        \"translation_key\": \"%s\"\n    }\n}\n"
  },
  {
    "path": "fabric/src/main/resources/pack.mcmeta",
    "content": "{\n    \"pack\": {\n        \"pack_format\": 18,\n        \"description\": \"CarbonChat Data\"\n    }\n}\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[plugins]\nsponge-gradle = { id = \"org.spongepowered.gradle.plugin\", version = \"2.3.0\" }\nhangar-publish = { id = \"io.papermc.hangar-publish-plugin\", version = \"0.1.4\" }\nindra-publishing-sonatype = { id = \"net.kyori.indra.publishing.sonatype\", version.ref = \"indra\" }\njavadoc-links = { id = \"org.incendo.cloud-build-logic.javadoc-links\" }\ncloud-buildLogic-rootProject-publishing = { id = \"org.incendo.cloud-build-logic.publishing.root-project\", version.ref = \"cloud-build-logic\" }\nresource-factory-paper-convention = { id = \"xyz.jpenilla.resource-factory-paper-convention\", version.ref = \"resource-factory\" }\nresource-factory-bukkit-convention = { id = \"xyz.jpenilla.resource-factory-bukkit-convention\", version.ref = \"resource-factory\" }\nresource-factory-velocity-convention = { id = \"xyz.jpenilla.resource-factory-velocity-convention\", version.ref = \"resource-factory\" }\nresource-factory-fabric-convention = { id = \"xyz.jpenilla.resource-factory-fabric-convention\", version.ref = \"resource-factory\" }\n\n[versions]\nindra = \"4.0.0\"\ncloud-build-logic = \"0.0.17\"\nshadow = \"9.4.1\"\nmod-publish-plugin = \"1.1.0\"\ngremlin = \"0.0.9\"\nrunTask = \"3.0.2\"\nresource-factory = \"1.3.1\"\n\nadventure = \"4.18.0\"\ncloud = \"2.0.0\"\ncloudMinecraft = \"2.0.0-beta.15\"\ncloudModded = \"2.0.0-beta.15\"\ncloudSponge = \"2.0.0-SNAPSHOT\"\nconfigurate = \"4.2.0\"\ncheckerQual = \"3.49.2\"\nstylecheck = \"0.2.1\"\nbstats = \"3.1.0\"\npaperApi = \"1.21.4-R0.1-SNAPSHOT\"\npaperTrail = \"1.0.1\"\nfoliaApi = \"1.21.4-R0.1-SNAPSHOT\"\nevent = \"1.0.0\"\nregistry = \"1.0.0-SNAPSHOT\"\nkyoriMoonshine = \"2.0.4\"\nguice = \"7.0.0\"\nvelocityApi = \"3.5.0-SNAPSHOT\"\nminecraft = \"1.21.11\"\nfabricLoader = \"0.18.3\"\nfabricApi = \"0.139.5+1.21.11\"\nfabricPermissionsApi = \"0.6.1\"\nadventurePlatformFabric = \"6.8.0\"\nluckPermsApi = \"5.5\"\nessentialsx = \"2.20.1\"\ndiscordsrv = \"1.30.1\"\nplaceholderapi = \"2.11.6\"\nminiplaceholders = \"3.1.0\"\njdbi = \"3.49.6\"\nhikari = \"7.0.2\"\nmysql = \"9.7.0\"\nflyway = \"11.14.1\"\ncaffeine = \"3.2.3\"\nmariadb = \"3.5.7\"\nmessenger = \"1.1.0-SNAPSHOT\"\nzstdjni = \"1.5.7-6\"\njedis = \"7.0.0\"\npostgresql = \"42.7.8\"\nrabbitmq = \"5.27.0\"\nnats = \"2.23.0\"\nh2 = \"2.4.240\"\ntowny = \"0.101.2.5\"\nplotsquared_bom = \"1.55\"\nplotsquared_core = \"7.5.8\"\nmcmmo = \"2.2.043\"\nadpParties = \"3.2.9\"\nfuuid = \"1.6.9.5-U0.6.35\"\n\n# synced with version used by lowest supported mc (currently 1.21.4 on paper)\ngson = \"2.11.0\"\nguava = \"33.3.1-jre\"\nlog4j = \"2.24.1\"\nnetty = \"4.1.115.Final\"\n\n[libraries]\nindraCommon = { group = \"net.kyori\", name = \"indra-common\", version.ref = \"indra\" }\ncloud-build-logic = { module = \"org.incendo:cloud-build-logic\", version.ref = \"cloud-build-logic\" }\nindraLicenseHeader = { group = \"net.kyori\", name = \"indra-licenser-spotless\", version.ref = \"indra\" }\nshadow = { group = \"com.gradleup.shadow\", name = \"shadow-gradle-plugin\", version.ref = \"shadow\" }\nmod-publish-plugin = { module = \"me.modmuss50:mod-publish-plugin\", version.ref = \"mod-publish-plugin\" }\ngremlin-gradle = { group = \"xyz.jpenilla\", name = \"gremlin-gradle\", version.ref = \"gremlin\" }\nrun-task = { module = \"xyz.jpenilla:run-task\", version.ref = \"runTask\" }\n\nadventureBom = { group = \"net.kyori\", name = \"adventure-bom\", version.ref = \"adventure\" }\nadventureApi = { group = \"net.kyori\", name = \"adventure-api\" }\nadventureTextSerializerGson = { group = \"net.kyori\", name = \"adventure-text-serializer-gson\" }\nadventureTextSerializerPlain = { group = \"net.kyori\", name = \"adventure-text-serializer-plain\" }\nadventureTextSerializerLegacy = { group = \"net.kyori\", name = \"adventure-text-serializer-legacy\" }\nadventureSerializerConfigurate4 = { group = \"net.kyori\", name = \"adventure-serializer-configurate4\", version.ref = \"adventure\" }\nminimessage = { group = \"net.kyori\", name = \"adventure-text-minimessage\", version.ref = \"adventure\" }\nadventurePlatformFabric = { group = \"net.kyori\", name = \"adventure-platform-fabric\", version.ref = \"adventurePlatformFabric\" }\nlog4jBom = { group = \"org.apache.logging.log4j\", name = \"log4j-bom\", version.ref = \"log4j\" }\nlog4jApi = { group = \"org.apache.logging.log4j\", name = \"log4j-api\" }\nevent = { group = \"com.sasorio\", name = \"event-api\", version.ref = \"event\" }\nregistry = { group = \"com.seiama\", name = \"registry\", version.ref = \"registry\" }\nkyoriMoonshine = { group = \"net.kyori.moonshine\", name = \"moonshine\", version.ref = \"kyoriMoonshine\" }\nkyoriMoonshineCore = { group = \"net.kyori.moonshine\", name = \"moonshine-core\", version.ref = \"kyoriMoonshine\" }\nkyoriMoonshineStandard = { group = \"net.kyori.moonshine\", name = \"moonshine-standard\", version.ref = \"kyoriMoonshine\" }\n\ncloudBom = { module = \"org.incendo:cloud-bom\", version.ref = \"cloud\" }\ncloudCore = { group = \"org.incendo\", name = \"cloud-core\", version.ref = \"cloud\" }\nbrigadier = \"com.mojang:brigadier:1.0.18\"\ncloudMinecraftBom = { module = \"org.incendo:cloud-minecraft-bom\", version.ref = \"cloudMinecraft\" }\ncloudMinecraftExtras = { group = \"org.incendo\", name = \"cloud-minecraft-extras\", version.ref = \"cloudMinecraft\" }\ncloudPaper = { group = \"org.incendo\", name = \"cloud-paper\", version.ref = \"cloudMinecraft\" }\ncloudSigned = { group = \"org.incendo\", name = \"cloud-minecraft-signed-arguments\", version.ref = \"cloudMinecraft\" }\ncloudPaperSigned = { group = \"org.incendo\", name = \"cloud-paper-signed-arguments\", version.ref = \"cloudMinecraft\" }\ncloudSponge = { group = \"org.incendo\", name = \"cloud-sponge\", version.ref = \"cloudSponge\" }\ncloudVelocity = { group = \"org.incendo\", name = \"cloud-velocity\", version.ref = \"cloudMinecraft\" }\ncloudFabric = { group = \"org.incendo\", name = \"cloud-fabric\", version.ref = \"cloudModded\" }\n\nconfigurateCore = { group = \"org.spongepowered\", name = \"configurate-core\", version.ref = \"configurate\" }\nconfigurateHocon = { group = \"org.spongepowered\", name = \"configurate-hocon\", version.ref = \"configurate\" }\nconfigurateYaml = { group = \"org.spongepowered\", name = \"configurate-yaml\", version.ref = \"configurate\" }\n\ncheckerQual = { group = \"org.checkerframework\", name = \"checker-qual\", version.ref = \"checkerQual\" }\nstylecheck = { group = \"ca.stellardrift\", name = \"stylecheck\", version.ref = \"stylecheck\" }\ngson = { group = \"com.google.code.gson\", name = \"gson\", version.ref = \"gson\" }\nguava = { group = \"com.google.guava\", name = \"guava\", version.ref = \"guava\" }\nbstatsBukkit = { group = \"org.bstats\", name = \"bstats-bukkit\", version.ref = \"bstats\" }\nbstatsVelocity = { group = \"org.bstats\", name = \"bstats-velocity\", version.ref = \"bstats\" }\nbstatsSponge = { group = \"org.bstats\", name = \"bstats-sponge\", version.ref = \"bstats\" }\nguice = { group = \"com.google.inject\", name = \"guice\", version.ref = \"guice\" }\nassistedInject = { group = \"com.google.inject.extensions\", name = \"guice-assistedinject\", version.ref = \"guice\" }\njdbiCore = { group = \"org.jdbi\", name = \"jdbi3-core\", version.ref = \"jdbi\" }\njdbiObject = { group = \"org.jdbi\", name = \"jdbi3-sqlobject\", version.ref = \"jdbi\" }\njdbiPostgres = { group = \"org.jdbi\", name = \"jdbi3-postgres\", version.ref = \"jdbi\" }\nhikariCP = { group = \"com.zaxxer\", name = \"HikariCP\", version.ref = \"hikari\" }\nflyway = { group = \"org.flywaydb\", name = \"flyway-core\", version.ref = \"flyway\" }\nflywayMysql = { group = \"org.flywaydb\", name = \"flyway-mysql\", version.ref = \"flyway\" }\nflywayPostgres = { module = \"org.flywaydb:flyway-database-postgresql\", version.ref = \"flyway\" }\nmysql = { group = \"com.mysql\", name = \"mysql-connector-j\", version.ref = \"mysql\" }\npostgresql = { group = \"org.postgresql\", name = \"postgresql\", version.ref = \"postgresql\" }\ncaffeine = { group = \"com.github.ben-manes.caffeine\", name = \"caffeine\", version.ref = \"caffeine\" }\nmariadb = { group = \"org.mariadb.jdbc\", name = \"mariadb-java-client\", version.ref = \"mariadb\" }\nh2 = { group = \"com.h2database\", name = \"h2\", version.ref = \"h2\" }\njarRelocator = { group = \"me.lucko\", name = \"jar-relocator\", version = \"1.7\" }\nmessenger = { group = \"de.hexaoxi\", name = \"messenger-api\", version.ref = \"messenger\" }\nmessengerNats = { group = \"de.hexaoxi\", name = \"messenger-nats\", version.ref = \"messenger\" }\nmessengerRabbitmq = { group = \"de.hexaoxi\", name = \"messenger-rabbitmq\", version.ref = \"messenger\" }\nmessengerRedis = { group = \"de.hexaoxi\", name = \"messenger-redis\", version.ref = \"messenger\" }\nnetty = { group = \"io.netty\", name = \"netty-all\", version.ref = \"netty\" }\nzstdjni = { group = \"com.github.luben\", name = \"zstd-jni\", version.ref = \"zstdjni\" }\njedis = { group = \"redis.clients\", name = \"jedis\", version.ref = \"jedis\" }\nrabbitmq = { group = \"com.rabbitmq\", name = \"amqp-client\", version.ref = \"rabbitmq\" }\nnats = { group = \"io.nats\", name = \"jnats\", version.ref = \"nats\" }\npaperApi = { group = \"io.papermc.paper\", name = \"paper-api\", version.ref = \"paperApi\" }\npaperTrail = { group = \"io.papermc\", name = \"paper-trail\", version.ref = \"paperTrail\" }\nfoliaApi = { group = \"dev.folia\", name = \"folia-api\", version.ref = \"foliaApi\" }\nvelocityApi = { group = \"com.velocitypowered\", name = \"velocity-api\", version.ref = \"velocityApi\" }\nfabricMinecraft = { group = \"com.mojang\", name = \"minecraft\", version.ref = \"minecraft\" }\nfabricLoader = { group = \"net.fabricmc\", name = \"fabric-loader\", version.ref = \"fabricLoader\" }\nfabricApi = { group = \"net.fabricmc.fabric-api\", name = \"fabric-api\", version.ref = \"fabricApi\" }\nfabricApiDeprecated = { group = \"net.fabricmc.fabric-api\", name = \"fabric-api-deprecated\", version.ref = \"fabricApi\" }\nfabricPermissionsApi = { group = \"me.lucko\", name = \"fabric-permissions-api\", version.ref = \"fabricPermissionsApi\" }\ngremlin-runtime = { group = \"xyz.jpenilla\", name = \"gremlin-runtime\", version.ref = \"gremlin\" }\nluckPermsApi = { group = \"net.luckperms\", name = \"api\", version.ref = \"luckPermsApi\" }\nessentialsXDiscord = { group = \"net.essentialsx\", name = \"EssentialsXDiscord\", version.ref = \"essentialsx\" }\ndiscordsrv = { group = \"com.discordsrv\", name = \"discordsrv\", version.ref = \"discordsrv\" }\nplaceholderapi = { group = \"me.clip\", name = \"placeholderapi\", version.ref = \"placeholderapi\" }\nminiplaceholders = { group = \"io.github.miniplaceholders\", name = \"miniplaceholders-api\", version.ref = \"miniplaceholders\" }\ntowny = { group = \"com.palmergames.bukkit.towny\", name = \"towny\", version.ref = \"towny\" }\nplotsquaredbom = { group = \"com.intellectualsites.bom\", name = \"bom-newest\", version.ref = \"plotsquared_bom\" }\nplotsquaredcore = { group = \"com.intellectualsites.plotsquared\", name = \"plotsquared-core\", version.ref = \"plotsquared_core\" }\nmcmmo = { group = \"com.gmail.nossr50.mcMMO\", name = \"mcMMO\", version.ref = \"mcmmo\" }\nadpParties = { group = \"com.alessiodp.parties\", name = \"parties-api\", version.ref = \"adpParties\" }\nfactionsUuid = { group = \"com.massivecraft\", name = \"Factions\", version.ref = \"fuuid\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.5.0-bin.zip\nnetworkTimeout=10000\nretries=0\nretryBackOffMs=500\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project Properties\nprojectVersion=3.0.0-SNAPSHOT\ngroup=de.hexaoxi\ndescription=CarbonChat - A modern chat plugin\n\n# Gradle Properties\norg.gradle.caching=true\norg.gradle.parallel=true\norg.gradle.jvmargs=-Xmx2G\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables, and ensure extensions are enabled\r\nsetlocal EnableExtensions\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\n\"%COMSPEC%\" /c exit 1\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\n\"%COMSPEC%\" /c exit 1\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n@rem endlocal doesn't take effect until after the line is parsed and variables are expanded\r\n@rem which allows us to clear the local environment before executing the java command\r\nendlocal & \"%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\r\n\r\n:exitWithErrorLevel\r\n@rem Use \"%COMSPEC%\" /c exit to allow operators to work properly in scripts\r\n\"%COMSPEC%\" /c exit %ERRORLEVEL%\r\n"
  },
  {
    "path": "paper/build.gradle.kts",
    "content": "import xyz.jpenilla.resourcefactory.paper.PaperPluginYaml.Load\nimport xyz.jpenilla.runpaper.task.RunServer\n\nplugins {\n  id(\"carbon.shadow-platform\")\n  alias(libs.plugins.resource.factory.paper.convention)\n  alias(libs.plugins.resource.factory.bukkit.convention)\n  id(\"xyz.jpenilla.run-paper\")\n  id(\"carbon.permissions\")\n  id(\"carbon.configurable-plugins\")\n}\n\ndependencies {\n  implementation(projects.carbonchatCommon)\n\n  // Server\n  compileOnly(libs.foliaApi)\n  implementation(libs.paperTrail)\n\n  // Commands\n  implementation(libs.cloudPaper)\n  implementation(libs.cloudPaperSigned)\n\n  // Misc\n  implementation(libs.bstatsBukkit)\n\n  // Plugins\n  compileOnly(libs.placeholderapi)\n  compileOnly(libs.miniplaceholders)\n  compileOnly(libs.essentialsXDiscord) {\n    exclude(\"org.spigotmc\", \"spigot-api\")\n  }\n  compileOnly(libs.discordsrv) {\n    isTransitive = false\n  }\n  compileOnly(libs.towny)\n  compileOnly(libs.mcmmo) {\n    isTransitive = false\n  }\n  compileOnly(libs.adpParties)\n  compileOnly(libs.factionsUuid)\n  implementation(libs.plotsquaredbom)\n  compileOnly(libs.plotsquaredcore)\n}\n\nconfigurablePlugins {\n  dependency(libs.towny)\n  dependency(libs.mcmmo)\n  dependency(libs.adpParties)\n  dependency(libs.factionsUuid)\n  dependency(libs.plotsquaredbom)\n  dependency(libs.plotsquaredcore)\n}\n\ntasks {\n  shadowJar {\n    relocateDependency(\"io.papermc.papertrail\")\n    relocateDependency(\"io.leangen.geantyref\")\n    relocateDependency(\"xyz.jpenilla.reflectionremapper\")\n    relocateDependency(\"net.fabricmc.mappingio\")\n    relocateCloud()\n  }\n  val luckperms = FetchLuckPermsJar.setup(project, \"bukkit\")\n  withType(RunServer::class).configureEach {\n    version.set(\"1.21.4\")\n    downloadPlugins {\n      github(\"MiniPlaceholders\", \"MiniPlaceholders\", libs.versions.miniplaceholders.get(), \"MiniPlaceholders-Paper-${libs.versions.miniplaceholders.get()}.jar\")\n      // TODO: install MP extensions to its folder\n      // github(\"MiniPlaceholders\", \"PlaceholderAPI-Expansion\", \"2.1.0\", \"PlaceholderAPI-Expansion-2.1.0.jar\")\n      hangar(\"PlaceholderAPI\", libs.versions.placeholderapi.get())\n      modrinth(\"parties\", libs.versions.adpParties.get())\n    }\n    pluginJars.from(luckperms.flatMap { it.outputFile })\n    providers.gradleProperty(\"smokeTest\").map { it.toBoolean() }.getOrElse(false).let { smokeTest ->\n      if (smokeTest) {\n        runDirectory.set(layout.buildDirectory.dir(\"tmp/smokeTest\"))\n        doFirst {\n          runDirectory.get().file(\"carbonchat-smoketest\").asFile.takeIf { it.exists() }?.delete()\n          runDirectory.get().file(\"eula.txt\").asFile.also { it.parentFile.mkdirs() }.writeText(\"eula=true\")\n        }\n        doLast {\n          val pass = runDirectory.get().file(\"carbonchat-smoketest\").asFile.exists()\n          if (!pass) {\n            throw GradleException(\"Smoke test failed, please check the logs.\")\n          }\n        }\n        systemProperty(\"carbonchat.smokeTest\", true)\n        systemProperty(\"carbonchat.smokeTestMode\", providers.gradleProperty(\"smokeTestMode\").getOrElse(\"h2\"))\n        systemProperty(\"paper.disablePluginRemapping\", true)\n      }\n    }\n  }\n  register<RunServer>(\"runServer2\") {\n    pluginJars.from(shadowJar.flatMap { it.archiveFile })\n    runDirectory.set(layout.projectDirectory.dir(\"run2\"))\n  }\n}\n\nrunPaper.folia.registerTask()\n\npaperPluginYaml {\n  name = rootProject.name\n  loader = \"net.draycia.carbon.paper.CarbonPaperLoader\"\n  main = \"net.draycia.carbon.paper.CarbonPaperBootstrap\"\n  apiVersion = \"1.21.4\"\n  authors = listOf(\"Draycia\", \"jmp\")\n  website = GITHUB_REPO_URL\n  foliaSupported = true\n\n  dependencies {\n    server(\"LuckPerms\", Load.BEFORE, true)\n    server(\"PlaceholderAPI\", Load.BEFORE, false)\n    server(\"EssentialsDiscord\", Load.BEFORE, false)\n    server(\"DiscordSRV\", Load.BEFORE, false)\n    server(\"MiniPlaceholders\", Load.BEFORE, false)\n\n    // Integrations\n    server(\"Towny\", Load.BEFORE, false)\n    server(\"mcMMO\", Load.BEFORE, false)\n    server(\"Parties\", Load.BEFORE, false)\n    server(\"Factions\", Load.BEFORE, false)\n    server(\"PlotSquared\", Load.BEFORE, false)\n  }\n}\n\nbukkitPluginYaml {\n  name = rootProject.name\n  main = \"carbonchat.libs.io.papermc.papertrail.RequiresPaperPlugins\"\n  apiVersion = \"1.21.4\"\n  authors = listOf(\"Draycia\", \"jmp\")\n  website = GITHUB_REPO_URL\n}\n\ncarbonPermission.permissions.get().forEach {\n  setOf(bukkitPluginYaml.permissions, paperPluginYaml.permissions).forEach { container ->\n    container.register(it.string) {\n      description = it.description\n      it.children?.let { children.putAll(it) }\n    }\n  }\n}\n\npublishMods.modrinth {\n  modLoaders.addAll(\"paper\", \"folia\")\n}\n\nconfigurations.runtimeDownload {\n  exclude(\"org.checkerframework\", \"checker-qual\")\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/CarbonChatPaper.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.google.inject.Key;\nimport com.google.inject.Provider;\nimport com.google.inject.Singleton;\nimport com.google.inject.TypeLiteral;\nimport java.io.File;\nimport java.io.IOException;\nimport java.util.Set;\nimport java.util.concurrent.ScheduledExecutorService;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.common.CarbonChatInternal;\nimport net.draycia.carbon.common.PeriodicTasks;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.command.ExecutionCoordinatorHolder;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.config.MessagingSettings;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.users.PlatformUserManager;\nimport net.draycia.carbon.common.users.ProfileCache;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.paper.hooks.CarbonPAPIPlaceholders;\nimport net.draycia.carbon.paper.hooks.PAPIChatHook;\nimport org.apache.logging.log4j.LogManager;\nimport org.bstats.bukkit.Metrics;\nimport org.bstats.charts.SimplePie;\nimport org.bukkit.Bukkit;\nimport org.bukkit.event.Listener;\nimport org.bukkit.plugin.java.JavaPlugin;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic final class CarbonChatPaper extends CarbonChatInternal {\n\n    private static final int BSTATS_PLUGIN_ID = 8720;\n\n    private final JavaPlugin plugin;\n\n    @Inject\n    private CarbonChatPaper(\n        final Injector injector,\n        final JavaPlugin plugin,\n        final CarbonMessages carbonMessages,\n        final CarbonEventHandler eventHandler,\n        final CarbonChannelRegistry channelRegistry,\n        final Provider<MessagingManager> messagingManager,\n        final CarbonServer carbonServer,\n        final PlatformUserManager userManager,\n        @PeriodicTasks final ScheduledExecutorService periodicTasks,\n        final ProfileCache profileCache,\n        final ProfileResolver profileResolver,\n        final ExecutionCoordinatorHolder commandExecutor\n    ) {\n        super(\n            injector,\n            LogManager.getLogger(\"CarbonChat\"),\n            periodicTasks,\n            profileCache,\n            profileResolver,\n            userManager,\n            commandExecutor,\n            carbonServer,\n            carbonMessages,\n            eventHandler,\n            channelRegistry,\n            messagingManager\n        );\n        this.plugin = plugin;\n    }\n\n    void onEnable() {\n        this.init();\n\n        final Set<Listener> listeners = this.injector().getInstance(Key.get(new TypeLiteral<Set<Listener>>() {}));\n        for (final Listener listener : listeners) {\n            this.plugin.getServer().getPluginManager().registerEvents(\n                listener,\n                this.plugin\n            );\n        }\n\n        this.registerPlaceholders();\n\n        final Metrics metrics = new Metrics(this.plugin, BSTATS_PLUGIN_ID);\n        metrics.addCustomChart(new SimplePie(\"user_manager_type\", () -> this.injector().getInstance(ConfigManager.class).primaryConfig().storageType().name()));\n        metrics.addCustomChart(new SimplePie(\"messaging\", () -> {\n            final MessagingSettings settings = this.injector().getInstance(ConfigManager.class).primaryConfig().messagingSettings();\n            if (!settings.enabled()) {\n                return \"disabled\";\n            }\n            return settings.brokerType().name();\n        }));\n\n        this.checkVersion();\n\n        if (Boolean.getBoolean(\"carbonchat.smokeTest\")) {\n            this.logger().info(\"Smoke test: CarbonChat successfully enabled.\");\n            try {\n                new File(\"carbonchat-smoketest\").createNewFile();\n            } catch (final IOException e) {\n                this.logger().error(\"Smoke test: Failed to create file.\", e);\n            }\n            this.plugin.getServer().getScheduler().runTaskLater(this.plugin, () -> {\n                this.logger().info(\"Smoke test: Shutting down server.\");\n                Bukkit.getServer().shutdown();\n            }, 20L);\n        }\n    }\n\n    private void registerPlaceholders() {\n        if (papiLoaded()) {\n            this.injector().getInstance(PAPIChatHook.class);\n            this.injector().getInstance(CarbonPAPIPlaceholders.class);\n        }\n    }\n\n    void onDisable() {\n        this.shutdown();\n    }\n\n    public static boolean papiLoaded() {\n        return Bukkit.getPluginManager().isPluginEnabled(\"PlaceholderAPI\");\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/CarbonChatPaperModule.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper;\n\nimport com.google.inject.Provides;\nimport com.google.inject.Singleton;\nimport com.google.inject.multibindings.Multibinder;\nimport java.nio.file.Path;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.CarbonCommonModule;\nimport net.draycia.carbon.common.CarbonPlatformModule;\nimport net.draycia.carbon.common.DataDirectory;\nimport net.draycia.carbon.common.PlatformScheduler;\nimport net.draycia.carbon.common.RawChat;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ExecutionCoordinatorHolder;\nimport net.draycia.carbon.common.integration.Integration;\nimport net.draycia.carbon.common.messages.CarbonMessageRenderer;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.users.PlatformUserManager;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.common.util.CloudUtils;\nimport net.draycia.carbon.paper.command.PaperCommander;\nimport net.draycia.carbon.paper.command.PaperPlayerCommander;\nimport net.draycia.carbon.paper.integration.alessiodp_parties.AlessiodpPartiesIntegration;\nimport net.draycia.carbon.paper.integration.dsrv.DSRVIntegration;\nimport net.draycia.carbon.paper.integration.essxd.EssXDIntegration;\nimport net.draycia.carbon.paper.integration.fuuid.FactionsIntegration;\nimport net.draycia.carbon.paper.integration.mcmmo.McmmoIntegration;\nimport net.draycia.carbon.paper.integration.plotsquared.PlotSquaredIntegration;\nimport net.draycia.carbon.paper.integration.towny.TownyIntegration;\nimport net.draycia.carbon.paper.listeners.PaperChatListener;\nimport net.draycia.carbon.paper.listeners.PaperPlayerJoinListener;\nimport net.draycia.carbon.paper.messages.PaperMessageRenderer;\nimport net.draycia.carbon.paper.users.CarbonPlayerPaper;\nimport net.draycia.carbon.paper.users.PaperProfileResolver;\nimport net.kyori.adventure.key.Key;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.bukkit.Server;\nimport org.bukkit.entity.Player;\nimport org.bukkit.event.Listener;\nimport org.bukkit.plugin.java.JavaPlugin;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.SenderMapper;\nimport org.incendo.cloud.paper.LegacyPaperCommandManager;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonChatPaperModule extends CarbonPlatformModule {\n\n    private final Logger logger = LogManager.getLogger(\"CarbonChat\");\n    private final CarbonPaperBootstrap bootstrap;\n\n    CarbonChatPaperModule(final CarbonPaperBootstrap bootstrap) {\n        this.bootstrap = bootstrap;\n    }\n\n    @Provides\n    @Singleton\n    @SuppressWarnings(\"unused\")\n    public CommandManager<Commander> commandManager(final UserManager<?> userManager, final CarbonMessages messages, final ExecutionCoordinatorHolder executionCoordinatorHolder) {\n        final LegacyPaperCommandManager<Commander> commandManager = new LegacyPaperCommandManager<>(\n            this.bootstrap,\n            executionCoordinatorHolder.executionCoordinator(),\n            SenderMapper.create(\n                commandSender -> {\n                    if (commandSender instanceof Player player) {\n                        return new PaperPlayerCommander(userManager, player);\n                    }\n                    return PaperCommander.from(commandSender);\n                },\n                commander -> ((PaperCommander) commander).commandSender()\n            )\n        );\n\n        CloudUtils.decorateCommandManager(commandManager, messages, this.logger);\n\n        commandManager.registerBrigadier();\n\n        return commandManager;\n    }\n\n    @Override\n    protected void configurePlatform() {\n        this.install(new CarbonCommonModule());\n\n        this.bind(CarbonChat.class).to(CarbonChatPaper.class);\n        this.bind(JavaPlugin.class).toInstance(this.bootstrap);\n        this.bind(Server.class).toInstance(this.bootstrap.getServer());\n        this.bind(Logger.class).toInstance(this.logger);\n        this.bind(Path.class).annotatedWith(DataDirectory.class).toInstance(this.bootstrap.getDataFolder().toPath());\n        this.bind(CarbonServer.class).to(CarbonServerPaper.class);\n        this.bind(ProfileResolver.class).to(PaperProfileResolver.class);\n        this.bind(PlatformScheduler.class).to(PaperScheduler.class);\n        this.install(PlatformUserManager.PlayerFactory.moduleFor(CarbonPlayerPaper.class));\n        this.bind(CarbonMessageRenderer.class).to(PaperMessageRenderer.class);\n        this.bind(Key.class).annotatedWith(RawChat.class).toInstance(Key.key(\"paper:raw\"));\n\n        this.configureListeners();\n    }\n\n    @Override\n    protected void configureIntegrations(final Multibinder<Integration> integrations, final Multibinder<Integration.ConfigMeta> configs) {\n        super.configureIntegrations(integrations, configs);\n\n        integrations.addBinding().to(TownyIntegration.class);\n        configs.addBinding().toInstance(TownyIntegration.configMeta());\n\n        integrations.addBinding().to(McmmoIntegration.class);\n        configs.addBinding().toInstance(McmmoIntegration.configMeta());\n\n        integrations.addBinding().to(AlessiodpPartiesIntegration.class);\n        configs.addBinding().toInstance(AlessiodpPartiesIntegration.configMeta());\n\n        integrations.addBinding().to(FactionsIntegration.class);\n        configs.addBinding().toInstance(FactionsIntegration.configMeta());\n\n        integrations.addBinding().to(EssXDIntegration.class);\n        configs.addBinding().toInstance(EssXDIntegration.configMeta());\n\n        integrations.addBinding().to(DSRVIntegration.class);\n        configs.addBinding().toInstance(DSRVIntegration.configMeta());\n\n        integrations.addBinding().to(PlotSquaredIntegration.class);\n        configs.addBinding().toInstance(PlotSquaredIntegration.configMeta());\n    }\n\n    private void configureListeners() {\n        final Multibinder<Listener> listeners = Multibinder.newSetBinder(this.binder(), Listener.class);\n        listeners.addBinding().to(PaperChatListener.class);\n        listeners.addBinding().to(PaperPlayerJoinListener.class);\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/CarbonPaperBootstrap.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper;\n\nimport com.google.inject.Guice;\nimport net.draycia.carbon.api.CarbonChatProvider;\nimport org.bukkit.plugin.java.JavaPlugin;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonPaperBootstrap extends JavaPlugin {\n\n    private @MonotonicNonNull CarbonChatPaper carbonChat;\n\n    @Override\n    public void onLoad() {\n        this.carbonChat = Guice.createInjector(new CarbonChatPaperModule(this))\n            .getInstance(CarbonChatPaper.class);\n        CarbonChatProvider.register(this.carbonChat);\n    }\n\n    @Override\n    public void onEnable() {\n        if (this.carbonChat != null) {\n            this.carbonChat.onEnable();\n        }\n    }\n\n    @Override\n    public void onDisable() {\n        if (this.carbonChat != null) {\n            this.carbonChat.onDisable();\n        }\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/CarbonPaperLoader.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper;\n\nimport io.papermc.paper.plugin.loader.PluginClasspathBuilder;\nimport io.papermc.paper.plugin.loader.PluginLoader;\nimport net.draycia.carbon.common.util.CarbonDependencies;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport xyz.jpenilla.gremlin.runtime.platformsupport.PaperClasspathAppender;\n\n@DefaultQualifier(NonNull.class)\npublic class CarbonPaperLoader implements PluginLoader {\n\n    @Override\n    public void classloader(final PluginClasspathBuilder classpathBuilder) {\n        new PaperClasspathAppender(classpathBuilder).append(\n            CarbonDependencies.resolve(classpathBuilder.getContext().getDataDirectory().resolve(\"libraries\"))\n        );\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/CarbonServerPaper.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.util.List;\nimport java.util.Objects;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport org.bukkit.Server;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic final class CarbonServerPaper implements CarbonServer, ForwardingAudience.Single {\n\n    private final Server server;\n    private final UserManager<?> userManager;\n\n    @Inject\n    private CarbonServerPaper(final Server server, final UserManager<?> userManager) {\n        this.server = server;\n        this.userManager = userManager;\n    }\n\n    @Override\n    public Audience audience() {\n        return this.server;\n    }\n\n    @Override\n    public Audience console() {\n        return new ConsoleCarbonPlayer(this.server.getConsoleSender());\n    }\n\n    @Override\n    public List<? extends CarbonPlayer> players() {\n        return this.server.getOnlinePlayers().stream()\n            .map(bukkit -> this.userManager.user(bukkit.getUniqueId()).getNow(null))\n            .filter(Objects::nonNull)\n            .toList();\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/PaperScheduler.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.PlatformScheduler;\nimport org.bukkit.Server;\nimport org.bukkit.entity.Player;\nimport org.bukkit.plugin.java.JavaPlugin;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic final class PaperScheduler implements PlatformScheduler {\n\n    private static final boolean FOLIA;\n\n    static {\n        boolean folia;\n        try {\n            Class.forName(\"io.papermc.paper.threadedregions.RegionizedServer\");\n            folia = true;\n        } catch (final ClassNotFoundException exception) {\n            folia = false;\n        }\n        FOLIA = folia;\n    }\n\n    private final JavaPlugin plugin;\n    private final Server server;\n    private final @Nullable Folia folia;\n\n    @Inject\n    private PaperScheduler(final JavaPlugin plugin, final Server server) {\n        this.plugin = plugin;\n        this.server = server;\n        this.folia = FOLIA ? new Folia() : null;\n    }\n\n    public void scheduleForPlayer(final CarbonPlayer carbonPlayer, final Runnable runnable) {\n        if (this.folia != null) {\n            this.folia.scheduleForPlayer(carbonPlayer, runnable);\n            return;\n        }\n\n        if (this.server.isPrimaryThread()) {\n            runnable.run();\n        } else {\n            this.server.getScheduler().runTask(this.plugin, runnable);\n        }\n    }\n\n    // inner class to avoid Guice trying to load ScheduledTask when scanning for methods to inject,\n    // and finding the synthetic method generated for our ScheduledTask consumer lambda\n    private final class Folia implements PlatformScheduler {\n\n        @Override\n        public void scheduleForPlayer(final CarbonPlayer carbonPlayer, final Runnable runnable) {\n            final @Nullable Player player = PaperScheduler.this.server.getPlayer(carbonPlayer.uuid());\n\n            if (player == null) {\n                runnable.run();\n                return;\n            }\n\n            if (PaperScheduler.this.server.isOwnedByCurrentRegion(player)) {\n                runnable.run();\n            } else {\n                player.getScheduler().run(PaperScheduler.this.plugin, $ -> runnable.run(), null);\n            }\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/command/PaperCommander.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.command;\n\nimport net.draycia.carbon.common.command.Commander;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport org.bukkit.command.CommandSender;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic interface PaperCommander extends Commander, ForwardingAudience.Single {\n\n    static PaperCommander from(final CommandSender sender) {\n        return new PaperCommanderImpl(sender);\n    }\n\n    CommandSender commandSender();\n\n    record PaperCommanderImpl(CommandSender commandSender) implements PaperCommander {\n\n        @Override\n        public Audience audience() {\n            return this.commandSender;\n        }\n\n        @Override\n        public boolean hasPermission(final String permission) {\n            return this.commandSender.hasPermission(permission);\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/command/PaperPlayerCommander.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.command;\n\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.kyori.adventure.audience.Audience;\nimport org.bukkit.command.CommandSender;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static java.util.Objects.requireNonNull;\n\n@DefaultQualifier(NonNull.class)\npublic record PaperPlayerCommander(\n    UserManager<?> userManager,\n    Player player\n) implements PlayerCommander, PaperCommander {\n\n    @Override\n    public CommandSender commandSender() {\n        return this.player;\n    }\n\n    @Override\n    public Audience audience() {\n        return this.player;\n    }\n\n    @Override\n    public CarbonPlayer carbonPlayer() {\n        return requireNonNull(this.userManager.user(this.player.getUniqueId()).join(), \"No CarbonPlayer for logged in Player!\");\n    }\n\n    @Override\n    public boolean hasPermission(final String permission) {\n        return this.player.hasPermission(permission);\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/hooks/CarbonPAPIPlaceholders.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.hooks;\n\nimport com.google.inject.Inject;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.Function;\nimport me.clip.placeholderapi.expansion.PlaceholderExpansion;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.Party;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nimport net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;\nimport net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;\nimport org.bukkit.OfflinePlayer;\nimport org.bukkit.plugin.java.JavaPlugin;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class CarbonPAPIPlaceholders extends PlaceholderExpansion {\n\n    private final UserManager<?> userManager;\n    private final ChannelRegistry channels;\n    private final JavaPlugin plugin;\n    private final Map<String, Function<OfflinePlayer, Component>> componentResolvers;\n    private final Map<String, Function<OfflinePlayer, String>> stringResolvers;\n\n    @Inject\n    public CarbonPAPIPlaceholders(\n        final UserManager<?> userManager,\n        final ChannelRegistry channels,\n        final JavaPlugin plugin\n    ) {\n        this.userManager = userManager;\n        this.channels = channels;\n        this.plugin = plugin;\n        this.componentResolvers = Map.of(\n            \"party\", this::partyName,\n            \"nickname\", this::nickname,\n            \"displayname\", this::displayName\n        );\n        this.stringResolvers = Map.of(\n            \"channel_key\", this::selectedChannelKey\n        );\n        this.register();\n    }\n\n    @Override\n    public String getIdentifier() {\n        return this.plugin.getName().toLowerCase(Locale.ROOT);\n    }\n\n    @Override\n    public String getAuthor() {\n        return \"[\" + String.join(\", \", this.plugin.getPluginMeta().getAuthors()) + \"]\";\n    }\n\n    @Override\n    public String getVersion() {\n        return this.plugin.getPluginMeta().getVersion();\n    }\n\n    @Override\n    public boolean persist() {\n        return true;\n    }\n\n    @Override\n    public @Nullable String onRequest(final OfflinePlayer player, final String params) {\n        for (final Map.Entry<String, Function<OfflinePlayer, Component>> entry : this.componentResolvers.entrySet()) {\n            if (params.endsWith(entry.getKey())) {\n                return mm(entry.getValue().apply(player));\n            } else if (params.endsWith(entry.getKey() + \"_l\")) {\n                return legacy(entry.getValue().apply(player));\n            } else if (params.endsWith(entry.getKey() + \"_p\")) {\n                return plain(entry.getValue().apply(player));\n            }\n        }\n\n        for (final Map.Entry<String, Function<OfflinePlayer, String>> entry : this.stringResolvers.entrySet()) {\n            if (params.endsWith(entry.getKey())) {\n                return entry.getValue().apply(player);\n            }\n        }\n\n        return null;\n    }\n\n    private static String mm(final Component in) {\n        return MiniMessage.miniMessage().serialize(in);\n    }\n\n    private static String legacy(final Component in) {\n        return LegacyComponentSerializer.legacySection().serialize(in);\n    }\n\n    private static String plain(final Component in) {\n        return PlainTextComponentSerializer.plainText().serialize(in);\n    }\n\n    private Component partyName(final OfflinePlayer player) {\n        final @Nullable Party party = this.userManager.user(player.getUniqueId()).thenCompose(CarbonPlayer::party).join();\n        return party == null ? Component.empty() : party.name();\n    }\n\n    private Component displayName(final OfflinePlayer player) {\n        final CarbonPlayer carbonPlayer = this.userManager.user(player.getUniqueId()).join();\n        return carbonPlayer.displayName();\n    }\n\n    private Component nickname(final OfflinePlayer player) {\n        final CarbonPlayer carbonPlayer = this.userManager.user(player.getUniqueId()).join();\n        final @Nullable Component nickname = carbonPlayer.nickname();\n        return nickname == null ? Component.text(carbonPlayer.username()) : nickname;\n    }\n\n    private String selectedChannelKey(final OfflinePlayer player) {\n        final CarbonPlayer carbonPlayer = this.userManager.user(player.getUniqueId()).join();\n        final @Nullable ChatChannel selected = carbonPlayer.selectedChannel();\n        if (selected != null) {\n            return selected.key().asString();\n        }\n        return this.channels.defaultKey().asString();\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/hooks/PAPIChatHook.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.hooks;\n\nimport com.google.inject.Inject;\nimport me.clip.placeholderapi.PlaceholderAPI;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.common.event.events.CarbonEarlyChatEvent;\nimport net.draycia.carbon.common.listeners.Listener;\nimport net.draycia.carbon.common.util.ColorUtils;\nimport net.draycia.carbon.paper.CarbonChatPaper;\nimport net.draycia.carbon.paper.users.CarbonPlayerPaper;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class PAPIChatHook implements Listener {\n\n    @Inject\n    public PAPIChatHook(final CarbonEventHandler events) {\n        events.subscribe(CarbonEarlyChatEvent.class, 0, false, event -> {\n            if (!CarbonChatPaper.papiLoaded()) {\n                return;\n            }\n\n            if (!event.sender().hasPermission(\"carbon.chatplaceholders\")) {\n                return;\n            }\n\n            if (!(event.sender() instanceof CarbonPlayerPaper playerPaper)) {\n                return;\n            }\n\n            final String papiParsed = PlaceholderAPI.setPlaceholders(playerPaper.bukkitPlayer(), event.message());\n\n            event.message(ColorUtils.legacyToMiniMessage(papiParsed));\n        });\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/alessiodp_parties/AlessiodpPartiesIntegration.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.alessiodp_parties;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.Integration;\nimport org.apache.logging.log4j.Logger;\nimport org.bukkit.Bukkit;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\n\n@DefaultQualifier(NonNull.class)\npublic class AlessiodpPartiesIntegration implements Integration {\n\n    private final CarbonChannelRegistry channelRegistry;\n    private final ConfigManager configManager;\n    private final Logger logger;\n    private final AlessiodpPartiesIntegration.Config config;\n\n    @Inject\n    public AlessiodpPartiesIntegration(\n        final CarbonChannelRegistry channelRegistry,\n        final ConfigManager configManager,\n        final Logger logger\n    ) {\n        this.channelRegistry = channelRegistry;\n        this.configManager = configManager;\n        this.logger = logger;\n        this.config = this.config(configManager, configMeta());\n    }\n\n    @Override\n    public boolean eligible() {\n        try {\n            Class.forName(\"com.alessiodp.parties.api.Parties\");\n            return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled(\"Parties\");\n        } catch (final ClassNotFoundException ignored) {\n            return false;\n        }\n    }\n\n    @Override\n    public void register() {\n        if (this.config.partyChannel) {\n            if (this.configManager.primaryConfig().partyChat().enabled) {\n                this.logger.warn(\"Both CarbonChat parties and the Parties party chat channel are enabled!\");\n                this.logger.warn(\"Usually, you want one or the other enabled. Additionally, their default channel configs will conflict.\");\n            }\n            this.channelRegistry.registerSpecialConfigChannel(AlessiodpPartiesPartyChannel.FILE_NAME, AlessiodpPartiesPartyChannel.class);\n        }\n    }\n\n    public static ConfigMeta configMeta() {\n        return Integration.configMeta(\"alessiodp-parties\", AlessiodpPartiesIntegration.Config.class);\n    }\n\n    @ConfigSerializable\n    public static final class Config {\n\n        boolean enabled = true;\n\n        @Comment(\"You will likely want to disable Carbon's built-in party system above when using Parties party chat.\")\n        boolean partyChannel = true;\n\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/alessiodp_parties/AlessiodpPartiesPartyChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.alessiodp_parties;\n\nimport com.alessiodp.parties.api.Parties;\nimport com.alessiodp.parties.api.interfaces.Party;\nimport com.alessiodp.parties.api.interfaces.PartyPlayer;\nimport com.google.inject.Inject;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.channels.ConfigChatChannel;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\nimport static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult;\n\n@DefaultQualifier(NonNull.class)\n@ConfigSerializable\npublic class AlessiodpPartiesPartyChannel extends ConfigChatChannel {\n\n    public static final String FILE_NAME = \"alessiodp-parties-party.conf\";\n\n    private transient @MonotonicNonNull\n    @Inject CarbonMessages messages;\n    private transient @MonotonicNonNull @Inject UserManager<?> users;\n\n    public AlessiodpPartiesPartyChannel() {\n        this.key = Key.key(\"carbon\", \"partychat\");\n        this.commandAliases = List.of(\"pc\");\n\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n            \"default_format\", \"(party: %party%) <display_name>: <message>\",\n            \"console\", \"[party: %party%] <username>: <message>\"\n        );\n    }\n\n    @Override\n    public ChannelPermissions permissions() {\n        return ChannelPermissions.uniformDynamic(player -> channelPermissionResult(\n            this.party(player) != null,\n            () -> this.messages.cannotUseADPPartiesPartyChannel(player)\n        ));\n    }\n\n    @Override\n    public List<Audience> recipients(final CarbonPlayer sender) {\n        final @Nullable Party party = this.party(sender);\n\n        if (party == null) {\n            if (sender.online()) {\n                sender.sendMessage(this.messages.cannotUseADPPartiesPartyChannel(sender));\n            }\n\n            return Collections.emptyList();\n        }\n\n        final List<Audience> recipients = new ArrayList<>();\n        for (final PartyPlayer player : party.getOnlineMembers()) {\n            final @Nullable CarbonPlayer carbon = this.users.user(player.getPlayerUUID()).getNow(null);\n            if (carbon != null) {\n                recipients.add(carbon);\n            }\n        }\n\n        recipients.add(this.server.console());\n\n        return recipients;\n    }\n\n    private @Nullable Party party(final CarbonPlayer player) {\n        return Parties.getApi().getPartyOfPlayer(player.uuid());\n    }\n\n    @Override\n    public boolean shouldCrossServer() {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/dsrv/DSRVIntegration.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.dsrv;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.Integration;\nimport org.bukkit.Bukkit;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\npublic final class DSRVIntegration implements Integration {\n\n    private final Injector injector;\n    private final DSRVIntegration.Config config;\n\n    @Inject\n    private DSRVIntegration(\n        final Injector injector,\n        final ConfigManager configManager\n    ) {\n        this.injector = injector;\n        this.config = this.config(configManager, configMeta());\n    }\n\n    @Override\n    public boolean eligible() {\n        return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled(\"DiscordSRV\");\n    }\n\n    @Override\n    public void register() {\n        this.injector.getInstance(DSRVListener.class).register();\n    }\n\n    public static ConfigMeta configMeta() {\n        return Integration.configMeta(\"discordsrv\", DSRVIntegration.Config.class);\n    }\n\n    @ConfigSerializable\n    public static final class Config {\n\n        boolean enabled = true;\n\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/dsrv/DSRVListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.dsrv;\n\nimport com.github.benmanes.caffeine.cache.Cache;\nimport com.github.benmanes.caffeine.cache.Caffeine;\nimport com.google.inject.Inject;\nimport github.scarsz.discordsrv.Debug;\nimport github.scarsz.discordsrv.DiscordSRV;\nimport github.scarsz.discordsrv.api.Subscribe;\nimport github.scarsz.discordsrv.api.events.GameChatMessagePreProcessEvent;\nimport github.scarsz.discordsrv.hooks.chat.ChatHook;\nimport java.time.Duration;\nimport net.draycia.carbon.api.channels.ChatChannel;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.api.event.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.messages.TagPermissions;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.draycia.carbon.common.users.WrappedCarbonPlayer;\nimport net.draycia.carbon.common.util.ChannelUtils;\nimport net.draycia.carbon.paper.users.CarbonPlayerPaper;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;\nimport net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;\nimport org.apache.commons.lang3.tuple.ImmutablePair;\nimport org.bukkit.entity.Player;\nimport org.bukkit.plugin.Plugin;\nimport org.bukkit.plugin.java.JavaPlugin;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\npublic final class DSRVListener implements ChatHook {\n\n    private final CarbonChannelRegistry channelRegistry;\n    private final JavaPlugin plugin;\n    private final CarbonEventHandler eventHandler;\n\n    @Inject\n    private DSRVListener(\n        final CarbonEventHandler eventHandler,\n        final CarbonChannelRegistry channelRegistry,\n        final JavaPlugin plugin\n    ) {\n        this.channelRegistry = channelRegistry;\n        this.eventHandler = eventHandler;\n        this.plugin = plugin;\n    }\n\n    public void register() {\n        DiscordSRV.getPlugin().getPluginHooks().add(this);\n\n        final Cache<ImmutablePair<CarbonPlayer, ChatChannel>, Component> awaitingEvent = Caffeine.newBuilder()\n            .expireAfterWrite(Duration.ofMillis(25))\n            .build();\n\n        this.eventHandler.subscribe(CarbonChatEvent.class, 100, false, event -> {\n            final ChatChannel chatChannel = event.chatChannel();\n            final CarbonPlayer carbonPlayer = event.sender();\n\n            if (carbonPlayer instanceof ConsoleCarbonPlayer) {\n                return;\n            }\n\n            if (carbonPlayer.muted()) {\n                return;\n            }\n\n            final ImmutablePair<CarbonPlayer, ChatChannel> pair = new ImmutablePair<>(carbonPlayer, chatChannel);\n            Component messageComponent = awaitingEvent.getIfPresent(pair);\n            awaitingEvent.invalidate(pair);\n\n            if (messageComponent == null) {\n                messageComponent = event.message();\n            }\n\n            final String messageContents = PlainTextComponentSerializer.plainText().serialize(messageComponent);\n            final Component eventMessage;\n\n            if (carbonPlayer instanceof WrappedCarbonPlayer wrapped) {\n                eventMessage = wrapped.parseMessageTags(messageContents);\n            } else {\n                eventMessage = TagPermissions.parseTags(carbonPlayer, TagPermissions.MESSAGE, messageContents, carbonPlayer::hasPermission);\n            }\n\n            DiscordSRV.debug(Debug.MINECRAFT_TO_DISCORD, \"Received a CarbonChatEvent (player: \" + carbonPlayer.username() + \")\");\n\n            final @Nullable Player player = ((CarbonPlayerPaper) carbonPlayer).bukkitPlayer();\n\n            if (player != null) {\n                DiscordSRV.getPlugin().processChatMessage(player, this.toDsrv(eventMessage), chatChannel.commandName(), event.cancelled(), null);\n            }\n        });\n\n        DiscordSRV.api.subscribe(new Object() {\n            @Subscribe\n            public void handle(final GameChatMessagePreProcessEvent event) {\n                if (event.getTriggeringBukkitEvent() == null) {\n                    return;\n                }\n\n                event.setCancelled(true);\n            }\n        });\n    }\n\n    @Override\n    public void broadcastMessageToChannel(final String channel, final github.scarsz.discordsrv.dependencies.kyori.adventure.text.Component message) {\n        final @Nullable ChatChannel chatChannel = this.channelRegistry.channelByValue(channel);\n\n        if (chatChannel == null) {\n            this.plugin.getLogger().warning(\"Error sending message from Discord to Minecraft, no matching channel found for [\" + channel + \"]\");\n        } else {\n            ChannelUtils.broadcastMessageToChannel(this.fromDsrv(message), chatChannel);\n        }\n    }\n\n    private github.scarsz.discordsrv.dependencies.kyori.adventure.text.Component toDsrv(final Component component) {\n        return github.scarsz.discordsrv.dependencies.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().deserialize(\n            GsonComponentSerializer.gson().serialize(component)\n        );\n    }\n\n    private Component fromDsrv(final github.scarsz.discordsrv.dependencies.kyori.adventure.text.Component component) {\n        return GsonComponentSerializer.gson().deserialize(\n            github.scarsz.discordsrv.dependencies.kyori.adventure.text.serializer.gson.GsonComponentSerializer.gson().serialize(component)\n        );\n    }\n\n    @Override\n    public Plugin getPlugin() {\n        return this.plugin;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/essxd/EssXDIntegration.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.essxd;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.Integration;\nimport org.bukkit.Bukkit;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\npublic final class EssXDIntegration implements Integration {\n\n    private final Injector injector;\n    private final EssXDIntegration.Config config;\n\n    @Inject\n    private EssXDIntegration(\n        final Injector injector,\n        final ConfigManager configManager\n    ) {\n        this.injector = injector;\n        this.config = this.config(configManager, configMeta());\n    }\n\n    @Override\n    public boolean eligible() {\n        return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled(\"EssentialsDiscord\");\n    }\n\n    @Override\n    public void register() {\n        this.injector.getInstance(EssXDListener.class).register();\n    }\n\n    public static ConfigMeta configMeta() {\n        return Integration.configMeta(\"essentialsx_discord\", EssXDIntegration.Config.class);\n    }\n\n    @ConfigSerializable\n    public static final class Config {\n\n        boolean enabled = true;\n\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/essxd/EssXDListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.essxd;\n\nimport com.google.inject.Inject;\nimport java.util.HashMap;\nimport java.util.Map;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.essentialsx.api.v2.events.discord.DiscordMessageEvent;\nimport net.essentialsx.api.v2.services.discord.DiscordService;\nimport net.essentialsx.api.v2.services.discord.MessageType;\nimport net.kyori.adventure.key.Key;\nimport org.apache.logging.log4j.Logger;\nimport org.bukkit.Bukkit;\nimport org.bukkit.event.EventHandler;\nimport org.bukkit.event.Listener;\nimport org.bukkit.plugin.java.JavaPlugin;\nimport org.checkerframework.checker.nullness.qual.Nullable;\n\npublic final class EssXDListener implements Listener {\n\n    private final CarbonChat carbonChat;\n    private final JavaPlugin plugin;\n    private final Map<Key, MessageType> channelMessageTypes = new HashMap<>();\n    private final Logger logger;\n\n    @Inject\n    private EssXDListener(\n        final JavaPlugin plugin,\n        final CarbonChat carbonChat,\n        final Logger logger\n    ) {\n        this.plugin = plugin;\n        this.carbonChat = carbonChat;\n        this.logger = logger;\n    }\n\n    // Minecraft -> Discord\n    @EventHandler\n    public void onDiscordMessage(final DiscordMessageEvent event) {\n        if (!event.getType().equals(MessageType.DefaultTypes.CHAT)) {\n            return;\n        }\n\n        final var result = this.carbonChat.userManager().user(event.getUUID()).join();\n\n        var channel = result.selectedChannel();\n\n        if (channel == null) {\n            channel = this.carbonChat.channelRegistry().defaultChannel();\n        }\n\n        final var messageType = this.channelMessageTypes.get(channel.key());\n\n        event.setType(messageType);\n    }\n\n    public void register() {\n        Bukkit.getPluginManager().registerEvents(this, this.plugin);\n\n        final @Nullable DiscordService discord = Bukkit.getServicesManager().load(DiscordService.class);\n\n        if (discord != null) {\n            this.carbonChat.channelRegistry().allKeys(key -> {\n                final MessageType channelMessageType = new MessageType(key.value());\n                try {\n                    discord.registerMessageType(this.plugin, channelMessageType);\n                } catch (final IllegalArgumentException exception) {\n                    this.logger.info(\"Skipping registration of message type [{}]\", channelMessageType);\n                }\n                this.channelMessageTypes.put(key, channelMessageType);\n            });\n        }\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/AbstractFactionsChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.fuuid;\n\nimport com.massivecraft.factions.FPlayer;\nimport com.massivecraft.factions.FPlayers;\nimport com.massivecraft.factions.Faction;\nimport com.massivecraft.factions.perms.Relation;\nimport com.massivecraft.factions.perms.Role;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.channels.ConfigChatChannel;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jspecify.annotations.NonNull;\n\n@DefaultQualifier(NonNull.class)\nabstract class AbstractFactionsChannel extends ConfigChatChannel {\n\n    protected final @Nullable Faction faction(final CarbonPlayer player) {\n        final @Nullable FPlayer fPlayer = this.factionPlayer(player);\n\n        if (fPlayer == null || !fPlayer.hasFaction()) {\n            return null;\n        }\n\n        return fPlayer.getFaction();\n    }\n\n    protected final @Nullable FPlayer factionPlayer(final CarbonPlayer player) {\n        return FPlayers.getInstance().getById(player.uuid().toString());\n    }\n\n    protected final @Nullable Role factionRole(final CarbonPlayer player) {\n        final @Nullable FPlayer fPlayer = this.factionPlayer(player);\n\n        if (fPlayer == null || !fPlayer.hasFaction()) {\n            return null;\n        }\n\n        return fPlayer.getRole();\n    }\n\n    protected final boolean hasRelations(final CarbonPlayer player, final Relation relation) {\n        final @Nullable Faction faction = this.faction(player);\n\n        return faction != null && faction.getRelationCount(relation) > 0;\n    }\n\n    @Override\n    public boolean shouldCrossServer() {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/AllianceChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.fuuid;\n\nimport com.google.inject.Inject;\nimport com.massivecraft.factions.FPlayer;\nimport com.massivecraft.factions.FPlayers;\nimport com.massivecraft.factions.Faction;\nimport com.massivecraft.factions.perms.Relation;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\nimport static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult;\n\n@DefaultQualifier(NonNull.class)\n@ConfigSerializable\npublic class AllianceChannel extends AbstractFactionsChannel {\n\n    public static final String FILE_NAME = \"factionsuuid-alliancechat.conf\";\n\n    private transient @MonotonicNonNull @Inject UserManager<?> users;\n\n    public AllianceChannel() {\n        this.key = Key.key(\"carbon\", \"alliancechat\");\n        this.commandAliases = List.of(\"ac\");\n\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n            \"default_format\", \"(alliance: %factionsuuid_faction_name%) <display_name>: <message>\",\n            \"console\", \"[alliance: %factionsuuid_faction_name%] <username>: <message>\"\n        );\n    }\n\n    @Override\n    public ChannelPermissions permissions() {\n        return ChannelPermissions.uniformDynamic(player -> channelPermissionResult(\n            this.hasRelations(player, Relation.ALLY),\n            () -> this.messages.cannotUseFactionAllianceChannel(player)\n        ));\n    }\n\n    @Override\n    public List<Audience> recipients(final CarbonPlayer sender) {\n        if (!this.hasRelations(sender, Relation.ALLY)) {\n            if (sender.online()) {\n                sender.sendMessage(this.messages.cannotUseFactionAllianceChannel(sender));\n            }\n\n            return Collections.emptyList();\n        }\n\n        final List<Audience> recipients = new ArrayList<>();\n        for (final Player player : this.alliedPlayersTo(sender)) {\n            final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null);\n            if (carbon != null) {\n                recipients.add(carbon);\n            }\n        }\n\n        recipients.add(this.server.console());\n\n        return recipients;\n    }\n\n    private List<Player> alliedPlayersTo(final CarbonPlayer player) {\n        final @Nullable Faction faction = this.faction(player);\n\n        if (faction == null) {\n            return List.of();\n        }\n\n        final List<Player> alliedPlayers = new ArrayList<>();\n\n        for (final FPlayer onlinePlayer : FPlayers.getInstance().getOnlinePlayers()) {\n            final Relation relation = faction.getRelationTo(onlinePlayer);\n\n            if (relation.isAtLeast(Relation.ALLY)) {\n                alliedPlayers.add(onlinePlayer.getPlayer());\n            }\n        }\n\n        return alliedPlayers;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/FactionChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.fuuid;\n\nimport com.google.inject.Inject;\nimport com.massivecraft.factions.Faction;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\nimport static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult;\n\n@DefaultQualifier(NonNull.class)\n@ConfigSerializable\npublic class FactionChannel extends AbstractFactionsChannel {\n\n    public static final String FILE_NAME = \"factionsuuid-factionchat.conf\";\n\n    private transient @MonotonicNonNull @Inject UserManager<?> users;\n\n    public FactionChannel() {\n        this.key = Key.key(\"carbon\", \"factionchat\");\n        this.commandAliases = List.of(\"fc\");\n\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n            \"default_format\", \"(faction: %factionsuuid_faction_name%) <display_name>: <message>\",\n            \"console\", \"[faction: %factionsuuid_faction_name%] <username>: <message>\"\n        );\n    }\n\n    @Override\n    public ChannelPermissions permissions() {\n        return ChannelPermissions.uniformDynamic(player -> channelPermissionResult(\n            this.faction(player) != null,\n            () -> this.messages.cannotUseFactionChannel(player)\n        ));\n    }\n\n    @Override\n    public List<Audience> recipients(final CarbonPlayer sender) {\n        final @Nullable Faction faction = this.faction(sender);\n\n        if (faction == null) {\n            if (sender.online()) {\n                sender.sendMessage(this.messages.cannotUseFactionChannel(sender));\n            }\n\n            return Collections.emptyList();\n        }\n\n        final List<Audience> recipients = new ArrayList<>();\n        for (final Player player : faction.getOnlinePlayers()) {\n            final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null);\n            if (carbon != null) {\n                recipients.add(carbon);\n            }\n        }\n\n        recipients.add(this.server.console());\n\n        return recipients;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/FactionModChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.fuuid;\n\nimport com.google.inject.Inject;\nimport com.massivecraft.factions.FPlayer;\nimport com.massivecraft.factions.Faction;\nimport com.massivecraft.factions.perms.Role;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\nimport static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult;\n\n@DefaultQualifier(NonNull.class)\n@ConfigSerializable\npublic class FactionModChannel extends AbstractFactionsChannel {\n\n    public static final String FILE_NAME = \"factionsuuid-factionmodchat.conf\";\n\n    private transient @MonotonicNonNull @Inject UserManager<?> users;\n\n    // We could check if the player doesn't have the normal role, but this list may be configurable in the future?\n    private transient final List<Role> validRoles = List.of(Role.ADMIN, Role.MODERATOR, Role.COLEADER);\n\n    public FactionModChannel() {\n        this.key = Key.key(\"carbon\", \"factionmodchat\");\n        this.commandAliases = List.of(\"mc\");\n\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n            \"default_format\", \"(fmod: %factionsuuid_faction_name%) <display_name>: <message>\",\n            \"console\", \"[fmod: %factionsuuid_faction_name%] <username>: <message>\"\n        );\n    }\n\n    @Override\n    public ChannelPermissions permissions() {\n        return ChannelPermissions.uniformDynamic(player -> channelPermissionResult(\n            this.validRoles.contains(this.factionRole(player)),\n            () -> this.messages.cannotUseFactionModChannel(player)\n        ));\n    }\n\n    @Override\n    public List<Audience> recipients(final CarbonPlayer sender) {\n        if (!this.validRoles.contains(this.factionRole(sender))) {\n            if (sender.online()) {\n                sender.sendMessage(this.messages.cannotUseFactionModChannel(sender));\n            }\n\n            return Collections.emptyList();\n        }\n\n        final List<Audience> recipients = new ArrayList<>();\n        for (final Player player : this.factionMods(sender)) {\n            final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null);\n            if (carbon != null) {\n                recipients.add(carbon);\n            }\n        }\n\n        recipients.add(this.server.console());\n\n        return recipients;\n    }\n\n    private List<Player> factionMods(final CarbonPlayer player) {\n        final @Nullable Faction faction = this.faction(player);\n\n        if (faction == null) {\n            return List.of();\n        }\n\n        final List<Player> factionMods = new ArrayList<>();\n\n        for (final FPlayer onlinePlayer : faction.getFPlayersWhereOnline(true)) {\n            if (this.validRoles.contains(onlinePlayer.getRole())) {\n                factionMods.add(onlinePlayer.getPlayer());\n            }\n        }\n\n        return factionMods;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/FactionsIntegration.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.fuuid;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.Integration;\nimport org.bukkit.Bukkit;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\n@DefaultQualifier(NonNull.class)\npublic final class FactionsIntegration implements Integration {\n\n    private final CarbonChannelRegistry channelRegistry;\n    private final Config config;\n\n    @Inject\n    public FactionsIntegration(\n        final CarbonChannelRegistry channelRegistry,\n        final ConfigManager configManager\n    ) {\n        this.channelRegistry = channelRegistry;\n        this.config = this.config(configManager, configMeta());\n    }\n\n    @Override\n    public boolean eligible() {\n        return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled(\"Factions\");\n    }\n\n    @Override\n    public void register() {\n        if (this.config.factionChannel) {\n            this.channelRegistry.registerSpecialConfigChannel(FactionChannel.FILE_NAME, FactionChannel.class);\n        }\n        if (this.config.allianceChannel) {\n            this.channelRegistry.registerSpecialConfigChannel(AllianceChannel.FILE_NAME, AllianceChannel.class);\n        }\n        if (this.config.truceChannel) {\n            this.channelRegistry.registerSpecialConfigChannel(TruceChannel.FILE_NAME, TruceChannel.class);\n        }\n        if (this.config.factionModChannel) {\n            this.channelRegistry.registerSpecialConfigChannel(FactionModChannel.FILE_NAME, FactionModChannel.class);\n        }\n    }\n\n    public static ConfigMeta configMeta() {\n        return Integration.configMeta(\"factionsuuid\", FactionsIntegration.Config.class);\n    }\n\n    @ConfigSerializable\n    public static final class Config {\n\n        boolean enabled = true;\n\n        boolean factionChannel = true;\n        boolean allianceChannel = true;\n        boolean truceChannel = true;\n        boolean factionModChannel = false;\n\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/fuuid/TruceChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.fuuid;\n\nimport com.google.inject.Inject;\nimport com.massivecraft.factions.FPlayer;\nimport com.massivecraft.factions.FPlayers;\nimport com.massivecraft.factions.Faction;\nimport com.massivecraft.factions.perms.Relation;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\nimport static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult;\n\n@DefaultQualifier(NonNull.class)\n@ConfigSerializable\npublic class TruceChannel extends AbstractFactionsChannel {\n\n    public static final String FILE_NAME = \"factionsuuid-trucechat.conf\";\n\n    private transient @MonotonicNonNull @Inject CarbonMessages messages;\n    private transient @MonotonicNonNull @Inject UserManager<?> users;\n\n    public TruceChannel() {\n        this.key = Key.key(\"carbon\", \"trucechat\");\n        this.commandAliases = List.of(\"tc\");\n\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n            \"default_format\", \"(truce: %factionsuuid_faction_name%) <display_name>: <message>\",\n            \"console\", \"[truce: %factionsuuid_faction_name%] <username>: <message>\"\n        );\n    }\n\n    @Override\n    public ChannelPermissions permissions() {\n        return ChannelPermissions.uniformDynamic(player -> channelPermissionResult(\n            this.hasRelations(player, Relation.TRUCE),\n            () -> this.messages.cannotUseTruceChannel(player)\n        ));\n    }\n\n    @Override\n    public List<Audience> recipients(final CarbonPlayer sender) {\n        if (!this.hasRelations(sender, Relation.TRUCE)) {\n            if (sender.online()) {\n                sender.sendMessage(this.messages.cannotUseTruceChannel(sender));\n            }\n\n            return Collections.emptyList();\n        }\n\n        final List<Audience> recipients = new ArrayList<>();\n        for (final Player player : this.hasTruceWith(sender)) {\n            final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null);\n            if (carbon != null) {\n                recipients.add(carbon);\n            }\n        }\n\n        recipients.add(this.server.console());\n\n        return recipients;\n    }\n\n    private List<Player> hasTruceWith(final CarbonPlayer player) {\n        final @Nullable Faction faction = this.faction(player);\n\n        if (faction == null) {\n            return List.of();\n        }\n\n        final List<Player> alliedPlayers = new ArrayList<>();\n\n        for (final FPlayer onlinePlayer : FPlayers.getInstance().getOnlinePlayers()) {\n            final Relation relation = faction.getRelationTo(onlinePlayer);\n\n            if (relation.isAtLeast(Relation.TRUCE)) {\n                alliedPlayers.add(onlinePlayer.getPlayer());\n            }\n        }\n\n        return alliedPlayers;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/mcmmo/McmmoIntegration.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.mcmmo;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.Integration;\nimport org.apache.logging.log4j.Logger;\nimport org.bukkit.Bukkit;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\nimport org.spongepowered.configurate.objectmapping.meta.Comment;\n\n@DefaultQualifier(NonNull.class)\npublic final class McmmoIntegration implements Integration {\n\n    private final CarbonChannelRegistry channelRegistry;\n    private final ConfigManager configManager;\n    private final Logger logger;\n    private final Config config;\n\n    @Inject\n    public McmmoIntegration(\n        final CarbonChannelRegistry channelRegistry,\n        final ConfigManager configManager,\n        final Logger logger\n    ) {\n        this.channelRegistry = channelRegistry;\n        this.configManager = configManager;\n        this.logger = logger;\n        this.config = this.config(configManager, configMeta());\n    }\n\n    @Override\n    public boolean eligible() {\n        return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled(\"mcMMO\");\n    }\n\n    @Override\n    public void register() {\n        if (this.config.partyChannel) {\n            if (this.configManager.primaryConfig().partyChat().enabled) {\n                this.logger.warn(\"Both CarbonChat parties and the mcMMO party chat channel are enabled!\");\n                this.logger.warn(\"Usually, you want one or the other enabled. Additionally, their default channel configs will conflict.\");\n            }\n            this.channelRegistry.registerSpecialConfigChannel(McmmoPartyChannel.FILE_NAME, McmmoPartyChannel.class);\n        }\n    }\n\n    public static ConfigMeta configMeta() {\n        return Integration.configMeta(\"mcmmo\", McmmoIntegration.Config.class);\n    }\n\n    @ConfigSerializable\n    public static final class Config {\n\n        boolean enabled = true;\n\n        @Comment(\"You will likely want to disable Carbon's built-in party system above when using mcMMO party chat.\")\n        boolean partyChannel = true;\n\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/mcmmo/McmmoPartyChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.mcmmo;\n\nimport com.gmail.nossr50.datatypes.party.Party;\nimport com.google.inject.Inject;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.channels.ConfigChatChannel;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport org.bukkit.Bukkit;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\nimport static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult;\n\n@DefaultQualifier(NonNull.class)\n@ConfigSerializable\npublic class McmmoPartyChannel extends ConfigChatChannel {\n\n    public static final String FILE_NAME = \"mcmmo-party.conf\";\n\n    private transient @MonotonicNonNull @Inject CarbonMessages messages;\n    private transient @MonotonicNonNull @Inject UserManager<?> users;\n\n    public McmmoPartyChannel() {\n        this.key = Key.key(\"carbon\", \"partychat\");\n        this.commandAliases = List.of(\"pc\");\n\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n            \"default_format\", \"(party: %mcmmo_party_name%) <display_name>: <message>\",\n            \"console\", \"[party: %mcmmo_party_name%] <username>: <message>\"\n        );\n    }\n\n    @Override\n    public ChannelPermissions permissions() {\n        return ChannelPermissions.uniformDynamic(player -> channelPermissionResult(\n            this.party(player) != null,\n            () -> this.messages.cannotUseMcmmoPartyChannel(player)\n        ));\n    }\n\n    @Override\n    public List<Audience> recipients(final CarbonPlayer sender) {\n        final @Nullable Party party = this.party(sender);\n\n        if (party == null) {\n            if (sender.online()) {\n                sender.sendMessage(this.messages.cannotUseMcmmoPartyChannel(sender));\n            }\n\n            return Collections.emptyList();\n        }\n\n        final List<Audience> recipients = new ArrayList<>();\n        for (final Player player : party.getOnlineMembers()) {\n            final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null);\n            if (carbon != null) {\n                recipients.add(carbon);\n            }\n        }\n\n        recipients.add(this.server.console());\n\n        return recipients;\n    }\n\n    private @Nullable Party party(final CarbonPlayer player) {\n        return com.gmail.nossr50.util.player.UserManager.getPlayer(Bukkit.getPlayer(player.uuid())).getParty();\n    }\n\n    @Override\n    public boolean shouldCrossServer() {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/plotsquared/PlotChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.plotsquared;\n\nimport com.google.inject.Inject;\nimport com.plotsquared.core.PlotAPI;\nimport com.plotsquared.core.player.PlotPlayer;\nimport com.plotsquared.core.plot.Plot;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.channels.ConfigChatChannel;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.draycia.carbon.common.config.ConfigHeader;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\nimport static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult;\n\n@DefaultQualifier(NonNull.class)\n@ConfigSerializable\n@ConfigHeader(PlotChannel.PLOT_CHANNEL_HEADER)\npublic class PlotChannel extends ConfigChatChannel {\n\n    protected static final String PLOT_CHANNEL_HEADER = \"\"\"\n            See the PlotSquared Wiki at https://intellectualsites.gitbook.io/plotsquared/customization/placeholders\n            for placeholders PlotSquared provides to PlaceholderAPI.\n            \"\"\";\n    protected final static PlotAPI PLOT_API = new PlotAPI();\n\n    public static final String FILE_NAME = \"plotsquared-plotchat.conf\";\n\n    protected transient @MonotonicNonNull @Inject UserManager<?> users;\n\n    public PlotChannel() {\n        this.key = Key.key(\"carbon\", \"plotchat\");\n        this.commandAliases = List.of(\"local\");\n\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n                \"default_format\", \"<display_name>: <message>\",\n                \"console\", \"<username>: <message>\"\n        );\n    }\n\n    protected @Nullable Plot plot(final CarbonPlayer carbonPlayer) {\n        final @Nullable PlotPlayer<?> plotPlayer = PLOT_API.wrapPlayer(carbonPlayer.uuid());\n        if (plotPlayer != null) {\n            return plotPlayer.getCurrentPlot();\n        }\n        return null;\n    }\n\n    @Override\n    public ChannelPermissions permissions() {\n        return ChannelPermissions.uniformDynamic(player -> channelPermissionResult(\n                this.plot(player) != null,\n                () -> this.cannotUseChannel(player)\n        ));\n    }\n\n    @Override\n    public List<Audience> recipients(final CarbonPlayer sender) {\n        final @Nullable Plot plot = this.plot(sender);\n\n        if (plot == null) {\n            if (sender.online()) {\n                sender.sendMessage(this.cannotUseChannel(sender));\n            }\n\n            return Collections.emptyList();\n        }\n\n        final List<Audience> recipients = new ArrayList<>();\n        for (final PlotPlayer<?> plotPlayer : this.onlinePlayers(plot)) {\n            final @Nullable CarbonPlayer carbon = this.users.user(plotPlayer.getUUID()).getNow(null);\n            if (carbon != null) {\n                recipients.add(carbon);\n            }\n        }\n\n        recipients.add(this.server.console());\n\n        return recipients;\n    }\n\n    protected List<PlotPlayer<?>> onlinePlayers(final Plot plot) {\n        return plot.getPlayersInPlot();\n    }\n\n    protected Component cannotUseChannel(final CarbonPlayer player) {\n        return this.messages.cannotUsePlotChannel(player);\n    }\n\n    @Override\n    public boolean shouldCrossServer() {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/plotsquared/PlotSquaredIntegration.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.plotsquared;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.Integration;\nimport org.bukkit.Bukkit;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\n@DefaultQualifier(NonNull.class)\npublic final class PlotSquaredIntegration implements Integration {\n\n    private final CarbonChannelRegistry channelRegistry;\n    private final Config config;\n\n    @Inject\n    public PlotSquaredIntegration(\n        final CarbonChannelRegistry channelRegistry,\n        final ConfigManager configManager\n    ) {\n        this.channelRegistry = channelRegistry;\n        this.config = this.config(configManager, configMeta());\n    }\n\n    @Override\n    public boolean eligible() {\n        return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled(\"PlotSquared\");\n    }\n\n    @Override\n    public void register() {\n        if (this.config.plotChannel) {\n            this.channelRegistry.registerSpecialConfigChannel(PlotChannel.FILE_NAME, PlotChannel.class);\n        }\n    }\n\n    public static ConfigMeta configMeta() {\n        return Integration.configMeta(\"plotsquared\", PlotSquaredIntegration.Config.class);\n    }\n\n    @ConfigSerializable\n    public static final class Config {\n        boolean enabled = true;\n        boolean plotChannel = true;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/towny/AllianceChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.towny;\n\nimport com.palmergames.bukkit.towny.object.Nation;\nimport java.util.List;\nimport java.util.Map;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.draycia.carbon.common.config.ConfigHeader;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\n@DefaultQualifier(NonNull.class)\n@ConfigSerializable\n@ConfigHeader(ResidentListChannel.TOWNY_CHANNEL_HEADER)\npublic class AllianceChannel extends NationChannel {\n\n    public static final String FILE_NAME = \"towny-alliancechat.conf\";\n\n    public AllianceChannel() {\n        this.key = Key.key(\"carbon\", \"alliancechat\");\n        this.commandAliases = List.of();\n\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n            \"default_format\", \"(alliance) <display_name>: <message>\",\n            \"console\", \"[alliance] <username>: <message>\"\n        );\n    }\n\n    @Override\n    protected List<Player> onlinePlayers(final Nation residentList) {\n        return TOWNY_API.getOnlinePlayersAlliance(residentList);\n    }\n\n    @Override\n    protected Component cannotUseChannel(final CarbonPlayer player) {\n        return this.messages.cannotUseAllianceChannel(player);\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/towny/NationChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.towny;\n\nimport com.palmergames.bukkit.towny.object.Nation;\nimport com.palmergames.bukkit.towny.object.Resident;\nimport java.util.List;\nimport java.util.Map;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.draycia.carbon.common.config.ConfigHeader;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\n@DefaultQualifier(NonNull.class)\n@ConfigSerializable\n@ConfigHeader(ResidentListChannel.TOWNY_CHANNEL_HEADER)\npublic class NationChannel extends ResidentListChannel<Nation> {\n\n    public static final String FILE_NAME = \"towny-nationchat.conf\";\n\n    public NationChannel() {\n        this.key = Key.key(\"carbon\", \"nationchat\");\n        this.commandAliases = List.of(\"nc\");\n\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n            \"default_format\", \"(nation: %townyadvanced_nation_unformatted%) <display_name>: <message>\",\n            \"console\", \"[nation: %townyadvanced_nation_unformatted%] <username>: <message>\"\n        );\n    }\n\n    @Override\n    protected @Nullable Nation residentList(final CarbonPlayer player) {\n        final @Nullable Resident resident = TOWNY_API.getResident(player.uuid());\n        if (resident == null) {\n            return null;\n        }\n        return TOWNY_API.getResidentNationOrNull(resident);\n    }\n\n    @Override\n    protected Component cannotUseChannel(final CarbonPlayer player) {\n        return this.messages.cannotUseNationChannel(player);\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/towny/ResidentListChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.towny;\n\nimport com.google.inject.Inject;\nimport com.palmergames.bukkit.towny.TownyAPI;\nimport com.palmergames.bukkit.towny.object.ResidentList;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport net.draycia.carbon.api.channels.ChannelPermissions;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.channels.ConfigChatChannel;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static net.draycia.carbon.api.channels.ChannelPermissionResult.channelPermissionResult;\n\n@DefaultQualifier(NonNull.class)\nabstract class ResidentListChannel<T extends ResidentList> extends ConfigChatChannel {\n\n    protected static final String TOWNY_CHANNEL_HEADER = \"\"\"\n        See the Towny Wiki at https://github.com/TownyAdvanced/Towny/wiki/Placeholders\n        for placeholders Towny provides to PlaceholderAPI.\n        \"\"\";\n    protected final static TownyAPI TOWNY_API = TownyAPI.getInstance();\n\n    protected transient @MonotonicNonNull @Inject UserManager<?> users;\n\n    protected abstract @Nullable T residentList(CarbonPlayer player);\n\n    @Override\n    public ChannelPermissions permissions() {\n        return ChannelPermissions.uniformDynamic(player -> channelPermissionResult(\n            this.residentList(player) != null,\n            () -> this.cannotUseChannel(player)\n        ));\n    }\n\n    @Override\n    public List<Audience> recipients(final CarbonPlayer sender) {\n        final @Nullable T residentList = this.residentList(sender);\n\n        if (residentList == null) {\n            if (sender.online()) {\n                sender.sendMessage(this.cannotUseChannel(sender));\n            }\n\n            return Collections.emptyList();\n        }\n\n        final List<Audience> recipients = new ArrayList<>();\n        for (final Player player : this.onlinePlayers(residentList)) {\n            final @Nullable CarbonPlayer carbon = this.users.user(player.getUniqueId()).getNow(null);\n            if (carbon != null) {\n                recipients.add(carbon);\n            }\n        }\n\n        recipients.add(this.server.console());\n\n        return recipients;\n    }\n\n    protected List<Player> onlinePlayers(final T residentList) {\n        return TOWNY_API.getOnlinePlayers(residentList);\n    }\n\n    protected abstract Component cannotUseChannel(CarbonPlayer player);\n\n    @Override\n    public boolean shouldCrossServer() {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/towny/TownChannel.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.towny;\n\nimport com.palmergames.bukkit.towny.object.Resident;\nimport com.palmergames.bukkit.towny.object.Town;\nimport java.util.List;\nimport java.util.Map;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.channels.messages.ConfigChannelMessageSource;\nimport net.draycia.carbon.common.config.ConfigHeader;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\n@DefaultQualifier(NonNull.class)\n@ConfigSerializable\n@ConfigHeader(ResidentListChannel.TOWNY_CHANNEL_HEADER)\npublic class TownChannel extends ResidentListChannel<Town> {\n\n    public static final String FILE_NAME = \"towny-townchat.conf\";\n\n    public TownChannel() {\n        this.key = Key.key(\"carbon\", \"townchat\");\n        this.commandAliases = List.of(\"tc\");\n\n        this.messageSource = new ConfigChannelMessageSource();\n        this.messageSource.defaults = Map.of(\n            \"default_format\", \"(town: %townyadvanced_town_unformatted%) <display_name>: <message>\",\n            \"console\", \"[town: %townyadvanced_town_unformatted%] <username>: <message>\"\n        );\n    }\n\n    @Override\n    protected @Nullable Town residentList(final CarbonPlayer player) {\n        final @Nullable Resident resident = TOWNY_API.getResident(player.uuid());\n        if (resident == null) {\n            return null;\n        }\n        return TOWNY_API.getResidentTownOrNull(resident);\n    }\n\n    @Override\n    protected Component cannotUseChannel(final CarbonPlayer player) {\n        return this.messages.cannotUseTownChannel(player);\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/integration/towny/TownyIntegration.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.integration.towny;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.Integration;\nimport org.bukkit.Bukkit;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.configurate.objectmapping.ConfigSerializable;\n\n@DefaultQualifier(NonNull.class)\npublic final class TownyIntegration implements Integration {\n\n    private final CarbonChannelRegistry channelRegistry;\n    private final Config config;\n\n    @Inject\n    public TownyIntegration(\n        final CarbonChannelRegistry channelRegistry,\n        final ConfigManager configManager\n    ) {\n        this.channelRegistry = channelRegistry;\n        this.config = this.config(configManager, configMeta());\n    }\n\n    @Override\n    public boolean eligible() {\n        return this.config.enabled && Bukkit.getPluginManager().isPluginEnabled(\"Towny\");\n    }\n\n    @Override\n    public void register() {\n        if (this.config.townChannel) {\n            this.channelRegistry.registerSpecialConfigChannel(TownChannel.FILE_NAME, TownChannel.class);\n        }\n\n        if (this.config.nationChannel) {\n            this.channelRegistry.registerSpecialConfigChannel(NationChannel.FILE_NAME, NationChannel.class);\n        }\n\n        if (this.config.allianceChannel) {\n            this.channelRegistry.registerSpecialConfigChannel(AllianceChannel.FILE_NAME, AllianceChannel.class);\n        }\n    }\n\n    public static ConfigMeta configMeta() {\n        return Integration.configMeta(\"towny\", TownyIntegration.Config.class);\n    }\n\n    @ConfigSerializable\n    public static final class Config {\n        boolean enabled = true;\n        boolean townChannel = true;\n        boolean nationChannel = true;\n        boolean allianceChannel = false;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/listeners/PaperChatListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.listeners;\n\nimport com.google.inject.Inject;\nimport io.papermc.paper.event.player.AsyncChatCommandDecorateEvent;\nimport io.papermc.paper.event.player.AsyncChatDecorateEvent;\nimport io.papermc.paper.event.player.AsyncChatEvent;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.event.events.CarbonChatEventImpl;\nimport net.draycia.carbon.common.event.events.CarbonEarlyChatEvent;\nimport net.draycia.carbon.common.listeners.ChatListenerInternal;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.identity.Identity;\nimport net.kyori.adventure.text.Component;\nimport org.bukkit.event.EventHandler;\nimport org.bukkit.event.EventPriority;\nimport org.bukkit.event.Listener;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class PaperChatListener extends ChatListenerInternal implements Listener {\n\n    private final CarbonChat carbonChat;\n    final ConfigManager configManager;\n\n    @Inject\n    public PaperChatListener(\n        final CarbonChat carbonChat,\n        final CarbonMessages carbonMessages,\n        final ConfigManager configManager\n    ) {\n        super(carbonChat.eventHandler(), carbonMessages, configManager);\n        this.carbonChat = carbonChat;\n        this.configManager = configManager;\n    }\n\n    @SuppressWarnings(\"UnstableApiUsage\")\n    @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)\n    public void onPaperChatDecorate(final @NonNull AsyncChatDecorateEvent event) {\n        if (event.player() == null) {\n            return;\n        }\n\n        final @Nullable CarbonPlayer sender = this.carbonChat.userManager().user(event.player().getUniqueId()).join();\n        final @Nullable CarbonEarlyChatEvent earlyChatEvent = this.prepareAndEmitPreChatEvent(sender, event.result());\n\n        if (earlyChatEvent == null || earlyChatEvent.cancelled()) {\n            event.setCancelled(true);\n            return;\n        }\n\n        final @Nullable Component message = this.parseTags(sender, earlyChatEvent.message());\n\n        if (message != null) {\n            event.result(message);\n        }\n    }\n\n    @SuppressWarnings(\"UnstableApiUsage\")\n    @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)\n    public void onPaperCommandDecorate(final @NonNull AsyncChatCommandDecorateEvent event) {\n        this.onPaperChatDecorate(event);\n    }\n\n    @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST)\n    public void onPaperChat(final @NonNull AsyncChatEvent event) {\n        final @Nullable CarbonPlayer sender = this.carbonChat.userManager().user(event.getPlayer().getUniqueId()).join();\n\n        if (event.viewers().isEmpty()) {\n            return;\n        }\n\n        final @Nullable CarbonChatEventImpl chatEvent = this.prepareAndEmitChatEvent(sender, event.message(), event.signedMessage());\n\n        if (chatEvent == null || chatEvent.cancelled()) {\n            event.setCancelled(true);\n            return;\n        }\n\n        try {\n            event.viewers().clear();\n            event.viewers().addAll(chatEvent.recipients());\n        } catch (final UnsupportedOperationException exception) {\n            exception.printStackTrace();\n        }\n\n        event.renderer(($, $$, $$$, recipient) -> {\n            final var recipientUUID = recipient.get(Identity.UUID);\n            final Audience recipientViewer;\n\n            if (recipientUUID.isPresent()) {\n                recipientViewer = this.carbonChat.userManager().user(recipientUUID.get()).join();\n            } else {\n                recipientViewer = recipient;\n            }\n\n            return chatEvent.renderFor(recipientViewer);\n        });\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/listeners/PaperPlayerJoinListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.listeners;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Provider;\nimport java.util.List;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.messaging.packets.PacketFactory;\nimport net.draycia.carbon.common.users.ProfileCache;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport org.apache.logging.log4j.Logger;\nimport org.bukkit.event.EventHandler;\nimport org.bukkit.event.EventPriority;\nimport org.bukkit.event.Listener;\nimport org.bukkit.event.player.PlayerJoinEvent;\nimport org.bukkit.event.player.PlayerLoginEvent;\nimport org.bukkit.event.player.PlayerQuitEvent;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static net.draycia.carbon.common.users.PlayerUtils.joinExceptionHandler;\nimport static net.draycia.carbon.common.users.PlayerUtils.saveExceptionHandler;\n\n@DefaultQualifier(NonNull.class)\npublic class PaperPlayerJoinListener implements Listener {\n\n    private final ConfigManager configManager;\n    private final Logger logger;\n    private final ProfileCache profileCache;\n    private final UserManagerInternal<?> userManager;\n    private final Provider<MessagingManager> messaging;\n    private final PacketFactory packetFactory;\n\n    @Inject\n    public PaperPlayerJoinListener(\n        final ConfigManager configManager,\n        final Logger logger,\n        final ProfileCache profileCache,\n        final UserManagerInternal<?> userManager,\n        final Provider<MessagingManager> messaging,\n        final PacketFactory packetFactory\n    ) {\n        this.configManager = configManager;\n        this.logger = logger;\n        this.profileCache = profileCache;\n        this.userManager = userManager;\n        this.messaging = messaging;\n        this.packetFactory = packetFactory;\n    }\n\n    @EventHandler\n    public void onLogin(final PlayerLoginEvent event) {\n        this.profileCache.cache(event.getPlayer().getUniqueId(), event.getPlayer().getName());\n    }\n\n    @EventHandler(priority = EventPriority.LOWEST)\n    public void onJoinEarly(final PlayerJoinEvent event) {\n        this.messaging.get().queuePacket(() -> this.packetFactory.addLocalPlayerPacket(event.getPlayer().getUniqueId(), event.getPlayer().getName()));\n    }\n\n    @EventHandler(priority = EventPriority.HIGH)\n    public void onJoin(final PlayerJoinEvent event) {\n        this.userManager.user(event.getPlayer().getUniqueId()).exceptionally(joinExceptionHandler(this.logger, event.getPlayer().getName(), event.getPlayer().getUniqueId()));\n\n        final @Nullable List<String> suggestions = this.configManager.primaryConfig().customChatSuggestions();\n\n        if (suggestions == null || suggestions.isEmpty()) {\n            return;\n        }\n\n        event.getPlayer().addAdditionalChatCompletions(suggestions);\n    }\n\n    @EventHandler(priority = EventPriority.HIGH)\n    public void onQuit(final PlayerQuitEvent event) {\n        this.userManager.loggedOut(event.getPlayer().getUniqueId())\n            .exceptionally(saveExceptionHandler(this.logger, event.getPlayer().getName(), event.getPlayer().getUniqueId()));\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/messages/PaperMessageRenderer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.messages;\n\nimport com.google.common.base.Suppliers;\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport io.github.miniplaceholders.api.MiniPlaceholders;\nimport java.util.function.Supplier;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersIntegration;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil;\nimport net.draycia.carbon.common.messages.CarbonMessageRenderer;\nimport net.draycia.carbon.common.messages.RenderForTagResolver;\nimport net.draycia.carbon.common.messages.SourcedAudience;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.draycia.carbon.paper.CarbonChatPaper;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport org.bukkit.Bukkit;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static java.util.Objects.requireNonNull;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic class PaperMessageRenderer extends CarbonMessageRenderer {\n\n    private final Supplier<@MonotonicNonNull PlaceholderAPIMiniMessageParser> placeholderApiProcessor = Suppliers.memoize(() -> {\n        if (CarbonChatPaper.papiLoaded()) {\n            return PlaceholderAPIMiniMessageParser.create(MiniMessage.miniMessage());\n        }\n        return null;\n    });\n    private final ConfigManager configManager;\n    private final MiniMessage miniMessage;\n\n    @Inject\n    public PaperMessageRenderer(final ConfigManager configManager, final RenderForTagResolver.Factory renderForTagResolver) {\n        super(renderForTagResolver);\n        this.miniMessage = MiniMessage.miniMessage();\n        this.configManager = configManager;\n    }\n\n    @Override\n    public Component render(\n        final Audience receiver,\n        final String intermediateMessage,\n        final TagResolver.Builder tagResolver\n    ) {\n        final String placeholderResolvedMessage = this.configManager.primaryConfig().applyCustomPlaceholders(intermediateMessage);\n\n        if (MiniPlaceholdersUtil.miniPlaceholdersLoaded()) {\n            tagResolver.resolver(MiniPlaceholders.globalPlaceholders());\n        }\n\n        if (!(receiver instanceof SourcedAudience sourced)) {\n            return this.miniMessage.deserialize(placeholderResolvedMessage, tagResolver.build());\n        }\n\n        if (!(sourced.sender() instanceof CarbonPlayer sender && sender.online())) {\n            return this.miniMessage.deserialize(placeholderResolvedMessage, tagResolver.build());\n        }\n\n        // We can't/shouldn't resolve placeholders for non-players\n        if (sender instanceof ConsoleCarbonPlayer) {\n            return this.miniMessage.deserialize(placeholderResolvedMessage, tagResolver.build());\n        }\n\n        final Player senderBukkitPlayer = requireNonNull(Bukkit.getPlayer(sender.uuid()));\n\n        final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig = MiniPlaceholdersUtil.miniPlaceholdersLoaded()\n            ? this.configManager.primaryConfig().integrations().config(MiniPlaceholdersIntegration.configMeta())\n            : null;\n\n        if (miniplaceholdersConfig != null) {\n            tagResolver.resolver(MiniPlaceholders.audiencePlaceholders());\n        }\n\n        if (!(sourced.recipient() instanceof CarbonPlayer recipient && recipient.online())) {\n            if (this.hasPlaceholderAPI()) {\n                return this.placeholderApiProcessor.get().parse(senderBukkitPlayer,\n                    placeholderResolvedMessage, tagResolver.build(), miniplaceholdersConfig);\n            }\n            return this.miniMessage.deserialize(placeholderResolvedMessage, MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, sourced.recipient(), senderBukkitPlayer), tagResolver.build());\n        }\n\n        final @Nullable Player recipientBukkitPlayer = Bukkit.getPlayer(recipient.uuid());\n        if (recipientBukkitPlayer == null) {\n            if (this.hasPlaceholderAPI()) {\n                return this.placeholderApiProcessor.get().parse(senderBukkitPlayer,\n                    placeholderResolvedMessage, tagResolver.build(), miniplaceholdersConfig);\n            }\n            return this.miniMessage.deserialize(placeholderResolvedMessage, MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, sourced.recipient(), senderBukkitPlayer), tagResolver.build());\n        }\n\n        if (miniplaceholdersConfig != null && miniplaceholdersConfig.relationalPlaceholders) {\n            tagResolver.resolver(MiniPlaceholders.relationalPlaceholders());\n        }\n        if (this.hasPlaceholderAPI()) {\n            return this.placeholderApiProcessor.get().parseRelational(recipientBukkitPlayer,\n                senderBukkitPlayer, placeholderResolvedMessage, tagResolver.build(), miniplaceholdersConfig);\n        }\n\n        return this.miniMessage.deserialize(placeholderResolvedMessage, MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, recipientBukkitPlayer, senderBukkitPlayer), tagResolver.build());\n    }\n\n    private boolean hasPlaceholderAPI() {\n        return this.placeholderApiProcessor.get() != null;\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/messages/PlaceholderAPIMiniMessageParser.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.messages;\n\nimport java.util.function.UnaryOperator;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport me.clip.placeholderapi.PlaceholderAPI;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersIntegration;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class PlaceholderAPIMiniMessageParser {\n\n    private final MiniMessage miniMessage;\n\n    private PlaceholderAPIMiniMessageParser(final MiniMessage miniMessage) {\n        this.miniMessage = miniMessage;\n    }\n\n    public static PlaceholderAPIMiniMessageParser create(final MiniMessage backingInstance) {\n        return new PlaceholderAPIMiniMessageParser(backingInstance);\n    }\n\n    private static boolean containsLegacyColorCodes(final String string) {\n        final char[] charArray = string.toCharArray();\n        for (int i = 0; i < charArray.length - 1; i++) {\n            if (charArray[i] == LegacyComponentSerializer.SECTION_CHAR\n                && \"0123456789AaBbCcDdEeFfKkLlMmNnOoRrXx\".indexOf(charArray[i + 1]) > -1) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    public Component parse(final Player player, final String input, final TagResolver tagResolver, final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig) {\n        return this.parse(\n            null,\n            player,\n            PlaceholderAPI.getPlaceholderPattern(),\n            match -> PlaceholderAPI.setPlaceholders(player, match),\n            input,\n            tagResolver,\n            miniplaceholdersConfig\n        );\n    }\n\n    public Component parse(final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig, final Player player, final String input) {\n        return this.parse(player, input, TagResolver.empty(), miniplaceholdersConfig);\n    }\n\n    public Component parseRelational(final Player recipient, final Player sender, final String input, final TagResolver tagResolver, final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig) {\n        return this.parse(\n            recipient,\n            sender,\n            PlaceholderAPI.getPlaceholderPattern(),\n            match -> PlaceholderAPI.setPlaceholders(sender, PlaceholderAPI.setRelationalPlaceholders(recipient, sender, match)),\n            input,\n            tagResolver,\n            miniplaceholdersConfig\n        );\n    }\n\n    public Component parseRelational(final Player recipient, final Player sender, final String input, final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig) {\n        return this.parseRelational(recipient, sender, input, TagResolver.empty(), miniplaceholdersConfig);\n    }\n\n    private Component parse(\n        final @Nullable Audience recipient,\n        final Audience sender,\n        final Pattern pattern,\n        final UnaryOperator<String> placeholderResolver,\n        final String input,\n        final TagResolver originalTags,\n        final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig\n    ) {\n        final Matcher matcher = pattern.matcher(input);\n        final TagResolver.Builder tagResolver = TagResolver.builder().resolvers(originalTags);\n        final StringBuilder builder = new StringBuilder();\n        int id = 0;\n\n        while (matcher.find()) {\n            final String match = matcher.group();\n            final String replaced = placeholderResolver.apply(match);\n\n            if (match.equals(replaced) || !containsLegacyColorCodes(replaced)) {\n                matcher.appendReplacement(builder, Matcher.quoteReplacement(replaced));\n            } else {\n                final String key = \"papi_generated_template_\" + id;\n                id++;\n                tagResolver.tag(key, Tag.inserting(LegacyComponentSerializer.legacySection().deserialize(replaced)));\n                matcher.appendReplacement(builder, Matcher.quoteReplacement(\"<\" + key + \">\"));\n            }\n        }\n\n        matcher.appendTail(builder);\n\n        return this.miniMessage.deserialize(builder.toString(), MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, recipient, sender), tagResolver.build());\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/users/CarbonPlayerPaper.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.users;\n\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport io.papermc.paper.datacomponent.DataComponentTypes;\nimport java.util.Collection;\nimport java.util.Locale;\nimport java.util.Optional;\nimport java.util.function.Consumer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.InventorySlot;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.users.WrappedCarbonPlayer;\nimport net.draycia.carbon.common.util.EmptyAudienceWithPointers;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport net.kyori.adventure.text.Component;\nimport org.bukkit.Bukkit;\nimport org.bukkit.entity.Player;\nimport org.bukkit.inventory.EntityEquipment;\nimport org.bukkit.inventory.EquipmentSlot;\nimport org.bukkit.inventory.ItemRarity;\nimport org.bukkit.inventory.ItemStack;\nimport org.bukkit.metadata.MetadataValue;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonPlayerPaper extends WrappedCarbonPlayer implements ForwardingAudience.Single {\n\n    @AssistedInject\n    private CarbonPlayerPaper(\n        final @Assisted CarbonPlayerCommon carbonPlayerCommon,\n        final ConfigManager config\n    ) {\n        super(carbonPlayerCommon);\n\n        if (config.primaryConfig().nickname().useCarbonNicknames()) {\n            this.player().ifPresent(this.applyDisplayNameToBukkit(this.hasNickname() ? this.displayName() : null));\n        }\n    }\n\n    private Optional<Player> player() {\n        return Optional.ofNullable(Bukkit.getPlayer(this.carbonPlayerCommon.uuid()));\n    }\n\n    @Override\n    protected Optional<Component> platformDisplayName() {\n        return this.player().map(Player::displayName);\n    }\n\n    @Override\n    public @NotNull Audience audience() {\n        return this.player().map(player -> (Audience) player).orElseGet(() -> EmptyAudienceWithPointers.forCarbonPlayer(this));\n    }\n\n    @Override\n    public double distanceSquaredFrom(final CarbonPlayer other) {\n        if (this.player().isEmpty()) {\n            return -1;\n        }\n\n        final @Nullable Player player = this.player().orElse(null);\n        final @Nullable Player otherPlayer = Bukkit.getPlayer(other.uuid());\n\n        if (player == null || otherPlayer == null) {\n            return -1;\n        }\n\n        return player.getLocation().distanceSquared(otherPlayer.getLocation());\n    }\n\n    @Override\n    public boolean sameWorldAs(final CarbonPlayer other) {\n        if (this.player().isEmpty()) {\n            return false;\n        }\n\n        final Optional<Player> player = this.player();\n        final @Nullable Player otherPlayer = Bukkit.getPlayer(other.uuid());\n\n        if (player.isEmpty() || otherPlayer == null) {\n            return false;\n        }\n\n        return player.get().getWorld().equals(otherPlayer.getWorld());\n    }\n\n    @Override\n    public void nickname(final @Nullable Component nickname) {\n        super.nickname(nickname);\n\n        this.player().ifPresent(this.applyDisplayNameToBukkit(nickname == null ? null : this.displayName()));\n    }\n\n    private Consumer<Player> applyDisplayNameToBukkit(final @Nullable Component displayName) {\n        return bukkit -> this.carbonPlayerCommon.schedule(() -> {\n            bukkit.displayName(displayName);\n\n            if (this.carbonPlayerCommon.configManager().primaryConfig().nickname().updateTabList()) {\n                bukkit.playerListName(displayName);\n            }\n        });\n    }\n\n    @Override\n    public @Nullable Component createItemHoverComponent(final InventorySlot slot) {\n        final Optional<Player> player = this.player(); // This is temporary (it's not)\n\n        if (player.isEmpty()) {\n            return null;\n        }\n\n        final EquipmentSlot equipmentSlot;\n\n        if (slot.equals(InventorySlot.MAIN_HAND)) {\n            equipmentSlot = EquipmentSlot.HAND;\n        } else if (slot.equals(InventorySlot.OFF_HAND)) {\n            equipmentSlot = EquipmentSlot.OFF_HAND;\n        } else if (slot.equals(InventorySlot.HELMET)) {\n            equipmentSlot = EquipmentSlot.HEAD;\n        } else if (slot.equals(InventorySlot.CHEST)) {\n            equipmentSlot = EquipmentSlot.CHEST;\n        } else if (slot.equals(InventorySlot.LEGS)) {\n            equipmentSlot = EquipmentSlot.LEGS;\n        } else if (slot.equals(InventorySlot.BOOTS)) {\n            equipmentSlot = EquipmentSlot.FEET;\n        } else {\n            return null;\n        }\n\n        final @Nullable EntityEquipment equipment = player.get().getEquipment();\n\n        if (equipment == null) {\n            return null;\n        }\n\n        final @Nullable ItemStack itemStack = equipment.getItem(equipmentSlot);\n\n        if (itemStack == null || itemStack.getType().isAir()) {\n            return null;\n        }\n\n        final int amount = Math.min(itemStack.getAmount(), 99);\n        final Component quantity = amount <= 1 ? Component.empty() : Component.text(\" x\" + amount);\n\n        return Component.empty().append(\n            Component.text(\"[\"),\n            itemStack.effectiveName(),\n            quantity,\n            Component.text(\"]\")\n        )\n            .hoverEvent(itemStack)\n            .colorIfAbsent(itemStack.getDataOrDefault(DataComponentTypes.RARITY, ItemRarity.COMMON).color());\n    }\n\n    @Override\n    public @Nullable Locale locale() {\n        return this.player().map(Player::locale).orElse(null);\n    }\n\n    @Override\n    public void sendMessageAsPlayer(final String message) {\n        // TODO: ensure method is not executed from main thread\n        // bukkit doesn't like that\n        this.player().ifPresent(player -> player.chat(message));\n    }\n\n    @Override\n    public boolean online() {\n        return this.player().isPresent();\n    }\n\n    @Override\n    public boolean vanished() {\n        return this.hasVanishMeta();\n    }\n\n    // Supported by PremiumVanish, SuperVanish, VanishNoPacket\n    private boolean hasVanishMeta() {\n        return this.player().stream()\n            .map(player -> player.getMetadata(\"vanished\"))\n            .flatMap(Collection::stream)\n            .filter(value -> value.value() instanceof Boolean)\n            .anyMatch(MetadataValue::asBoolean);\n    }\n\n    public @Nullable Player bukkitPlayer() {\n        return Bukkit.getPlayer(this.uuid());\n    }\n\n}\n"
  },
  {
    "path": "paper/src/main/java/net/draycia/carbon/paper/users/PaperProfileResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.paper.users;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.common.users.MojangProfileResolver;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport org.bukkit.Server;\nimport org.bukkit.entity.Player;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic final class PaperProfileResolver implements ProfileResolver {\n\n    private final Server server;\n    private final ProfileResolver mojang;\n\n    @Inject\n    private PaperProfileResolver(final Server server, final MojangProfileResolver mojang) {\n        this.server = server;\n        this.mojang = mojang;\n    }\n\n    @Override\n    public CompletableFuture<@Nullable UUID> resolveUUID(final String username, final boolean cacheOnly) {\n        final @Nullable Player exact = this.server.getPlayerExact(username);\n        if (exact != null) {\n            return CompletableFuture.completedFuture(exact.getUniqueId());\n        }\n        final @Nullable Player online = this.server.getPlayer(username);\n        if (online != null) {\n            return CompletableFuture.completedFuture(online.getUniqueId());\n        }\n\n        return this.mojang.resolveUUID(username, cacheOnly);\n    }\n\n    @Override\n    public CompletableFuture<@Nullable String> resolveName(final UUID uuid, final boolean cacheOnly) {\n        final @Nullable Player online = this.server.getPlayer(uuid);\n        if (online != null) {\n            return CompletableFuture.completedFuture(online.getName());\n        }\n\n        return this.mojang.resolveName(uuid, cacheOnly);\n    }\n\n    @Override\n    public void shutdown() {\n        this.mojang.shutdown();\n    }\n\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n    \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n    \"extends\": [\n        \"config:recommended\"\n    ],\n    \"ignoreDeps\": [\n        \"quiet-fabric-loom\",\n        \"com.mojang:minecraft\",\n        \"com.google.code.gson:gson\",\n        \"com.google.guava:guava\",\n        \"io.netty:netty-all\",\n        \"org.apache.logging.log4j:log4j-bom\"\n    ],\n    \"labels\": [\n        \"dependencies\"\n    ],\n    \"packageRules\": [\n        {\n            \"description\": \"Correct Fabric API version handling\",\n            \"matchPackageNames\": [\n                \"net.fabricmc.fabric-api:fabric-api\",\n                \"net.fabricmc.fabric-api:fabric-api-deprecated\"\n            ],\n            \"versioning\": \"regex:^(?<major>\\\\d+)(\\\\.(?<minor>\\\\d+))?(\\\\.(?<patch>\\\\d+))?(?:\\\\+(?<compatibility>.*))?$\"\n        },\n        {\n            \"description\": \"Correct FactionsUUID version handling\",\n            \"matchPackageNames\": [\n                \"com.massivecraft:Factions\"\n            ],\n            \"versioning\": \"regex:^1\\\\.6\\\\.9\\\\.5\\\\-U(?<major>\\\\d+)(\\\\.(?<minor>\\\\d+))?(\\\\.(?<patch>\\\\d+))?$\"\n        },\n        {\n            \"description\": \"Towny version handling\",\n            \"matchPackageNames\": [\n                \"com.palmergames.bukkit.towny:towny\"\n            ],\n            \"versioning\": \"regex:^0\\\\.(?<major>\\\\d+)(\\\\.(?<minor>\\\\d+))?(\\\\.(?<patch>\\\\d+))?$\"\n        },\n        {\n            \"description\": \"Ignore Towny patch updates\",\n            \"matchPackageNames\": [\n                \"com.palmergames.bukkit.towny:towny\"\n            ],\n            \"matchUpdateTypes\": \"patch\",\n            \"enabled\": false\n        },\n        {\n            \"matchManagers\": [\n                \"github-actions\",\n                \"gradle-wrapper\"\n            ],\n            \"groupName\": \"gradle and github actions\"\n        },\n        {\n            \"matchDepTypes\": [\n                \"plugin\"\n            ],\n            \"groupName\": \"gradle and github actions\"\n        },\n        {\n            \"matchFileNames\": [\n                \"build-logic/*\",\n                \"buildSrc/*\"\n            ],\n            \"groupName\": \"gradle and github actions\"\n        }\n    ],\n    \"schedule\": [\n        \"before 4am on Monday\"\n    ],\n    \"semanticCommitType\": \"build\",\n    \"commitMessagePrefix\": \"chore(deps): \"\n}\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "enableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")\n\ndependencyResolutionManagement {\n  repositories {\n    mavenCentral {\n      mavenContent { releasesOnly() }\n    }\n    maven(\"https://repo.jpenilla.xyz/snapshots/\") {\n      mavenContent {\n        snapshotsOnly()\n        includeModuleByRegex(\"de\\\\.hexaoxi\", \"messenger-.*\")\n        includeModule(\"org.incendo\", \"cloud-sponge\")\n        includeModule(\"com.seiama\", \"registry\")\n      }\n    }\n    maven(\"https://central.sonatype.com/repository/maven-snapshots/\") {\n      mavenContent { snapshotsOnly() }\n    }\n    // PaperMC\n    maven(\"https://repo.papermc.io/repository/maven-public/\")\n    // Sponge API\n    maven(\"https://repo.spongepowered.org/repository/maven-public/\")\n    // PlaceholderAPI\n    maven(\"https://repo.extendedclip.com/content/repositories/placeholderapi/\") {\n      content { includeGroup(\"me.clip\") }\n    }\n    // EssentialsDiscord\n    maven(\"https://repo.essentialsx.net/releases/\") {\n      mavenContent {\n        releasesOnly()\n        includeGroup(\"net.essentialsx\")\n      }\n    }\n    maven(\"https://repo.essentialsx.net/snapshots/\") {\n      mavenContent {\n        snapshotsOnly()\n        includeGroup(\"net.essentialsx\")\n      }\n    }\n    // DiscordSRV\n    maven(\"https://nexus.scarsz.me/content/groups/public/\") {\n      mavenContent {\n        includeGroup(\"com.discordsrv\")\n      }\n    }\n    // Glare's repo for Towny\n    maven(\"https://repo.glaremasters.me/repository/towny/\") {\n      content { includeGroup(\"com.palmergames.bukkit.towny\") }\n    }\n    // FactionsUUID\n    maven(\"https://ci.ender.zone/plugin/repository/everything/\") {\n      content { includeGroup(\"com.massivecraft\") }\n    }\n    // mcMMO\n    maven(\"https://nexus.neetgames.com/repository/maven-releases/\") {\n      content {\n        includeGroup(\"com.gmail.nossr50.mcMMO\")\n      }\n    }\n    // Parties\n    maven(\"https://repo.alessiodp.com/releases/\") {\n      content {\n        includeGroup(\"com.alessiodp.parties\")\n      }\n    }\n  }\n  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n}\n\npluginManagement {\n  repositories {\n    gradlePluginPortal()\n    maven(\"https://central.sonatype.com/repository/maven-snapshots/\") {\n      mavenContent { snapshotsOnly() }\n    }\n    maven(\"https://maven.fabricmc.net/\")\n    maven(\"https://repo.jpenilla.xyz/snapshots/\")\n  }\n  includeBuild(\"build-logic\")\n}\n\nplugins {\n  id(\"org.gradle.toolchains.foojay-resolver-convention\") version \"1.0.0\"\n  id(\"quiet-fabric-loom\") version \"1.16-SNAPSHOT\"\n}\n\nrootProject.name = \"CarbonChat\"\n\nlistOf(\n  \"api\",\n  \"common\",\n  \"paper\",\n  // \"sponge\", // TODO API 10\n  \"fabric\",\n  \"velocity\"\n).forEach {\n  include(\"carbonchat-$it\")\n  project(\":carbonchat-$it\").projectDir = file(it)\n}\n"
  },
  {
    "path": "sponge/build.gradle.kts",
    "content": "import org.spongepowered.gradle.plugin.config.PluginLoaders\nimport org.spongepowered.plugin.metadata.model.PluginDependency\nimport java.util.*\n\nplugins {\n  id(\"carbon.shadow-platform\")\n  id(\"org.spongepowered.gradle.plugin\")\n}\n\ndependencies {\n  implementation(projects.carbonchatCommon)\n  implementation(libs.cloudSponge)\n  //implementation(libs.bstatsSponge) // not updated for api 8 yet\n}\n\ntasks {\n  shadowJar {\n    dependencies {\n      // included in sponge\n      exclude(dependency(\"io.leangen.geantyref:geantyref\"))\n      exclude(dependency(\"com.google.inject:guice\"))\n      exclude(dependency(\"aopalliance:aopalliance\"))\n      exclude(dependency(\"javax.inject:javax.inject\"))\n    }\n  }\n}\n\nsponge {\n  injectRepositories(false) // We specify repositories in settings.gradle.kts\n  apiVersion(\"10.0.0-SNAPSHOT\")\n  plugin(rootProject.name.toLowerCase(Locale.ROOT)) {\n    loader {\n      name(PluginLoaders.JAVA_PLAIN)\n      version(\"1.0\")\n    }\n    displayName(rootProject.name)\n    entrypoint(\"net.draycia.carbon.sponge.CarbonChatSponge\")\n    description(project.description)\n    license(\"GPLv3\")\n    links {\n      homepage(GITHUB_REPO_URL)\n      source(GITHUB_REPO_URL)\n      issues(\"$GITHUB_REPO_URL/issues\")\n    }\n    contributor(\"Vicarious\") {\n      description(\"Lead Developer\")\n    }\n    contributor(\"Glare\") {\n      description(\"Moral Support\")\n    }\n    dependency(\"spongeapi\") {\n      loadOrder(PluginDependency.LoadOrder.AFTER)\n      optional(false)\n    }\n    dependency(\"luckperms\") {\n      version(\">=5.0.0\")\n      optional(true)\n    }\n  }\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/CarbonChatSponge.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.google.inject.TypeLiteral;\nimport java.nio.file.Path;\nimport java.util.Set;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.TimeUnit;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.CarbonChatProvider;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.events.CarbonEventHandler;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.api.util.Component;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.util.CloudUtils;\nimport net.draycia.carbon.common.util.ListenerUtils;\nimport net.draycia.carbon.common.util.PlayerUtils;\nimport net.draycia.carbon.sponge.listeners.SpongeChatListener;\nimport net.draycia.carbon.sponge.listeners.SpongePlayerJoinListener;\nimport net.draycia.carbon.sponge.listeners.SpongeReloadListener;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.moonshine.message.IMessageRenderer;\nimport ninja.egg82.messenger.services.PacketService;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.api.Game;\nimport org.spongepowered.api.Server;\nimport org.spongepowered.api.Sponge;\nimport org.spongepowered.api.config.ConfigDir;\nimport org.spongepowered.api.event.Listener;\nimport org.spongepowered.api.event.lifecycle.StartingEngineEvent;\nimport org.spongepowered.api.event.lifecycle.StoppingEngineEvent;\nimport org.spongepowered.api.scheduler.Task;\nimport org.spongepowered.api.service.permission.PermissionDescription;\nimport org.spongepowered.api.service.permission.PermissionService;\nimport org.spongepowered.plugin.PluginContainer;\nimport org.spongepowered.plugin.builtin.jvm.Plugin;\n\n@Plugin(\"carbonchat\")\n@DefaultQualifier(NonNull.class)\npublic final class CarbonChatSponge implements CarbonChat {\n\n    private static final Set<Class<?>> LISTENER_CLASSES = Set.of(SpongeChatListener.class,\n        SpongePlayerJoinListener.class, SpongeReloadListener.class);\n\n    private static final int BSTATS_PLUGIN_ID = 11279;\n\n    private final CarbonMessages carbonMessages;\n    private final CarbonServerSponge carbonServerSponge;\n    private final ChannelRegistry channelRegistry;\n    private final Injector injector;\n    private final Logger logger;\n    private final Path dataDirectory;\n    private final PluginContainer pluginContainer;\n    private final UserManager<CarbonPlayerCommon> userManager;\n    private final CarbonEventHandler eventHandler = new CarbonEventHandler();\n    private final UUID serverId = UUID.randomUUID();\n\n    private @MonotonicNonNull MessagingManager messagingManager = null;\n\n    @Inject\n    public CarbonChatSponge(\n        //final Metrics.Factory metricsFactory,\n        final Game game,\n        final PluginContainer pluginContainer,\n        final Injector injector,\n        final Logger logger,\n        @ConfigDir(sharedRoot = false) final Path dataDirectory\n    ) {\n        CarbonChatProvider.register(this);\n\n        this.pluginContainer = pluginContainer;\n\n        this.injector = injector.createChildInjector(new CarbonChatSpongeModule(this, dataDirectory, pluginContainer));\n        this.logger = logger;\n        this.carbonMessages = this.injector.getInstance(CarbonMessages.class);\n        this.channelRegistry = this.injector.getInstance(ChannelRegistry.class);\n        this.carbonServerSponge = this.injector.getInstance(CarbonServerSponge.class);\n        this.userManager = this.injector.getInstance(com.google.inject.Key.get(new TypeLiteral<UserManager<CarbonPlayerCommon>>() {}));\n        this.dataDirectory = dataDirectory;\n\n        for (final Class<?> clazz : LISTENER_CLASSES) {\n            game.eventManager().registerListeners(this.pluginContainer, this.injector.getInstance(clazz));\n        }\n\n        //metricsFactory.make(BSTATS_PLUGIN_ID);\n\n        // Listeners\n        ListenerUtils.registerCommonListeners(this.injector);\n\n        // Load channels\n        ((CarbonChannelRegistry) this.channelRegistry()).loadConfigChannels(this.carbonMessages);\n\n        // TODO: Register these in a central location, pull from that in this and plugin.yml\n        Sponge.serviceProvider().provide(PermissionService.class).ifPresent(permissionService -> {\n            final PermissionDescription.Builder builder = permissionService.newDescriptionBuilder(this.pluginContainer);\n\n            builder.id(\"carbon.clearchat.clear\")\n                .description(Component.text(\"Clears the chat for all players except those with carbon.chearchat.exempt.\"))\n                .register();\n\n            builder.id(\"carbon.clearchat.exempt\")\n                .description(Component.text(\"Exempts the player from having their chat cleared when /clearchat is executed.\"))\n                .register();\n\n            builder.id(\"carbon.debug\")\n                .description(Component.text(\"Allows the sender to quickly check what carbon think's the player's primary and non-primary groups are.\"))\n                .register();\n\n            builder.id(\"carbon.help\")\n                .description(Component.text(\"Shows Carbon's help menu, detailing each part of Carbon's commands.\"))\n                .register();\n\n            builder.id(\"carbon.hideidentity\")\n                .description(Component.text(\"Prevents messages from the player from being blocked clientside.\"))\n                .register();\n\n            builder.id(\"carbon.ignore\")\n                .description(Component.text(\"Ignores the player, hiding messages they send in chat and in whispers.\"))\n                .register();\n\n            builder.id(\"carbon.ignore.exempt\")\n                .description(Component.text(\"Prevents the player from being ignored.\"))\n                .register();\n\n            builder.id(\"carbon.ignore.unignore\")\n                .description(Component.text(\"Removes the player from the sender's ignore list.\"))\n                .register();\n\n            builder.id(\"carbon.itemlink\")\n                .description(Component.text(\"Shows the player's held or equipped item in chat.\"))\n                .register();\n\n            builder.id(\"carbon.mute\")\n                .description(Component.text(\"Mutes the player, preventing them from sending messages or whispers.\"))\n                .register();\n\n            builder.id(\"carbon.mute.exempt\")\n                .description(Component.text(\"Prevents the player from being muted.\"))\n                .register();\n\n            builder.id(\"carbon.mute.info\")\n                .description(Component.text(\"Shows if the player is muted or now.\"))\n                .register();\n\n            builder.id(\"carbon.mute.notify\")\n                .description(Component.text(\"Notifies the player when someone else has been mute.\"))\n                .register();\n\n            builder.id(\"carbon.mute.unmute\")\n                .description(Component.text(\"Unmutes the player, allowing them to use chat and send whispers.\"))\n                .register();\n\n            builder.id(\"carbon.nickname\")\n                .description(Component.text(\"The nickname command, by default shows your nickname.\"))\n                .register();\n\n            builder.id(\"carbon.nickname.others\")\n                .description(Component.text(\"Checks/sets other player's nicknames.\"))\n                .register();\n\n            builder.id(\"carbon.nickname.see\")\n                .description(Component.text(\"Checks your/other player's nicknames.\"))\n                .register();\n\n            builder.id(\"carbon.nickname.self\")\n                .description(Component.text(\"Checks/sets your nickname.\"))\n                .register();\n\n            builder.id(\"carbon.nickname.set\")\n                .description(Component.text(\"Sets your/other player's nicknames.\"))\n                .register();\n\n            builder.id(\"carbon.reload\")\n                .description(Component.text(\"Reloads Carbon's config, channel settings, and translations.\"))\n                .register();\n\n            builder.id(\"carbon.whisper\")\n                .description(Component.text(\"Sends private messages to other players.\"))\n                .register();\n\n            builder.id(\"carbon.whisper.continue\")\n                .description(Component.text(\"Sends a message to the last player you whispered.\"))\n                .register();\n\n            builder.id(\"carbon.whisper.reply\")\n                .description(Component.text(\"Sends a message to the last player who messaged you.\"))\n                .register();\n\n            builder.id(\"carbon.whisper.vanished\")\n                .description(Component.text(\"Allows the player to send messages to vanished players.\"))\n                .register();\n        });\n\n        // Commands\n        CloudUtils.loadCommands(this.injector);\n        final var commandSettings = CloudUtils.loadCommandSettings(this.injector);\n        CloudUtils.registerCommands(commandSettings);\n    }\n\n    @Override\n    public UUID serverId() {\n        return this.serverId;\n    }\n\n    @Override\n    public @Nullable PacketService packetService() {\n        if (this.messagingManager == null) {\n            this.messagingManager = this.injector.getInstance(MessagingManager.class);\n        }\n\n        return this.messagingManager.packetService();\n    }\n\n    @Listener\n    public void onInitialize(final StartingEngineEvent<Server> event) {\n        // Player data saving\n        Sponge.asyncScheduler().submit(Task.builder()\n            .interval(5, TimeUnit.MINUTES)\n            .plugin(this.pluginContainer)\n            .execute(() -> PlayerUtils.saveLoggedInPlayers(this.carbonServerSponge, this.userManager))\n            .build());\n    }\n\n    @Listener\n    public void onDisable(final StoppingEngineEvent<Server> event) {\n        PlayerUtils.saveLoggedInPlayers(this.carbonServerSponge, this.userManager).forEach(CompletableFuture::join);\n    }\n\n    @Override\n    public Logger logger() {\n        return this.logger;\n    }\n\n    @Override\n    public Path dataDirectory() {\n        return this.dataDirectory;\n    }\n\n    @Override\n    public CarbonServerSponge server() {\n        return this.carbonServerSponge;\n    }\n\n    @Override\n    public ChannelRegistry channelRegistry() {\n        return this.channelRegistry;\n    }\n\n    @Override\n    public <T extends Audience> IMessageRenderer<T, String, Component, Component> messageRenderer() {\n        return this.injector.getInstance(SpongeMessageRenderer.class);\n    }\n\n    public CarbonMessages carbonMessages() {\n        return this.carbonMessages;\n    }\n\n    @Override\n    public @NonNull CarbonEventHandler eventHandler() {\n        return this.eventHandler;\n    }\n\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/CarbonChatSpongeModule.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge;\n\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.execution.AsynchronousCommandExecutionCoordinator;\nimport org.incendo.cloud.sponge.SpongeCommandManager;\nimport org.incendo.cloud.sponge.argument.SinglePlayerSelectorArgument;\nimport com.google.inject.AbstractModule;\nimport com.google.inject.Injector;\nimport com.google.inject.Provides;\nimport com.google.inject.Singleton;\nimport java.nio.file.Path;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.util.Component;\nimport net.draycia.carbon.api.util.SourcedAudience;\nimport net.draycia.carbon.common.CarbonCommonModule;\nimport net.draycia.carbon.common.ForCarbon;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.argument.PlayerSuggestions;\nimport net.draycia.carbon.common.util.CloudUtils;\nimport net.draycia.carbon.sponge.command.SpongeCommander;\nimport net.draycia.carbon.sponge.command.SpongePlayerCommander;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.moonshine.message.IMessageRenderer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.api.entity.living.player.server.ServerPlayer;\nimport org.spongepowered.plugin.PluginContainer;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonChatSpongeModule extends AbstractModule {\n\n    private final CarbonChatSponge carbonChat;\n    private final Path configDir;\n    private final PluginContainer pluginContainer;\n\n    public CarbonChatSpongeModule(\n        final CarbonChatSponge carbonChat,\n        final Path configDir,\n        final PluginContainer pluginContainer\n    ) {\n        this.carbonChat = carbonChat;\n        this.configDir = configDir;\n        this.pluginContainer = pluginContainer;\n    }\n\n    @Provides\n    @Singleton\n    public CommandManager<Commander> commandManager() {\n        final SpongeCommandManager<Commander> commandManager = new SpongeCommandManager<>(\n            this.pluginContainer,\n            AsynchronousCommandExecutionCoordinator.<Commander>builder().build(),\n            commander -> ((SpongeCommander) commander).commandCause(),\n            commandCause -> {\n                if (commandCause.subject() instanceof ServerPlayer player) {\n                    return new SpongePlayerCommander(this.carbonChat, player, commandCause);\n                }\n\n                return SpongeCommander.from(commandCause);\n            }\n        );\n\n        CloudUtils.decorateCommandManager(commandManager, this.carbonChat.carbonMessages());\n\n        commandManager.parserMapper().cloudNumberSuggestions(true);\n\n        return commandManager;\n    }\n\n    @Provides\n    @Singleton\n    public IMessageRenderer<Audience, String, Component, Component> messageRenderer(final Injector injector) {\n        return injector.getInstance(SpongeMessageRenderer.class);\n    }\n\n    @Provides\n    @Singleton\n    public IMessageRenderer<SourcedAudience, String, Component, Component> sourcedRenderer(final Injector injector) {\n        return injector.getInstance(SpongeMessageRenderer.class);\n    }\n\n    @Override\n    public void configure() {\n        this.install(new CarbonCommonModule());\n\n        this.bind(Path.class).annotatedWith(ForCarbon.class).toInstance(this.configDir);\n        this.bind(CarbonChat.class).toInstance(this.carbonChat);\n        this.bind(CarbonServer.class).to(CarbonServerSponge.class);\n        this.bind(PlayerSuggestions.class).toInstance(new SinglePlayerSelectorArgument.Parser<Commander>()::suggestions);\n    }\n\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/CarbonServerSponge.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.ComponentPlayerResult;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.sponge.users.CarbonPlayerSponge;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.api.Game;\nimport org.spongepowered.api.Sponge;\nimport org.spongepowered.api.profile.ProfileNotFoundException;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic final class CarbonServerSponge implements CarbonServer, ForwardingAudience.Single {\n\n    private final Game game;\n    private final UserManager<CarbonPlayerSponge> userManager;\n\n    @Inject\n    private CarbonServerSponge(final UserManager<CarbonPlayerCommon> userManager, final Game game) {\n        this.game = game;\n        this.userManager = new SpongeUserManager(userManager);\n    }\n\n    @Override\n    public @NotNull Audience audience() {\n        return this.game.server();\n    }\n\n    @Override\n    public Audience console() {\n        return this.game.systemSubject();\n    }\n\n    @Override\n    public List<CarbonPlayerSponge> players() {\n        final var players = new ArrayList<CarbonPlayerSponge>();\n\n        for (final var player : Sponge.server().onlinePlayers()) {\n            final ComponentPlayerResult<CarbonPlayerSponge> result = this.userManager.carbonPlayer(player.uniqueId()).join();\n\n            if (result.player() != null) {\n                players.add(result.player());\n            }\n        }\n\n        return players;\n    }\n\n    @Override\n    public UserManager<CarbonPlayerSponge> userManager() {\n        return this.userManager;\n    }\n\n    @Override\n    public CompletableFuture<@Nullable UUID> resolveUUID(final String username) {\n        return CompletableFuture.supplyAsync(() -> {\n            try {\n                return Sponge.server().gameProfileManager().basicProfile(username).join().uuid();\n            } catch (final ProfileNotFoundException exception) {\n                return null;\n            }\n        });\n    }\n\n    @Override\n    public CompletableFuture<@Nullable String> resolveName(final UUID uuid) {\n        return CompletableFuture.supplyAsync(() -> {\n            try {\n                return Sponge.server().gameProfileManager().basicProfile(uuid).join().name().orElse(null);\n            } catch (final ProfileNotFoundException exception) {\n                return null;\n            }\n        });\n    }\n\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/SpongeMessageRenderer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge;\n\nimport com.google.inject.Inject;\nimport java.lang.reflect.Method;\nimport java.lang.reflect.Type;\nimport java.util.Map;\nimport net.draycia.carbon.common.config.ConfigFactory;\nimport net.draycia.carbon.common.util.ChatType;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.MessageType;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nimport net.kyori.adventure.text.minimessage.tag.Tag;\nimport net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport net.kyori.moonshine.message.IMessageRenderer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class SpongeMessageRenderer<T extends Audience> implements IMessageRenderer<T, String, Component, Component> {\n\n    private final ConfigFactory configFactory;\n\n    @Inject\n    public SpongeMessageRenderer(final ConfigFactory configFactory) {\n        this.configFactory = configFactory;\n    }\n\n    @Override\n    public Component render(\n        final T receiver,\n        final String intermediateMessage,\n        final Map<String, ? extends Component> resolvedPlaceholders,\n        final Method method,\n        final Type owner\n    ) {\n        final TagResolver.Builder tagResolver = TagResolver.builder();\n\n        for (final var entry : resolvedPlaceholders.entrySet()) {\n            tagResolver.tag(entry.getKey(), Tag.inserting(entry.getValue()));\n        }\n\n        this.configFactory.primaryConfig().customPlaceholders().forEach(\n            (key, value) -> tagResolver.resolver(Placeholder.unparsed(key, value))\n        );\n\n        return MiniMessage.miniMessage().deserialize(intermediateMessage, tagResolver.build());;\n    }\n\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/SpongeUserManager.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge;\n\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.api.users.ComponentPlayerResult;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.users.SaveOnChange;\nimport net.draycia.carbon.sponge.users.CarbonPlayerSponge;\nimport net.kyori.adventure.key.Key;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic class SpongeUserManager implements UserManager<CarbonPlayerSponge>, SaveOnChange {\n\n    protected final UserManager<CarbonPlayerCommon> proxiedUserManager;\n\n    public SpongeUserManager(final UserManager<CarbonPlayerCommon> proxiedUserManager) {\n        this.proxiedUserManager = proxiedUserManager;\n    }\n\n    @Override\n    public CompletableFuture<ComponentPlayerResult<CarbonPlayerSponge>> carbonPlayer(final UUID uuid) {\n        return this.proxiedUserManager.carbonPlayer(uuid).thenApply(result -> {\n            if (result.player() == null) {\n                return new ComponentPlayerResult<>(null, result.reason());\n            }\n\n            return new ComponentPlayerResult<>(new CarbonPlayerSponge(result.player()), result.reason());\n        });\n    }\n\n    @Override\n    public CompletableFuture<ComponentPlayerResult<CarbonPlayerSponge>> savePlayer(final CarbonPlayerSponge player) {\n        return this.proxiedUserManager.savePlayer(player.carbonPlayerCommon()).thenApply(result -> {\n            if (result.player() == null) {\n                return new ComponentPlayerResult<>(null, result.reason());\n            }\n\n            return new ComponentPlayerResult<>(new CarbonPlayerSponge(result.player()), result.reason());\n        });\n    }\n\n    @Override\n    public CompletableFuture<ComponentPlayerResult<CarbonPlayerSponge>> saveAndInvalidatePlayer(final CarbonPlayerSponge player) {\n        return this.proxiedUserManager.saveAndInvalidatePlayer(player.carbonPlayerCommon()).thenApply(result -> {\n            if (result.player() == null) {\n                return new ComponentPlayerResult<>(null, result.reason());\n            }\n\n            return new ComponentPlayerResult<>(new CarbonPlayerSponge(result.player()), result.reason());\n        });\n    }\n\n    @Override\n    public int saveDisplayName(final UUID id, final @Nullable Component component) {\n        if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) {\n            return saveOnChange.saveDisplayName(id, component);\n        }\n\n        return -1;\n    }\n\n    @Override\n    public int saveMuted(final UUID id, final boolean muted) {\n        if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) {\n            return saveOnChange.saveMuted(id, muted);\n        }\n\n        return -1;\n    }\n\n    @Override\n    public int saveDeafened(final UUID id, final boolean deafened) {\n        if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) {\n            return saveOnChange.saveDeafened(id, deafened);\n        }\n\n        return -1;\n    }\n\n    @Override\n    public int saveSpying(final UUID id, final boolean spying) {\n        if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) {\n            return saveOnChange.saveSpying(id, spying);\n        }\n\n        return -1;\n    }\n\n    @Override\n    public int saveSelectedChannel(final UUID id, final @Nullable Key selectedChannel) {\n        if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) {\n            return saveOnChange.saveSelectedChannel(id, selectedChannel);\n        }\n\n        return -1;\n    }\n\n    @Override\n    public int saveLastWhisperTarget(final UUID id, final @Nullable UUID lastWhisperTarget) {\n        if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) {\n            return saveOnChange.saveLastWhisperTarget(id, lastWhisperTarget);\n        }\n\n        return -1;\n    }\n\n    @Override\n    public int saveWhisperReplyTarget(final UUID id, final @Nullable UUID whisperReplyTarget) {\n        if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) {\n            return saveOnChange.saveWhisperReplyTarget(id, whisperReplyTarget);\n        }\n\n        return -1;\n    }\n\n    @Override\n    public int addIgnore(final UUID id, final UUID ignoredPlayer) {\n        if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) {\n            return saveOnChange.addIgnore(id, ignoredPlayer);\n        }\n\n        return -1;\n    }\n\n    @Override\n    public int removeIgnore(final UUID id, final UUID ignoredPlayer) {\n        if (this.proxiedUserManager instanceof SaveOnChange saveOnChange) {\n            return saveOnChange.removeIgnore(id, ignoredPlayer);\n        }\n\n        return -1;\n    }\n\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/command/SpongeCommander.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge.command;\n\nimport net.draycia.carbon.common.command.Commander;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.api.command.CommandCause;\n\n@DefaultQualifier(NonNull.class)\npublic interface SpongeCommander extends Commander, ForwardingAudience.Single {\n\n    static SpongeCommander from(final CommandCause commandCause) {\n        return new SpongeCommanderImpl(commandCause);\n    }\n\n    @NonNull CommandCause commandCause();\n\n    record SpongeCommanderImpl(CommandCause commandCause) implements SpongeCommander {\n\n        @Override\n        public @NotNull Audience audience() {\n            return this.commandCause.audience();\n        }\n\n    }\n\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/command/SpongePlayerCommander.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge.command;\n\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.kyori.adventure.audience.Audience;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.api.command.CommandCause;\nimport org.spongepowered.api.entity.living.player.server.ServerPlayer;\n\nimport static java.util.Objects.requireNonNull;\n\n@DefaultQualifier(NonNull.class)\npublic record SpongePlayerCommander(\n    CarbonChat carbon,\n    ServerPlayer player,\n    CommandCause commandCause\n) implements PlayerCommander, SpongeCommander {\n\n    @Override\n    public CarbonPlayer carbonPlayer() {\n        return requireNonNull(this.carbon.server().userManager().carbonPlayer(this.player.uniqueId()).join().player(), \"No CarbonPlayer for logged in Player!\");\n    }\n\n    @Override\n    public @NotNull Audience audience() {\n        return this.commandCause.audience();\n    }\n\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/listeners/SpongeChatListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge.listeners;\n\nimport com.google.inject.Inject;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Optional;\nimport java.util.regex.Pattern;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.channels.ChannelRegistry;\nimport net.draycia.carbon.api.events.CarbonChatEvent;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.ComponentPlayerResult;\nimport net.draycia.carbon.api.util.KeyedRenderer;\nimport net.draycia.carbon.api.util.Component;\nimport net.draycia.carbon.sponge.CarbonChatSponge;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport net.kyori.adventure.audience.MessageType;\nimport net.kyori.adventure.identity.Identity;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.TextReplacementConfig;\nimport net.kyori.adventure.text.event.ClickEvent;\nimport net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.api.entity.living.player.Player;\nimport org.spongepowered.api.entity.living.player.server.ServerPlayer;\nimport org.spongepowered.api.event.Listener;\nimport org.spongepowered.api.event.filter.IsCancelled;\nimport org.spongepowered.api.event.filter.cause.First;\nimport org.spongepowered.api.event.message.PlayerChatEvent;\nimport org.spongepowered.api.util.Tristate;\n\nimport static java.util.Objects.requireNonNullElse;\nimport static net.draycia.carbon.api.util.KeyedRenderer.keyedRenderer;\nimport static net.kyori.adventure.key.Key.key;\nimport static net.kyori.adventure.text.Component.empty;\nimport static net.kyori.adventure.text.Component.text;\n\n@DefaultQualifier(NonNull.class)\npublic final class SpongeChatListener {\n\n    private final CarbonChatSponge carbonChat;\n    private final ChannelRegistry registry;\n    private final CarbonMessages carbonMessages;\n\n    private static final Pattern DEFAULT_URL_PATTERN = Pattern.compile(\"(?:(https?)://)?([-\\\\w_.]+\\\\.\\\\w{2,})(/\\\\S*)?\");\n\n    @Inject\n    private SpongeChatListener(\n        final CarbonChat carbonChat,\n        final ChannelRegistry registry,\n        final CarbonMessages carbonMessages\n    ) {\n        this.carbonChat = (CarbonChatSponge) carbonChat;\n        this.registry = registry;\n        this.carbonMessages = carbonMessages;\n    }\n\n    @Listener\n    @IsCancelled(Tristate.FALSE)\n    public void onPlayerChat(final PlayerChatEvent event, final @First Player source) {\n        final var playerResult = this.carbonChat.server().userManager().carbonPlayer(source.uniqueId()).join();\n        final @Nullable CarbonPlayer sender = playerResult.player();\n\n        if (sender == null) {\n            return;\n        }\n\n        var channel = requireNonNullElse(sender.selectedChannel(), this.registry.defaultValue());\n\n        final var messageContents = PlainTextComponentSerializer.plainText().serialize(event.originalMessage());\n        var eventMessage = event.message();\n\n        final CarbonPlayer.ChannelMessage channelMessage = sender.channelForMessage(eventMessage);\n\n        if (channelMessage.channel() != null) {\n            channel = channelMessage.channel();\n        }\n\n        eventMessage = channelMessage.message();\n\n        if (sender.leftChannels().contains(channel.key())) {\n            sender.joinChannel(channel);\n            this.carbonMessages.channelJoined(sender);\n        }\n\n        for (final var chatChannel : this.registry) {\n            if (chatChannel.quickPrefix() == null) {\n                continue;\n            }\n\n            if (messageContents.startsWith(chatChannel.quickPrefix()) && chatChannel.speechPermitted(sender).permitted()) {\n                channel = chatChannel;\n                eventMessage = eventMessage.replaceText(TextReplacementConfig.builder()\n                    .once()\n                    .matchLiteral(channel.quickPrefix())\n                        .replacement(text())\n                    .build());\n                break;\n            }\n        }\n\n        final List<Audience> recipients;\n\n        if (event.audience().isPresent()) {\n            final var audience = event.audience().get();\n\n            if (audience instanceof ForwardingAudience forwardingAudience) {\n                recipients = new ArrayList<>();\n\n                forwardingAudience.forEachAudience(recipients::add);\n            } else {\n                recipients = channel.recipients(sender);\n            }\n        } else {\n            recipients = channel.recipients(sender);\n        }\n\n        final var renderers = new ArrayList<KeyedRenderer>();\n        renderers.add(keyedRenderer(Key.key(\"carbon\", \"default\"), channel));\n\n        final var chatEvent = new CarbonChatEvent(sender, eventMessage, recipients, renderers, channel, false);\n        final var result = this.carbonChat.eventHandler().emit(chatEvent);\n\n        if (!result.wasSuccessful() || chatEvent.result().cancelled()) {\n            if (!result.exceptions().isEmpty()) {\n                for (var entry : result.exceptions().entrySet()) {\n                    this.carbonChat.logger().error(\"Exception in event handler: \" + entry.getKey().getClass().getName());\n                    entry.getValue().printStackTrace();\n                }\n            }\n\n            final var failure = chatEvent.result().reason();\n\n            if (!failure.equals(empty())) {\n                sender.sendMessage(failure);\n            }\n        }\n\n        try {\n            event.setAudience(Audience.audience(chatEvent.recipients()));\n        } catch (final UnsupportedOperationException exception) {\n            exception.printStackTrace();\n            // Do we log something here? Would get spammy fast.\n        }\n\n        if (sender.hasPermission(\"carbon.hideidentity\")) {\n            for (final var recipient : chatEvent.recipients()) {\n                var renderedMessage = new Component(chatEvent.message(), MessageType.CHAT);\n\n                for (final var renderer : chatEvent.renderers()) {\n                    try {\n                        if (recipient instanceof Player player) {\n                            final ComponentPlayerResult<? extends CarbonPlayer> targetPlayer = this.carbonChat.server().userManager().carbonPlayer(player.uniqueId()).join();\n\n                            renderedMessage = renderer.render(sender, targetPlayer.player(), renderedMessage, chatEvent.message());\n                        } else {\n                            renderedMessage = renderer.render(sender, recipient, renderedMessage, chatEvent.message());\n                        }\n                    } catch (final Exception e) {\n                        e.printStackTrace();\n                    }\n                }\n\n                recipient.sendMessage(Identity.nil(), renderedMessage);\n            }\n        } else {\n            event.setChatFormatter((player, target, msg, originalMessage) -> {\n                Component component = msg;\n\n                for (final var renderer : chatEvent.renderers()) {\n                    if (target instanceof ServerPlayer serverPlayer) {\n                        final ComponentPlayerResult<? extends CarbonPlayer> targetPlayer = this.carbonChat.server().userManager().carbonPlayer(serverPlayer.uniqueId()).join();\n                        component = renderer.render(playerResult.player(), targetPlayer.player(), component, msg);\n                    } else {\n                        component = renderer.render(playerResult.player(), target, component, msg);\n                    }\n                }\n\n                if (component == Component.empty()) {\n                    return Optional.empty();\n                }\n\n                return Optional.ofNullable(component);\n            });\n        }\n    }\n\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/listeners/SpongePlayerJoinListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge.listeners;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.util.PlayerUtils;\nimport net.draycia.carbon.sponge.users.CarbonPlayerSponge;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.spongepowered.api.event.Listener;\nimport org.spongepowered.api.event.network.ServerSideConnectionEvent;\n\n@DefaultQualifier(NonNull.class)\npublic class SpongePlayerJoinListener {\n\n    private final CarbonChat carbonChat;\n    private final UserManager<CarbonPlayerCommon> userManager;\n\n    @Inject\n    public SpongePlayerJoinListener(\n        final CarbonChat carbonChat,\n        final UserManager<CarbonPlayerCommon> userManager\n    ) {\n        this.carbonChat = carbonChat;\n        this.userManager = userManager;\n    }\n\n    @Listener\n    public void onPlayerQuit(final ServerSideConnectionEvent.Disconnect event) {\n        this.carbonChat.server().userManager().carbonPlayer(event.player().uniqueId()).thenAccept(result -> {\n            if (result.player() == null) {\n                return;\n            }\n\n            PlayerUtils.saveAndInvalidatePlayer(this.carbonChat.server(), this.userManager, (CarbonPlayerSponge) result.player());\n        });\n    }\n\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/listeners/SpongeReloadListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge.listeners;\n\nimport com.google.inject.Inject;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.config.ConfigFactory;\nimport net.draycia.carbon.common.event.CarbonReloadEvent;\nimport org.spongepowered.api.event.Listener;\nimport org.spongepowered.api.event.lifecycle.RefreshGameEvent;\n\npublic class SpongeReloadListener {\n\n    final CarbonChat carbonChat;\n    final ConfigFactory configFactory;\n    final CarbonChannelRegistry channelRegistry;\n\n    @Inject\n    public SpongeReloadListener(\n        final CarbonChat carbonChat,\n        final ConfigFactory configFactory,\n        final CarbonChannelRegistry channelRegistry\n    ) {\n        this.carbonChat = carbonChat;\n        this.configFactory = configFactory;\n        this.channelRegistry = channelRegistry;\n    }\n\n    @Listener\n    public void onReload(final RefreshGameEvent event) {\n        this.carbonChat.eventHandler().emit(new CarbonReloadEvent());\n    }\n\n}\n"
  },
  {
    "path": "sponge/src/main/java/net/draycia/carbon/sponge/users/CarbonPlayerSponge.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2021 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.sponge.users;\n\nimport java.util.Locale;\nimport java.util.Optional;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.InventorySlot;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.users.WrappedCarbonPlayer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\nimport org.spongepowered.api.Sponge;\nimport org.spongepowered.api.data.Keys;\nimport org.spongepowered.api.entity.living.player.server.ServerPlayer;\nimport org.spongepowered.api.event.Cause;\nimport org.spongepowered.api.item.inventory.ItemStack;\nimport org.spongepowered.api.item.inventory.equipment.EquipmentType;\nimport org.spongepowered.api.item.inventory.equipment.EquipmentTypes;\nimport org.spongepowered.api.util.locale.LocaleSource;\n\nimport static net.kyori.adventure.text.Component.translatable;\nimport static net.kyori.adventure.text.format.TextDecoration.ITALIC;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonPlayerSponge extends WrappedCarbonPlayer implements ForwardingAudience.Single {\n\n    private final CarbonPlayerCommon carbonPlayerCommon;\n\n    public CarbonPlayerSponge(final CarbonPlayerCommon carbonPlayerCommon) {\n        this.carbonPlayerCommon = carbonPlayerCommon;\n    }\n\n    @Override\n    public @NotNull Audience audience() {\n        return this.player()\n            .map(player -> (Audience) player)\n            .orElseGet(Audience::empty);\n    }\n\n    @Override\n    public CarbonPlayerCommon carbonPlayerCommon() {\n        return this.carbonPlayerCommon;\n    }\n\n    private Optional<ServerPlayer> player() {\n        return Sponge.server().player(this.carbonPlayerCommon.uuid());\n    }\n\n    @Override\n    public void sendMessageAsPlayer(final String message) {\n        this.player().ifPresent(player -> player.simulateChat(Component.text(message), Cause.builder().build()));\n    }\n\n    @Override\n    public boolean online() {\n        return this.player().map(ServerPlayer::isOnline).orElse(false);\n    }\n\n    @Override\n    public @Nullable Locale locale() {\n        return this.player().map(LocaleSource::locale).orElse(null);\n    }\n\n    @Override\n    public double distanceSquaredFrom(final CarbonPlayer other) {\n        if (this.player().isEmpty()) {\n            return -1;\n        }\n\n        final @Nullable ServerPlayer player = this.player().orElse(null);\n        final @Nullable ServerPlayer otherPlayer = Sponge.server().player(other.uuid()).orElse(null);\n\n        if (player == null || otherPlayer == null) {\n            return -1;\n        }\n\n        final double deltaX = player.position().x() - otherPlayer.position().x();\n        final double deltaY = player.position().y() - otherPlayer.position().y();\n        final double deltaZ = player.position().z() - otherPlayer.position().z();\n\n        return (deltaX * deltaX) + (deltaY * deltaY) + (deltaZ * deltaZ);\n    }\n\n    @Override\n    public boolean sameWorldAs(final CarbonPlayer other) {\n        if (this.player().isEmpty()) {\n            return false;\n        }\n\n        final Optional<ServerPlayer> player = this.player();\n        final Optional<ServerPlayer> otherPlayer = Sponge.server().player(other.uuid());\n\n        if (player.isEmpty() || otherPlayer.isEmpty()) {\n            return false;\n        }\n\n        return player.get().world().equals(otherPlayer.get().world());\n    }\n\n    @Override\n    public void displayName(final @Nullable Component displayName) {\n        this.carbonPlayerCommon.displayName(displayName);\n    }\n\n    @Override\n    public @Nullable Component createItemHoverComponent(final InventorySlot slot) {\n        final Optional<ServerPlayer> optionalPlayer = this.player(); // This is temporary (it's not)\n\n        if (optionalPlayer.isEmpty()) {\n            return null;\n        }\n\n        final ServerPlayer player = optionalPlayer.get();\n        final EquipmentType equipmentSlot;\n\n        if (slot.equals(InventorySlot.MAIN_HAND)) {\n            equipmentSlot = EquipmentTypes.MAIN_HAND.get();\n        } else if (slot.equals(InventorySlot.OFF_HAND)) {\n            equipmentSlot = EquipmentTypes.OFF_HAND.get();\n        } else if (slot.equals(InventorySlot.HELMET)) {\n            equipmentSlot = EquipmentTypes.HEAD.get();\n        } else if (slot.equals(InventorySlot.CHEST)) {\n            equipmentSlot = EquipmentTypes.CHEST.get();\n        } else if (slot.equals(InventorySlot.LEGS)) {\n            equipmentSlot = EquipmentTypes.LEGS.get();\n        } else if (slot.equals(InventorySlot.BOOTS)) {\n            equipmentSlot = EquipmentTypes.FEET.get();\n        } else {\n            return null;\n        }\n\n        final Optional<ItemStack> equipment = player.equipment().peek(equipmentSlot);\n\n        if (equipment.isEmpty()) {\n            return null;\n        }\n\n        final @Nullable ItemStack itemStack = equipment.get();\n\n        return this.fromStack(itemStack);\n    }\n\n    private Component fromStack(final ItemStack stack) {\n        return stack.get(Keys.DISPLAY_NAME)\n\n            // This is here as a fallback, but really, every ItemStack should\n            // have a DISPLAY_NAME which is already formatted properly for us by the game.\n            .orElseGet(() -> translatable()\n                .key(\"chat.square_brackets\")\n                .args(stack.get(Keys.CUSTOM_NAME)\n                    .map(name -> name.decorate(ITALIC))\n                    .orElseGet(() -> stack.type().asComponent()))\n                .hoverEvent(stack.createSnapshot())\n                .apply(builder -> stack.get(Keys.ITEM_RARITY).ifPresent(rarity -> builder.color(rarity.color())))\n                .build());\n    }\n\n    @Override\n    public boolean vanished() {\n        return false;\n    }\n\n}\n"
  },
  {
    "path": "velocity/build.gradle.kts",
    "content": "import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar\n\nplugins {\n  id(\"carbon.shadow-platform\")\n  id(\"xyz.jpenilla.run-velocity\")\n  alias(libs.plugins.resource.factory.velocity.convention)\n}\n\nval bstats: Configuration by configurations.creating\nconfigurations.compileOnly {\n  extendsFrom(bstats)\n}\n\ndependencies {\n  implementation(projects.carbonchatCommon)\n  bstats(libs.bstatsVelocity)\n\n  compileOnly(libs.velocityApi)\n\n  implementation(libs.cloudVelocity)\n  compileOnly(libs.miniplaceholders)\n\n  runtimeDownload(libs.mysql)\n\n  compileOnly(\"javax.inject:javax.inject:1\")\n  implementation(libs.assistedInject)\n}\n\nvelocityPluginJson {\n  id = rootProject.name.lowercase()\n  main = \"net.draycia.carbon.velocity.CarbonVelocityBootstrap\"\n  name = rootProject.name\n  version = project.version.toString()\n  description = project.description\n  url = GITHUB_REPO_URL\n  authors = listOf(\"Draycia\", \"jmp\")\n  dependency(\"luckperms\", false)\n  dependency(\"miniplaceholders\", true)\n  dependency(\"signedvelocity\", true)\n}\n\ngremlin {\n  defaultJarRelocatorDependencies.set(true)\n}\n\nrunVelocityExtension.detectPluginJar = false\n\ntasks {\n  val bStatsJar = register<ShadowJar>(\"bStatsShadowJar\") {\n    archiveClassifier = \"bStats\"\n    configurations = listOf(bstats)\n    relocateDependency(\"org.bstats\")\n  }\n  shadowJar {\n    archiveClassifier = \"shadowJar\"\n    relocateCloud()\n    standardRuntimeRelocations()\n    relocateDependency(\"io.leangen.geantyref\")\n  }\n  val prod = register<Zip>(\"productionJar\") {\n    destinationDirectory.set(layout.buildDirectory.dir(\"libs\"))\n    archiveFileName.set(\"carbonchat-velocity-${project.version}.jar\")\n    from(zipTree(shadowJar.flatMap { it.archiveFile }))\n    from(zipTree(bStatsJar.flatMap { it.archiveFile })) {\n      exclude(\"META-INF/**\")\n    }\n  }\n  carbonPlatform.productionJar = prod.flatMap { it.archiveFile }\n  writeDependencies {\n    standardRuntimeRelocations()\n    relocateDependency(\"io.leangen.geantyref\")\n    relocateGuice()\n  }\n  val luckperms = FetchLuckPermsJar.setup(project, \"velocity\")\n  runVelocity {\n    velocityVersion(libs.versions.velocityApi.get())\n    pluginJars.from(prod)\n    pluginJars.from(luckperms.flatMap { it.outputFile })\n    downloadPlugins {\n      github(\"MiniPlaceholders\", \"MiniPlaceholders\", libs.versions.miniplaceholders.get(), \"MiniPlaceholders-Velocity-${libs.versions.miniplaceholders.get()}.jar\")\n    }\n  }\n}\n\npublishMods.modrinth {\n  modLoaders.addAll(\"velocity\")\n  optional {\n    slug = \"signedvelocity\"\n  }\n}\n\nconfigurations.runtimeDownload {\n  exclude(\"org.checkerframework\", \"checker-qual\")\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/CarbonChatVelocity.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.google.inject.Key;\nimport com.google.inject.Provider;\nimport com.google.inject.Singleton;\nimport com.google.inject.TypeLiteral;\nimport com.velocitypowered.api.plugin.PluginContainer;\nimport com.velocitypowered.api.proxy.ProxyServer;\nimport java.util.Set;\nimport java.util.concurrent.ScheduledExecutorService;\nimport net.draycia.carbon.api.CarbonChatProvider;\nimport net.draycia.carbon.api.event.CarbonEventHandler;\nimport net.draycia.carbon.common.CarbonChatInternal;\nimport net.draycia.carbon.common.PeriodicTasks;\nimport net.draycia.carbon.common.channels.CarbonChannelRegistry;\nimport net.draycia.carbon.common.command.ExecutionCoordinatorHolder;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.messaging.MessagingManager;\nimport net.draycia.carbon.common.users.PlatformUserManager;\nimport net.draycia.carbon.common.users.ProfileCache;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.velocity.listeners.VelocityListener;\nimport org.apache.logging.log4j.LogManager;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic class CarbonChatVelocity extends CarbonChatInternal {\n\n    private final ProxyServer proxyServer;\n\n    @Inject\n    public CarbonChatVelocity(\n        final ProxyServer proxyServer,\n        final Injector injector,\n        final PluginContainer pluginContainer,\n        @PeriodicTasks final ScheduledExecutorService periodicTasks,\n        final ProfileCache profileCache,\n        final ProfileResolver profileResolver,\n        final ExecutionCoordinatorHolder commandExecutor,\n        final CarbonMessages carbonMessages,\n        final PlatformUserManager userManager,\n        final CarbonServerVelocity carbonServer,\n        final CarbonEventHandler eventHandler,\n        final CarbonChannelRegistry channelRegistry,\n        final Provider<MessagingManager> messagingManager\n    ) {\n        super(\n            injector,\n            LogManager.getLogger(pluginContainer.getDescription().getId()),\n            periodicTasks,\n            profileCache,\n            profileResolver,\n            userManager,\n            commandExecutor,\n            carbonServer,\n            carbonMessages,\n            eventHandler,\n            channelRegistry,\n            messagingManager\n        );\n        this.proxyServer = proxyServer;\n\n        CarbonChatProvider.register(this);\n    }\n\n    public void onInitialization(final CarbonVelocityBootstrap carbonVelocityBootstrap) {\n        this.init();\n\n        final Set<VelocityListener<?>> listeners = this.injector().getInstance(Key.get(new TypeLiteral<Set<VelocityListener<?>>>() {}));\n        for (final VelocityListener<?> listener : listeners) {\n            listener.register(this.proxyServer.getEventManager(), carbonVelocityBootstrap);\n        }\n\n        this.checkVersion();\n    }\n\n    public void onShutdown() {\n        this.shutdown();\n    }\n\n    @Override\n    public boolean isProxy() {\n        return true;\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/CarbonChatVelocityModule.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity;\n\nimport com.google.inject.Provides;\nimport com.google.inject.Singleton;\nimport com.google.inject.TypeLiteral;\nimport com.google.inject.multibindings.Multibinder;\nimport com.velocitypowered.api.plugin.PluginContainer;\nimport com.velocitypowered.api.plugin.PluginManager;\nimport com.velocitypowered.api.proxy.Player;\nimport com.velocitypowered.api.proxy.ProxyServer;\nimport java.nio.file.Path;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.CarbonCommonModule;\nimport net.draycia.carbon.common.CarbonPlatformModule;\nimport net.draycia.carbon.common.DataDirectory;\nimport net.draycia.carbon.common.PlatformScheduler;\nimport net.draycia.carbon.common.RawChat;\nimport net.draycia.carbon.common.command.Commander;\nimport net.draycia.carbon.common.command.ExecutionCoordinatorHolder;\nimport net.draycia.carbon.common.messages.CarbonMessageRenderer;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.common.users.PlatformUserManager;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport net.draycia.carbon.common.util.CloudUtils;\nimport net.draycia.carbon.velocity.command.VelocityCommander;\nimport net.draycia.carbon.velocity.command.VelocityPlayerCommander;\nimport net.draycia.carbon.velocity.listeners.VelocityChatListener;\nimport net.draycia.carbon.velocity.listeners.VelocityListener;\nimport net.draycia.carbon.velocity.listeners.VelocityPlayerJoinListener;\nimport net.draycia.carbon.velocity.listeners.VelocityPlayerLeaveListener;\nimport net.draycia.carbon.velocity.users.CarbonPlayerVelocity;\nimport net.draycia.carbon.velocity.users.VelocityProfileResolver;\nimport net.kyori.adventure.key.Key;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.incendo.cloud.CommandManager;\nimport org.incendo.cloud.SenderMapper;\nimport org.incendo.cloud.velocity.VelocityCommandManager;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonChatVelocityModule extends CarbonPlatformModule {\n\n    private final Logger logger = LogManager.getLogger(\"carbonchat\");\n    private final CarbonVelocityBootstrap bootstrap;\n    private final PluginContainer pluginContainer;\n    private final ProxyServer proxyServer;\n    private final Path dataDirectory;\n\n    CarbonChatVelocityModule(\n        final CarbonVelocityBootstrap bootstrap,\n        final PluginContainer pluginContainer,\n        final ProxyServer proxyServer,\n        final Path dataDirectory\n    ) {\n        this.bootstrap = bootstrap;\n        this.pluginContainer = pluginContainer;\n        this.proxyServer = proxyServer;\n        this.dataDirectory = dataDirectory;\n    }\n\n    @Provides\n    @Singleton\n    public CommandManager<Commander> createCommandManager(\n        final ExecutionCoordinatorHolder executionCoordinatorHolder,\n        final UserManager<?> userManager,\n        final CarbonMessages messages\n    ) {\n        final VelocityCommandManager<Commander> commandManager = new VelocityCommandManager<>(\n            this.pluginContainer,\n            this.proxyServer,\n            executionCoordinatorHolder.executionCoordinator(),\n            SenderMapper.create(\n                commandSender -> {\n                    if (commandSender instanceof Player player) {\n                        return new VelocityPlayerCommander(userManager, player);\n                    }\n\n                    return VelocityCommander.from(commandSender);\n                },\n                commander -> ((VelocityCommander) commander).commandSource()\n            )\n        );\n\n        CloudUtils.decorateCommandManager(commandManager, messages, this.logger);\n\n        return commandManager;\n    }\n\n    @Override\n    protected void configurePlatform() {\n        this.install(new CarbonCommonModule());\n\n        this.bind(CarbonVelocityBootstrap.class).toInstance(this.bootstrap);\n        this.bind(PluginContainer.class).toInstance(this.pluginContainer);\n        this.bind(ProxyServer.class).toInstance(this.proxyServer);\n        this.bind(PluginManager.class).toInstance(this.proxyServer.getPluginManager());\n\n        this.bind(CarbonChat.class).to(CarbonChatVelocity.class);\n        this.bind(CarbonServer.class).to(CarbonServerVelocity.class);\n        this.bind(ProfileResolver.class).to(VelocityProfileResolver.class);\n        this.bind(Path.class).annotatedWith(DataDirectory.class).toInstance(this.dataDirectory);\n        this.bind(Logger.class).toInstance(this.logger);\n        this.bind(PlatformScheduler.class).to(PlatformScheduler.RunImmediately.class);\n        this.install(PlatformUserManager.PlayerFactory.moduleFor(CarbonPlayerVelocity.class));\n        this.bind(CarbonMessageRenderer.class).to(VelocityMessageRenderer.class);\n        this.bind(Key.class).annotatedWith(RawChat.class).toInstance(Key.key(\"unused:unused\"));\n\n        this.configureListeners();\n    }\n\n    private void configureListeners() {\n        final Multibinder<VelocityListener<?>> listeners = Multibinder.newSetBinder(this.binder(), new TypeLiteral<VelocityListener<?>>() {});\n        listeners.addBinding().to(VelocityChatListener.class);\n        listeners.addBinding().to(VelocityPlayerJoinListener.class);\n        listeners.addBinding().to(VelocityPlayerLeaveListener.class);\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/CarbonServerVelocity.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity;\n\nimport com.google.inject.Inject;\nimport com.velocitypowered.api.proxy.ProxyServer;\nimport java.util.List;\nimport java.util.Objects;\nimport net.draycia.carbon.api.CarbonServer;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonServerVelocity implements CarbonServer, ForwardingAudience.Single {\n\n    private final ProxyServer server;\n    private final UserManager<?> userManager;\n\n    @Inject\n    private CarbonServerVelocity(final ProxyServer server, final UserManagerInternal<?> userManager) {\n        this.server = server;\n        this.userManager = userManager;\n    }\n\n    @Override\n    public @NotNull Audience audience() {\n        return this.server;\n    }\n\n    @Override\n    public Audience console() {\n        return new ConsoleCarbonPlayer(this.server.getConsoleCommandSource());\n    }\n\n    @Override\n    public List<? extends CarbonPlayer> players() {\n        return this.server.getAllPlayers().stream()\n            .map(player -> this.userManager.user(player.getUniqueId()).getNow(null))\n            .filter(Objects::nonNull)\n            .toList();\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/CarbonVelocityBootstrap.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity;\n\nimport com.google.inject.Guice;\nimport com.google.inject.Inject;\nimport com.google.inject.Injector;\nimport com.velocitypowered.api.event.Subscribe;\nimport com.velocitypowered.api.event.proxy.ProxyInitializeEvent;\nimport com.velocitypowered.api.event.proxy.ProxyShutdownEvent;\nimport com.velocitypowered.api.plugin.PluginContainer;\nimport com.velocitypowered.api.plugin.annotation.DataDirectory;\nimport com.velocitypowered.api.proxy.ProxyServer;\nimport java.nio.file.Path;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.config.MessagingSettings;\nimport net.draycia.carbon.common.util.CarbonDependencies;\nimport org.bstats.charts.SimplePie;\nimport org.bstats.velocity.Metrics;\nimport org.checkerframework.checker.nullness.qual.MonotonicNonNull;\nimport xyz.jpenilla.gremlin.runtime.platformsupport.VelocityClasspathAppender;\n\npublic final class CarbonVelocityBootstrap {\n\n    private static final int BSTATS_PLUGIN_ID = 19505;\n\n    private final PluginContainer pluginContainer;\n    private final ProxyServer proxy;\n    private final Path dataDirectory;\n    private final Metrics.Factory metricsFactory;\n    private final Inner inner;\n\n    @Inject\n    public CarbonVelocityBootstrap(\n        final ProxyServer proxyServer,\n        final PluginContainer pluginContainer,\n        @DataDirectory final Path dataDirectory,\n        final Metrics.Factory metricsFactory\n    ) {\n        this.proxy = proxyServer;\n        this.pluginContainer = pluginContainer;\n        this.dataDirectory = dataDirectory;\n        this.metricsFactory = metricsFactory;\n\n        this.inner = new Inner();\n    }\n\n    @Subscribe\n    public void onProxyInitialize(final ProxyInitializeEvent event) {\n        this.inner.onProxyInitialize(event);\n    }\n\n    @Subscribe\n    public void onProxyShutdown(final ProxyShutdownEvent event) {\n        this.inner.onProxyShutdown(event);\n    }\n\n    // Inner class to avoid classloading issues with guice\n    private final class Inner {\n        private @MonotonicNonNull Injector injector;\n\n        void onProxyInitialize(final ProxyInitializeEvent event) {\n            new VelocityClasspathAppender(CarbonVelocityBootstrap.this.proxy, CarbonVelocityBootstrap.this).append(\n                CarbonDependencies.resolve(CarbonVelocityBootstrap.this.dataDirectory.resolve(\"libraries\"))\n            );\n\n            this.injector = Guice.createInjector(\n                new CarbonChatVelocityModule(\n                    CarbonVelocityBootstrap.this,\n                    CarbonVelocityBootstrap.this.pluginContainer,\n                    CarbonVelocityBootstrap.this.proxy,\n                    CarbonVelocityBootstrap.this.dataDirectory\n                )\n            );\n            final Injector injector = this.injector;\n            injector.getInstance(CarbonChatVelocity.class).onInitialization(CarbonVelocityBootstrap.this);\n\n            final Metrics metrics = CarbonVelocityBootstrap.this.metricsFactory.make(CarbonVelocityBootstrap.this, BSTATS_PLUGIN_ID);\n            metrics.addCustomChart(new SimplePie(\"user_manager_type\", () -> injector.getInstance(ConfigManager.class).primaryConfig().storageType().name()));\n            metrics.addCustomChart(new SimplePie(\"messaging\", () -> {\n                final MessagingSettings settings = injector.getInstance(ConfigManager.class).primaryConfig().messagingSettings();\n                if (!settings.enabled()) {\n                    return \"disabled\";\n                }\n                return settings.brokerType().name();\n            }));\n        }\n\n        void onProxyShutdown(final ProxyShutdownEvent event) {\n            this.injector.getInstance(CarbonChatVelocity.class).onShutdown();\n        }\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/VelocityMessageRenderer.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport com.velocitypowered.api.plugin.PluginManager;\nimport io.github.miniplaceholders.api.MiniPlaceholders;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersIntegration;\nimport net.draycia.carbon.common.integration.miniplaceholders.MiniPlaceholdersUtil;\nimport net.draycia.carbon.common.messages.CarbonMessageRenderer;\nimport net.draycia.carbon.common.messages.RenderForTagResolver;\nimport net.draycia.carbon.common.messages.SourcedAudience;\nimport net.draycia.carbon.common.users.ConsoleCarbonPlayer;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport net.kyori.adventure.text.minimessage.MiniMessage;\nimport net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\n@Singleton\npublic class VelocityMessageRenderer extends CarbonMessageRenderer {\n\n    private final ConfigManager configManager;\n    private final PluginManager pluginManager;\n\n    @Inject\n    public VelocityMessageRenderer(final ConfigManager configManager, final PluginManager pluginManager, final RenderForTagResolver.Factory renderForTagResolver) {\n        super(renderForTagResolver);\n        this.configManager = configManager;\n        this.pluginManager = pluginManager;\n    }\n\n    @Override\n    public Component render(\n        final Audience receiver,\n        final String intermediateMessage,\n        final TagResolver.Builder tagResolver\n    ) {\n        final String placeholderResolvedMessage = this.configManager.primaryConfig().applyCustomPlaceholders(intermediateMessage);\n\n        final MiniPlaceholdersIntegration.@Nullable Config miniplaceholdersConfig = MiniPlaceholdersUtil.miniPlaceholdersLoaded()\n            ? this.configManager.primaryConfig().integrations().config(MiniPlaceholdersIntegration.configMeta())\n            : null;\n\n        if (miniplaceholdersConfig != null) {\n            tagResolver.resolver(MiniPlaceholders.globalPlaceholders());\n\n            if (receiver instanceof SourcedAudience) {\n                tagResolver.resolver(MiniPlaceholders.audiencePlaceholders());\n                if (miniplaceholdersConfig.relationalPlaceholders) {\n                    tagResolver.resolver(MiniPlaceholders.relationalPlaceholders());\n                }\n            }\n        }\n        final Audience parseAudience;\n\n        if (receiver instanceof SourcedAudience sourced) {\n            if (sourced.recipient() instanceof ConsoleCarbonPlayer) {\n                parseAudience = MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, sourced.sender(), sourced.sender());\n            } else {\n                parseAudience = MiniPlaceholdersUtil.wrapAudiences(miniplaceholdersConfig, sourced.recipient(), sourced.sender());\n            }\n        } else {\n            parseAudience = receiver;\n        }\n\n        return MiniMessage.miniMessage().deserialize(placeholderResolvedMessage, parseAudience, tagResolver.build());\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/command/VelocityCommander.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity.command;\n\nimport com.velocitypowered.api.command.CommandSource;\nimport net.draycia.carbon.common.command.Commander;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\n\n@DefaultQualifier(NonNull.class)\npublic interface VelocityCommander extends Commander, ForwardingAudience.Single {\n\n    static VelocityCommander from(final CommandSource source) {\n        return new VelocityCommanderImpl(source);\n    }\n\n    CommandSource commandSource();\n\n    record VelocityCommanderImpl(CommandSource commandSource) implements VelocityCommander {\n\n        @Override\n        public @NotNull Audience audience() {\n            return this.commandSource;\n        }\n\n        @Override\n        public boolean hasPermission(final String permission) {\n            return this.commandSource.hasPermission(permission);\n        }\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/command/VelocityPlayerCommander.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity.command;\n\nimport com.velocitypowered.api.command.CommandSource;\nimport com.velocitypowered.api.proxy.Player;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.command.PlayerCommander;\nimport net.kyori.adventure.audience.Audience;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\n\nimport static java.util.Objects.requireNonNull;\n\n@DefaultQualifier(NonNull.class)\npublic record VelocityPlayerCommander(\n    UserManager<?> userManager,\n    Player player\n) implements PlayerCommander, VelocityCommander {\n\n    @Override\n    public CommandSource commandSource() {\n        return this.player;\n    }\n\n    @Override\n    public @NotNull Audience audience() {\n        return this.player;\n    }\n\n    @Override\n    public CarbonPlayer carbonPlayer() {\n        return requireNonNull(this.userManager.user(this.player.getUniqueId()).join(), \"No CarbonPlayer for logged in Player!\");\n    }\n\n    @Override\n    public boolean hasPermission(final String permission) {\n        return this.player.hasPermission(permission);\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/listeners/VelocityChatListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity.listeners;\n\nimport com.google.common.base.Suppliers;\nimport com.google.inject.Inject;\nimport com.velocitypowered.api.event.EventManager;\nimport com.velocitypowered.api.event.EventTask;\nimport com.velocitypowered.api.event.PostOrder;\nimport com.velocitypowered.api.event.player.PlayerChatEvent;\nimport com.velocitypowered.api.network.ProtocolVersion;\nimport com.velocitypowered.api.plugin.PluginManager;\nimport com.velocitypowered.api.proxy.Player;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.function.Supplier;\nimport net.draycia.carbon.api.CarbonChat;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.users.UserManager;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.event.events.CarbonChatEventImpl;\nimport net.draycia.carbon.common.event.events.CarbonEarlyChatEvent;\nimport net.draycia.carbon.common.listeners.ChatListenerInternal;\nimport net.draycia.carbon.common.messages.CarbonMessages;\nimport net.draycia.carbon.velocity.CarbonVelocityBootstrap;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.text.Component;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@DefaultQualifier(NonNull.class)\npublic final class VelocityChatListener extends ChatListenerInternal implements VelocityListener<PlayerChatEvent> {\n\n    private final UserManager<?> userManager;\n    private final Logger logger;\n    private final AtomicInteger timesWarned = new AtomicInteger(0);\n    private final Supplier<Boolean> signedSupplier;\n    final ConfigManager configManager;\n\n    @Inject\n    private VelocityChatListener(\n        final CarbonChat carbonChat,\n        final UserManager<?> userManager,\n        final Logger logger,\n        final PluginManager pluginManager,\n        final CarbonMessages carbonMessages,\n        final ConfigManager configManager\n    ) {\n        super(carbonChat.eventHandler(), carbonMessages, configManager);\n        this.userManager = userManager;\n        this.logger = logger;\n        this.configManager = configManager;\n        this.signedSupplier = Suppliers.memoize(\n            () -> pluginManager.isLoaded(\"unsignedvelocity\")\n                || pluginManager.isLoaded(\"signedvelocity\")\n        );\n    }\n\n    @Override\n    public void register(final EventManager eventManager, final CarbonVelocityBootstrap bootstrap) {\n        eventManager.register(bootstrap, PlayerChatEvent.class, PostOrder.LATE, this);\n    }\n\n    @Override\n    public EventTask executeAsync(final PlayerChatEvent event) {\n        return EventTask.async(() -> this.executeEvent(event));\n    }\n\n    private void executeEvent(final PlayerChatEvent event) {\n        if (!event.getResult().isAllowed()) {\n            return;\n        }\n\n        final Player player = event.getPlayer();\n        final boolean signedVersion = player.getIdentifiedKey() != null\n            && player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0;\n        if (signedVersion && !this.signedSupplier.get()) {\n            if (this.timesWarned.getAndIncrement() < 3) {\n                this.logger.warn(\"\"\"\n                    \n                    ==================================================\n                    We have avoided modifying {}'s chat ,\n                    since they use a version higher than 1.19.1,\n                    where this function is not supported.\n                    \n                    If you want to keep this function working,\n                    install SignedVelocity.\n                    ==================================================\n                    \"\"\", player.getUsername()\n                );\n            }\n            return;\n        }\n\n        event.setResult(PlayerChatEvent.ChatResult.denied());\n\n        final CarbonPlayer sender = this.userManager.user(event.getPlayer().getUniqueId()).join();\n        final @Nullable CarbonEarlyChatEvent earlyChatEvent = this.prepareAndEmitPreChatEvent(sender, Component.text(event.getMessage()));\n\n        if (earlyChatEvent == null || earlyChatEvent.cancelled()) {\n            return;\n        }\n\n        final @Nullable Component message = this.parseTags(sender, earlyChatEvent.message());\n        if (message == null) {\n            return;\n        }\n\n        final @Nullable CarbonChatEventImpl chatEvent = this.prepareAndEmitChatEvent(sender, message, null);\n\n        if (chatEvent == null || chatEvent.cancelled()) {\n            return;\n        }\n\n        for (final Audience recipient : chatEvent.recipients()) {\n            recipient.sendMessage(chatEvent.renderFor(recipient));\n        }\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/listeners/VelocityListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity.listeners;\n\nimport com.velocitypowered.api.event.AwaitingEventExecutor;\nimport com.velocitypowered.api.event.EventManager;\nimport net.draycia.carbon.velocity.CarbonVelocityBootstrap;\n\npublic interface VelocityListener<E> extends AwaitingEventExecutor<E> {\n\n    void register(EventManager eventManager, CarbonVelocityBootstrap bootstrap);\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/listeners/VelocityPlayerJoinListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity.listeners;\n\nimport com.google.inject.Inject;\nimport com.velocitypowered.api.event.EventManager;\nimport com.velocitypowered.api.event.EventTask;\nimport com.velocitypowered.api.event.connection.LoginEvent;\nimport java.util.List;\nimport net.draycia.carbon.common.config.ConfigManager;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport net.draycia.carbon.velocity.CarbonVelocityBootstrap;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static net.draycia.carbon.common.users.PlayerUtils.joinExceptionHandler;\n\n@DefaultQualifier(NonNull.class)\npublic class VelocityPlayerJoinListener implements VelocityListener<LoginEvent> {\n\n    private final ConfigManager configManager;\n    private final UserManagerInternal<?> userManager;\n    private final Logger logger;\n\n    @Inject\n    public VelocityPlayerJoinListener(\n        final ConfigManager configManager,\n        final UserManagerInternal<?> userManager,\n        final Logger logger\n    ) {\n        this.configManager = configManager;\n        this.userManager = userManager;\n        this.logger = logger;\n    }\n\n    @Override\n    public void register(final EventManager eventManager, final CarbonVelocityBootstrap bootstrap) {\n        eventManager.register(bootstrap, LoginEvent.class, this);\n    }\n\n    @Override\n    public EventTask executeAsync(final LoginEvent event) {\n        return EventTask.async(\n            () -> {\n                this.userManager.user(event.getPlayer().getUniqueId()).exceptionally(joinExceptionHandler(this.logger, event.getPlayer().getUsername(), event.getPlayer().getUniqueId()));\n\n                final @Nullable List<String> suggestions = this.configManager.primaryConfig().customChatSuggestions();\n\n                if (suggestions == null || suggestions.isEmpty()) {\n                    return;\n                }\n\n                event.getPlayer().addCustomChatCompletions(suggestions);\n            }\n        );\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/listeners/VelocityPlayerLeaveListener.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity.listeners;\n\nimport com.google.inject.Inject;\nimport com.velocitypowered.api.event.EventManager;\nimport com.velocitypowered.api.event.EventTask;\nimport com.velocitypowered.api.event.connection.DisconnectEvent;\nimport net.draycia.carbon.common.users.UserManagerInternal;\nimport net.draycia.carbon.velocity.CarbonVelocityBootstrap;\nimport org.apache.logging.log4j.Logger;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\nimport static net.draycia.carbon.common.users.PlayerUtils.saveExceptionHandler;\n\n@DefaultQualifier(NonNull.class)\npublic final class VelocityPlayerLeaveListener implements VelocityListener<DisconnectEvent> {\n\n    private final UserManagerInternal<?> userManager;\n    private final Logger logger;\n\n    @Inject\n    public VelocityPlayerLeaveListener(\n        final UserManagerInternal<?> userManager,\n        final Logger logger\n    ) {\n        this.userManager = userManager;\n        this.logger = logger;\n    }\n\n    @Override\n    public void register(final EventManager eventManager, final CarbonVelocityBootstrap bootstrap) {\n        eventManager.register(bootstrap, DisconnectEvent.class, this);\n    }\n\n    @Override\n    public EventTask executeAsync(final DisconnectEvent event) {\n        return EventTask.async(() -> {\n            if (event.getLoginStatus() == DisconnectEvent.LoginStatus.CONFLICTING_LOGIN) {\n                return;\n            }\n            this.userManager.loggedOut(event.getPlayer().getUniqueId())\n                .exceptionally(saveExceptionHandler(this.logger, event.getPlayer().getUsername(), event.getPlayer().getUniqueId()));\n        });\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/users/CarbonPlayerVelocity.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity.users;\n\nimport com.google.inject.assistedinject.Assisted;\nimport com.google.inject.assistedinject.AssistedInject;\nimport com.velocitypowered.api.proxy.Player;\nimport com.velocitypowered.api.proxy.ProxyServer;\nimport java.util.Locale;\nimport java.util.Optional;\nimport net.draycia.carbon.api.users.CarbonPlayer;\nimport net.draycia.carbon.api.util.InventorySlot;\nimport net.draycia.carbon.common.users.CarbonPlayerCommon;\nimport net.draycia.carbon.common.users.WrappedCarbonPlayer;\nimport net.draycia.carbon.common.util.EmptyAudienceWithPointers;\nimport net.kyori.adventure.audience.Audience;\nimport net.kyori.adventure.audience.ForwardingAudience;\nimport net.kyori.adventure.identity.Identity;\nimport net.kyori.adventure.text.Component;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\nimport org.jetbrains.annotations.NotNull;\n\n@DefaultQualifier(NonNull.class)\npublic final class CarbonPlayerVelocity extends WrappedCarbonPlayer implements ForwardingAudience.Single {\n\n    private final ProxyServer server;\n\n    @AssistedInject\n    private CarbonPlayerVelocity(final ProxyServer server, @Assisted final CarbonPlayerCommon carbonPlayerCommon) {\n        super(carbonPlayerCommon);\n        this.server = server;\n    }\n\n    @Override\n    public @NotNull Audience audience() {\n        return this.player().map(value -> (Audience) value).orElseGet(() -> EmptyAudienceWithPointers.forCarbonPlayer(this));\n    }\n\n    @Override\n    public boolean vanished() {\n        //TODO: VelocityVanish compatibility\n        return false;\n    }\n\n    public Optional<Player> player() {\n        return this.server.getPlayer(this.uuid());\n    }\n\n    @Override\n    public @Nullable Locale locale() {\n        return this.player().map(value -> value.getPlayerSettings().getLocale()).orElse(null);\n    }\n\n    @Override\n    public double distanceSquaredFrom(final CarbonPlayer other) {\n        return -1;\n    }\n\n    @Override\n    public boolean sameWorldAs(final CarbonPlayer other) {\n        final Optional<Player> player = this.player();\n        final Optional<Player> otherPlayer = this.server.getPlayer(other.uuid());\n\n        if (player.isEmpty() || otherPlayer.isEmpty()) {\n            return false;\n        }\n\n        final var playerServer = player.get().getCurrentServer().get();\n        final var otherServer = otherPlayer.get().getCurrentServer().get();\n\n        return playerServer.getServer().equals(otherServer.getServer());\n    }\n\n    @Override\n    protected Optional<Component> platformDisplayName() {\n        return this.player().flatMap(p -> p.get(Identity.DISPLAY_NAME));\n    }\n\n    @Override\n    public @Nullable Component createItemHoverComponent(final InventorySlot slot) {\n        return null;\n    }\n\n    @Override\n    public boolean online() {\n        final var player = this.player();\n        return player.isPresent() && player.get().isActive();\n    }\n\n}\n"
  },
  {
    "path": "velocity/src/main/java/net/draycia/carbon/velocity/users/VelocityProfileResolver.java",
    "content": "/*\n * CarbonChat\n *\n * Copyright (c) 2024 Josua Parks (Vicarious)\n *                    Contributors\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program.  If not, see <https://www.gnu.org/licenses/>.\n */\npackage net.draycia.carbon.velocity.users;\n\nimport com.google.inject.Inject;\nimport com.google.inject.Singleton;\nimport com.velocitypowered.api.proxy.ProxyServer;\nimport java.util.UUID;\nimport java.util.concurrent.CompletableFuture;\nimport net.draycia.carbon.common.users.MojangProfileResolver;\nimport net.draycia.carbon.common.users.ProfileResolver;\nimport org.checkerframework.checker.nullness.qual.NonNull;\nimport org.checkerframework.checker.nullness.qual.Nullable;\nimport org.checkerframework.framework.qual.DefaultQualifier;\n\n@Singleton\n@DefaultQualifier(NonNull.class)\npublic class VelocityProfileResolver implements ProfileResolver {\n\n    private final ProxyServer proxyServer;\n    private final MojangProfileResolver mojang;\n\n    @Inject\n    public VelocityProfileResolver(final ProxyServer proxyServer, final MojangProfileResolver mojang) {\n        this.proxyServer = proxyServer;\n        this.mojang = mojang;\n    }\n\n    @Override\n    public CompletableFuture<@Nullable UUID> resolveUUID(final String username, final boolean cacheOnly) {\n        final var serverPlayer = this.proxyServer.getPlayer(username);\n\n        return serverPlayer.map(player -> CompletableFuture.completedFuture(player.getUniqueId()))\n            .orElseGet(() -> this.mojang.resolveUUID(username));\n    }\n\n    @Override\n    public CompletableFuture<@Nullable String> resolveName(final UUID uuid, final boolean cacheOnly) {\n        final var serverPlayer = this.proxyServer.getPlayer(uuid);\n\n        return serverPlayer.map(player -> CompletableFuture.completedFuture(player.getUsername()))\n            .orElseGet(() -> this.mojang.resolveName(uuid));\n    }\n\n    @Override\n    public void shutdown() {\n        this.mojang.shutdown();\n    }\n\n}\n"
  }
]