├── .github ├── ISSUE_TEMPLATE │ └── bug_report.yml └── workflows │ ├── build.yml │ ├── gradle.yml │ ├── matrix_prep.yml │ ├── pre-release.yml │ ├── release.yml │ └── scripts │ ├── matrix.py │ └── summary.py ├── .gitignore ├── LICENSE ├── README.MD ├── README_EN.MD ├── build.gradle ├── common.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── indexImg.png ├── settings.gradle ├── settings.json ├── src └── main │ ├── java │ └── io │ │ └── github │ │ └── skydynamic │ │ └── quickbackupmulti │ │ ├── DataBaseManager.java │ │ ├── JavaUtilLog4jFilter.java │ │ ├── QbmConstant.java │ │ ├── QuickBackupMulti.java │ │ ├── QuickBackupMultiClient.java │ │ ├── api │ │ └── ServerPathGetter.java │ │ ├── backup │ │ ├── ClientRestoreDelegate.java │ │ └── RestoreTask.java │ │ ├── command │ │ ├── MakeCommand.java │ │ ├── QuickBackupMultiCommand.java │ │ └── SettingCommand.java │ │ ├── config │ │ ├── Config.java │ │ ├── ConfigStorage.java │ │ ├── Ignore.java │ │ ├── QbmTempConfig.java │ │ └── QuickBackupMultiConfig.java │ │ ├── i18n │ │ ├── LangSuggestionProvider.java │ │ └── Translate.java │ │ ├── mixin │ │ ├── MinecraftClientMixin.java │ │ ├── MinecraftServer_ClientMixin.java │ │ ├── MinecraftServer_ServerMixin.java │ │ └── TitleScreenMixin.java │ │ ├── screen │ │ ├── ConfigScreen.java │ │ ├── ScheduleConfigScreen.java │ │ ├── ScreenUtils.java │ │ └── TempConfig.java │ │ └── utils │ │ ├── ListUtils.java │ │ ├── MakeUtils.java │ │ ├── Messenger.java │ │ ├── QbmManager.java │ │ ├── ScheduleUtils.java │ │ ├── ServerPathUtils.java │ │ └── schedule │ │ ├── CronUtil.java │ │ ├── ScheduleBackup.java │ │ └── ScheduleUtils.java │ └── resources │ ├── assets │ └── quickbackupmulti │ │ ├── icon.png │ │ └── lang │ │ ├── en_us.yml │ │ └── zh_cn.yml │ ├── fabric.mod.json │ └── quickbackupmulti.mixins.json ├── updateindex.json └── versions ├── 1.18.2 ├── gradle.properties └── qbm.accesswidener ├── 1.19.4 ├── gradle.properties └── qbm.accesswidener ├── 1.20.3 ├── gradle.properties └── qbm.accesswidener ├── 1.20.5 ├── gradle.properties ├── qbm.accesswidener └── src │ └── main │ └── java │ └── io │ └── github │ └── skydynamic │ └── quickbackupmulti │ └── Packets.java ├── 1.20 ├── gradle.properties └── qbm.accesswidener ├── 1.21 ├── gradle.properties ├── qbm.accesswidener └── src │ └── main │ └── java │ └── io │ └── github │ └── skydynamic │ └── quickbackupmulti │ └── Packets.java ├── mainProject ├── mapping-1.18-1.19.txt └── mapping-empty.txt /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Something doesn't seem correct and it might be a bug 3 | labels: ["bug"] 4 | body: 5 | - type: textarea 6 | id: description 7 | attributes: 8 | label: Bug description / Bug描述 9 | description: | 10 | A clear and concise description of what the bug is. 11 | Is it a game crash, an unexpected behavior, or has something gone wrong? 12 | If applicable, add screenshots to help explain the bug. 13 | ------------------------------------------------------------------------- 14 | 用简洁的语言描述这是一个怎么样的bug! 15 | 是游戏崩溃、发生意料之外的情况,或者出了别的问题? 16 | 如果可以请添加截图或者 log 17 | placeholder: Tell us what you see! / 告诉我们你看到了什么问题 18 | validations: 19 | required: true 20 | 21 | - type: textarea 22 | id: to-reproduce 23 | attributes: 24 | label: Steps to reproduce / 复现步骤 25 | description: | 26 | Steps to reproduce the bug. 27 | --------------------------- 28 | 如何复现这个bug 29 | placeholder: | 30 | (Example) 31 | 1. Create a world 32 | 2. Wait until midnight 33 | 3. Hug a creeper 34 | ----------------------- 35 | (例子) 36 | 1. 创建一个世界 37 | 2. 等到午夜 38 | 2. 拥抱一只苦力怕 39 | validations: 40 | required: true 41 | 42 | - type: textarea 43 | id: expected-behavior 44 | attributes: 45 | label: Expected behavior / 预期行为 46 | description: | 47 | What did you expect to happen? 48 | ------------------------------ 49 | 你想要发生什么? 50 | placeholder: | 51 | (Example / 例子) 52 | The creeper explodes 53 | 苦力怕发生爆炸 54 | - type: textarea 55 | id: actual-behavior 56 | attributes: 57 | label: Actual behavior / 实际情况 58 | description: | 59 | What actually happened? 60 | ------------------------- 61 | 实际发生了什么? 62 | placeholder: | 63 | (Example / 例子) 64 | The creeper launches itself into the sky 65 | 苦力怕飞上了天空 66 | 67 | - type: textarea 68 | id: logs 69 | attributes: 70 | label: Relevant logs / 相关日志 71 | description: |- 72 | If it's a crash, send the corresponding Minecraft log in the `logs` folder, or crash report in the `crash-reports` folder, here. 73 | Please upload the log file as an attachment, or upload the log to [pastebin](https://pastebin.com/) / [mclo.gs](https://mclo.gs/) and paste the url here. 74 | Please refrain from pasting the entire log file directly. 75 | Leave empty if there is none. 76 | -------------------------------------------------- 77 | 如果发生了崩溃, 在这里放入位于`logs`文件夹的Minecraft的日志文件, 或位于`crash-reports`文件夹的崩溃报告 78 | 请作为附件上传日志文件, 或者上传日志文件到 [pastebin](https://pastebin.com/) / [mclo.gs](https://mclo.gs/) 然后复制链接到此处. 79 | **请作为附件上传而不是粘贴整个日志文件到此处** 80 | 如果没有请留空 81 | placeholder: https://pastebin.com/ 82 | 83 | - type: input 84 | id: minecraft-version 85 | attributes: 86 | label: Minecraft version / Minecraft版本 87 | description: | 88 | The Minecraft version(s) where this bug occurs in. 89 | 你在哪个Minecraft触发了这个bug 90 | placeholder: 1.20.1 91 | validations: 92 | required: true 93 | 94 | - type: input 95 | id: mod-version 96 | attributes: 97 | label: Mod version / Mod版本 98 | description: | 99 | The Mod version(s) where this bug occurs in. 100 | 你使用哪个版本的mod触发了bug 101 | placeholder: 1.1.3 102 | validations: 103 | required: true 104 | 105 | - type: textarea 106 | id: other-information 107 | attributes: 108 | label: Other information / 其他信息 109 | description: | 110 | Other useful information to this bug report, e.g. other related mod version(s). Leave empty if there is none. 111 | 如果有其他有用信息补充, 你可以写在这. 例如: 是否安装了其他mod / 修改了文件. 如果没有请留空 112 | placeholder: The issue only occurs if the player is in survival mode 113 | 114 | - type: checkboxes 115 | id: check-list 116 | attributes: 117 | label: Check list 118 | options: 119 | - label: I have verified that the issue persists in the latest version of the mod. / 该mod版本的问题在最新版本的mod中依旧存在 120 | required: true 121 | - label: I have searched the existing issues and confirmed that this is not a duplicate. / 我查看了现有issues并确保该问题不是重复的 122 | required: true -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | workflow_dispatch: 4 | workflow_call: 5 | inputs: 6 | release: 7 | type: boolean 8 | required: false 9 | description: '' 10 | default: false 11 | pre_version: 12 | type: string 13 | required: false 14 | description: '' 15 | default: '' 16 | target_subproject: 17 | description: The subproject name of the specified Minecraft version to be built. Leave it empty to build all 18 | type: string 19 | required: false 20 | default: '' 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | steps: 26 | 27 | - name: Check out 28 | uses: actions/checkout@v4 29 | 30 | - name: Set up JDK 21 31 | uses: actions/setup-java@v4 32 | with: 33 | java-version: '21' 34 | distribution: 'temurin' 35 | 36 | - name: Cache gradle files 37 | uses: actions/cache@v4 38 | with: 39 | path: | 40 | ~/.gradle/caches 41 | ~/.gradle/wrapper 42 | ./.gradle/loom-cache 43 | key: ${{ runner.os }}-gradle-${{ hashFiles('*.gradle', 'gradle.properties', '**/*.accesswidener') }} 44 | restore-keys: | 45 | ${{ runner.os }}-gradle- 46 | 47 | - name: Grant execute permission 48 | run: chmod +x gradlew 49 | 50 | - name: Build with Gradle 51 | run: ./gradlew remapJar --no-daemon 52 | env: 53 | BUILD_ID: ${{ github.run_number }} 54 | BUILD_RELEASE: ${{ inputs.release }} 55 | PRE_VERSION: ${{ inputs.pre_version }} 56 | 57 | - name: Upload JAR file 58 | uses: actions/upload-artifact@v3 59 | with: 60 | name: build-artifacts 61 | path: versions/*/build/libs/ 62 | 63 | summary: 64 | runs-on: ubuntu-22.04 65 | needs: 66 | - build 67 | 68 | steps: 69 | - uses: actions/checkout@v3 70 | 71 | - name: Download build artifacts 72 | uses: actions/download-artifact@v3 73 | with: 74 | name: build-artifacts 75 | path: build-artifacts 76 | 77 | - name: Make build summary 78 | run: python3 .github/workflows/scripts/summary.py 79 | env: 80 | TARGET_SUBPROJECT: ${{ inputs.target_subproject }} -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Dev Builds 2 | 3 | on: 4 | push: 5 | paths: 6 | - "*.gradle" 7 | - "gradle.properties" 8 | - "src/**" 9 | - "versions/**" 10 | - ".github/**" 11 | branches: 12 | - "main" 13 | - "dev" 14 | 15 | pull_request: 16 | paths: 17 | - "*.gradle" 18 | - "gradle.properties" 19 | - "src/**" 20 | - "versions/**" 21 | - ".github/**" 22 | 23 | 24 | jobs: 25 | build: 26 | uses: ./.github/workflows/build.yml -------------------------------------------------------------------------------- /.github/workflows/matrix_prep.yml: -------------------------------------------------------------------------------- 1 | name: step.matrix_prepare 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | target_subproject: 7 | description: see release.yml, for generating matrix entries 8 | type: string 9 | required: false 10 | default: '' 11 | outputs: 12 | matrix: 13 | description: The generated run matrix 14 | value: ${{ jobs.matrix_prep.outputs.matrix }} 15 | 16 | 17 | jobs: 18 | matrix_prep: 19 | runs-on: ubuntu-22.04 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - id: setmatrix 24 | run: python3 .github/workflows/scripts/matrix.py # ubuntu-22.04 uses Python 3.10.6 25 | env: 26 | TARGET_SUBPROJECT: ${{ inputs.target_subproject }} 27 | 28 | outputs: 29 | matrix: ${{ steps.setmatrix.outputs.matrix }} -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: PreRelease 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | target_subproject: 7 | description: |- 8 | The subproject name(s) of the specified Minecraft version to be released, seperated with ",". 9 | By default all subprojects will be released 10 | type: string 11 | required: false 12 | default: '' 13 | target_release_tag: 14 | description: The tag of the pre release you want to append the artifact to 15 | type: string 16 | required: true 17 | pre_release_version: 18 | description: The version for pre release 19 | type: string 20 | required: true 21 | default: '0' 22 | 23 | jobs: 24 | show_action_parameters: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Show action parameters 28 | run: | 29 | cat < $GITHUB_STEP_SUMMARY 30 | ## Action Parameters 31 | - target_subproject: \`${{ github.event.inputs.target_subproject }}\` 32 | - target_release_tag: \`${{ github.event.inputs.target_release_tag }}\` 33 | EOF 34 | 35 | matrix_prep: 36 | uses: ./.github/workflows/matrix_prep.yml 37 | with: 38 | target_subproject: ${{ github.event.inputs.target_subproject }} 39 | 40 | build: 41 | uses: ./.github/workflows/build.yml 42 | with: 43 | target_subproject: ${{ github.event.inputs.target_subproject }} 44 | release: false 45 | pre_version: ${{ github.event.inputs.pre_release_version }} 46 | 47 | release: 48 | needs: 49 | - matrix_prep 50 | - build 51 | runs-on: ubuntu-latest 52 | 53 | # allow the mod publish step to add asserts to release 54 | # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token 55 | permissions: 56 | contents: write 57 | 58 | strategy: 59 | matrix: ${{ fromJson(needs.matrix_prep.outputs.matrix) }} 60 | 61 | steps: 62 | - uses: actions/checkout@v3 63 | 64 | - name: Display context 65 | run: | 66 | echo ref_name = ${{ github.ref_name }} 67 | echo target_subproject = ${{ github.event.inputs.target_subproject }} 68 | echo target_release_tag = ${{ github.event.inputs.target_release_tag }} 69 | 70 | - name: Download build artifacts 71 | uses: actions/download-artifact@v3 72 | with: 73 | name: build-artifacts 74 | path: build-artifacts 75 | 76 | - name: Get github release information 77 | if: ${{ github.event_name == 'workflow_dispatch' }} 78 | id: get_release 79 | uses: cardinalby/git-get-release-action@1.2.4 80 | env: 81 | GITHUB_TOKEN: ${{ github.token }} 82 | with: 83 | tag: ${{ github.event.inputs.target_release_tag }} 84 | 85 | - name: Generate publish related information 86 | id: release_info 87 | run: | 88 | if [ $GITHUB_EVENT_NAME == 'release' ] 89 | then 90 | echo "tag_name=" >> $GITHUB_OUTPUT # leave an empty value here so softprops/action-gh-release will use the default value 91 | elif [ $GITHUB_EVENT_NAME == 'workflow_dispatch' ] 92 | then 93 | echo "tag_name=${{ github.event.inputs.target_release_tag }}" >> $GITHUB_OUTPUT 94 | else 95 | echo Unknown github event name $GITHUB_EVENT_NAME 96 | exit 1 97 | fi 98 | 99 | - name: Read common properties 100 | id: properties_g 101 | uses: christian-draeger/read-properties@1.1.1 102 | with: 103 | path: gradle.properties 104 | properties: 'mod_name mod_version' 105 | 106 | - name: Read version-specific properties 107 | id: properties_v 108 | uses: christian-draeger/read-properties@1.1.1 109 | with: 110 | path: ${{ format('versions/{0}/gradle.properties', matrix.subproject) }} 111 | properties: 'minecraft_version game_versions' 112 | 113 | - name: Fix game version 114 | id: game_versions 115 | run: | 116 | # Fixed \n in game_versions isn't parsed by christian-draeger/read-properties as a line separator 117 | echo 'value<> $GITHUB_OUTPUT 118 | echo -e "${{ steps.properties_v.outputs.game_versions }}" >> $GITHUB_OUTPUT 119 | echo 'EOF' >> $GITHUB_OUTPUT 120 | 121 | - name: Prepare file information 122 | id: file_info 123 | run: | 124 | shopt -s extglob 125 | FILE_PATHS=$(ls ${{ format('build-artifacts/{0}/build/libs/!(*-@(dev|sources|shadow)).jar', matrix.subproject) }}) 126 | if (( ${#FILE_PATHS[@]} != 1 )); then 127 | echo "Error: Found ${#FILE_PATHS[@]} files, expected exactly 1" 128 | exit 1 129 | else 130 | FILE_PATH=${FILE_PATHS[0]} 131 | fi 132 | 133 | FILE_NAME=$(basename $FILE_PATH) 134 | FILE_HASH=$(sha256sum $FILE_PATH | awk '{ print $1 }') 135 | echo "path=$FILE_PATH" >> $GITHUB_OUTPUT 136 | echo "name=$FILE_NAME" >> $GITHUB_OUTPUT 137 | echo "hash=$FILE_HASH" >> $GITHUB_OUTPUT 138 | cat $GITHUB_OUTPUT 139 | - name: Prepare changelog 140 | uses: actions/github-script@v6 141 | id: changelog 142 | with: 143 | script: return process.env.CHANGELOG 144 | result-encoding: string 145 | env: 146 | CHANGELOG: |- 147 | ${{ format('{0}{1}', github.event.release.body, steps.get_release.outputs.body) }} 148 | 149 | ------- 150 | 151 | Build Information 152 | 153 | - File name: `${{ steps.file_info.outputs.name }}` 154 | - SHA-256: `${{ steps.file_info.outputs.hash }}` 155 | - Built from: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 156 | 157 | - name: Publish Minecraft Mods 158 | uses: Kir-Antipov/mc-publish@v3.3 159 | with: 160 | modrinth-id: DgWBIBY5 # https://modrinth.com/mod/quickbackupmulti 161 | modrinth-token: ${{ secrets.MODRINTH_API_TOKEN }} 162 | 163 | github-tag: ${{ steps.release_info.outputs.tag_name }} 164 | github-token: ${{ secrets.GITHUB_TOKEN }} 165 | github-prerelease: true 166 | 167 | files: ${{ steps.file_info.outputs.path }} 168 | 169 | name: ${{ format('{0} v{1} for mc{2}', steps.properties_g.outputs.mod_name, steps.properties_g.outputs.mod_version, steps.properties_v.outputs.minecraft_version) }} 170 | version: ${{ format('mc{0}-v{1}', steps.properties_v.outputs.minecraft_version, steps.properties_g.outputs.mod_version) }} 171 | version-type: beta 172 | 173 | loaders: fabric 174 | game-versions: ${{ steps.game_versions.outputs.value }} 175 | game-version-filter: any 176 | 177 | github-changelog: ${{ format('{0}{1}', github.event.release.body, steps.get_release.outputs.body) }} 178 | modrinth-changelog: ${{ steps.changelog.outputs.result }} 179 | curseforge-changelog: ${{ steps.changelog.outputs.result }} 180 | 181 | retry-attempts: 3 182 | retry-delay: 10000 183 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | target_subproject: 7 | description: |- 8 | The subproject name(s) of the specified Minecraft version to be released, seperated with ",". 9 | By default all subprojects will be released 10 | type: string 11 | required: false 12 | default: '' 13 | target_release_tag: 14 | description: The tag of the release you want to append the artifact to 15 | type: string 16 | required: true 17 | 18 | 19 | jobs: 20 | show_action_parameters: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Show action parameters 24 | run: | 25 | cat < $GITHUB_STEP_SUMMARY 26 | ## Action Parameters 27 | - target_subproject: \`${{ github.event.inputs.target_subproject }}\` 28 | - target_release_tag: \`${{ github.event.inputs.target_release_tag }}\` 29 | EOF 30 | 31 | matrix_prep: 32 | uses: ./.github/workflows/matrix_prep.yml 33 | with: 34 | target_subproject: ${{ github.event.inputs.target_subproject }} 35 | 36 | build: 37 | uses: ./.github/workflows/build.yml 38 | with: 39 | target_subproject: ${{ github.event.inputs.target_subproject }} 40 | release: true 41 | 42 | release: 43 | needs: 44 | - matrix_prep 45 | - build 46 | runs-on: ubuntu-latest 47 | 48 | # allow the mod publish step to add asserts to release 49 | # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token 50 | permissions: 51 | contents: write 52 | 53 | strategy: 54 | matrix: ${{ fromJson(needs.matrix_prep.outputs.matrix) }} 55 | 56 | steps: 57 | - uses: actions/checkout@v3 58 | 59 | - name: Display context 60 | run: | 61 | echo ref_name = ${{ github.ref_name }} 62 | echo target_subproject = ${{ github.event.inputs.target_subproject }} 63 | echo target_release_tag = ${{ github.event.inputs.target_release_tag }} 64 | 65 | - name: Download build artifacts 66 | uses: actions/download-artifact@v3 67 | with: 68 | name: build-artifacts 69 | path: build-artifacts 70 | 71 | - name: Get github release information 72 | if: ${{ github.event_name == 'workflow_dispatch' }} 73 | id: get_release 74 | uses: cardinalby/git-get-release-action@1.2.4 75 | env: 76 | GITHUB_TOKEN: ${{ github.token }} 77 | with: 78 | tag: ${{ github.event.inputs.target_release_tag }} 79 | 80 | - name: Generate publish related information 81 | id: release_info 82 | run: | 83 | if [ $GITHUB_EVENT_NAME == 'release' ] 84 | then 85 | echo "tag_name=" >> $GITHUB_OUTPUT # leave an empty value here so softprops/action-gh-release will use the default value 86 | elif [ $GITHUB_EVENT_NAME == 'workflow_dispatch' ] 87 | then 88 | echo "tag_name=${{ github.event.inputs.target_release_tag }}" >> $GITHUB_OUTPUT 89 | else 90 | echo Unknown github event name $GITHUB_EVENT_NAME 91 | exit 1 92 | fi 93 | 94 | - name: Read common properties 95 | id: properties_g 96 | uses: christian-draeger/read-properties@1.1.1 97 | with: 98 | path: gradle.properties 99 | properties: 'mod_name mod_version' 100 | 101 | - name: Read version-specific properties 102 | id: properties_v 103 | uses: christian-draeger/read-properties@1.1.1 104 | with: 105 | path: ${{ format('versions/{0}/gradle.properties', matrix.subproject) }} 106 | properties: 'minecraft_version game_versions' 107 | 108 | - name: Fix game version 109 | id: game_versions 110 | run: | 111 | # Fixed \n in game_versions isn't parsed by christian-draeger/read-properties as a line separator 112 | echo 'value<> $GITHUB_OUTPUT 113 | echo -e "${{ steps.properties_v.outputs.game_versions }}" >> $GITHUB_OUTPUT 114 | echo 'EOF' >> $GITHUB_OUTPUT 115 | 116 | - name: Prepare file information 117 | id: file_info 118 | run: | 119 | shopt -s extglob 120 | FILE_PATHS=$(ls ${{ format('build-artifacts/{0}/build/libs/!(*-@(dev|sources|shadow)).jar', matrix.subproject) }}) 121 | if (( ${#FILE_PATHS[@]} != 1 )); then 122 | echo "Error: Found ${#FILE_PATHS[@]} files, expected exactly 1" 123 | exit 1 124 | else 125 | FILE_PATH=${FILE_PATHS[0]} 126 | fi 127 | 128 | FILE_NAME=$(basename $FILE_PATH) 129 | FILE_HASH=$(sha256sum $FILE_PATH | awk '{ print $1 }') 130 | echo "path=$FILE_PATH" >> $GITHUB_OUTPUT 131 | echo "name=$FILE_NAME" >> $GITHUB_OUTPUT 132 | echo "hash=$FILE_HASH" >> $GITHUB_OUTPUT 133 | cat $GITHUB_OUTPUT 134 | - name: Prepare changelog 135 | uses: actions/github-script@v6 136 | id: changelog 137 | with: 138 | script: return process.env.CHANGELOG 139 | result-encoding: string 140 | env: 141 | CHANGELOG: |- 142 | ${{ format('{0}{1}', github.event.release.body, steps.get_release.outputs.body) }} 143 | 144 | ------- 145 | 146 | Build Information 147 | 148 | - File name: `${{ steps.file_info.outputs.name }}` 149 | - SHA-256: `${{ steps.file_info.outputs.hash }}` 150 | - Built from: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 151 | 152 | - name: Publish Minecraft Mods 153 | uses: Kir-Antipov/mc-publish@v3.3 154 | with: 155 | modrinth-id: DgWBIBY5 # https://modrinth.com/mod/quickbackupmulti 156 | modrinth-token: ${{ secrets.MODRINTH_API_TOKEN }} 157 | 158 | curseforge-id: 951047 # https://www.curseforge.com/minecraft/mc-mods/quickbackupmulti 159 | curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }} 160 | 161 | github-tag: ${{ steps.release_info.outputs.tag_name }} 162 | github-token: ${{ secrets.GITHUB_TOKEN }} 163 | 164 | files: ${{ steps.file_info.outputs.path }} 165 | 166 | name: ${{ format('{0} v{1} for mc{2}', steps.properties_g.outputs.mod_name, steps.properties_g.outputs.mod_version, steps.properties_v.outputs.minecraft_version) }} 167 | version: ${{ format('mc{0}-v{1}', steps.properties_v.outputs.minecraft_version, steps.properties_g.outputs.mod_version) }} 168 | version-type: release 169 | 170 | loaders: fabric 171 | game-versions: ${{ steps.game_versions.outputs.value }} 172 | game-version-filter: any 173 | 174 | github-changelog: ${{ format('{0}{1}', github.event.release.body, steps.get_release.outputs.body) }} 175 | modrinth-changelog: ${{ steps.changelog.outputs.result }} 176 | curseforge-changelog: ${{ steps.changelog.outputs.result }} 177 | 178 | retry-attempts: 3 179 | retry-delay: 10000 180 | -------------------------------------------------------------------------------- /.github/workflows/scripts/matrix.py: -------------------------------------------------------------------------------- 1 | """ 2 | A script to scan through the versions directory and collect all folder names as the subproject list, 3 | then output a json as the github action include matrix 4 | """ 5 | __author__ = 'Fallen_Breath' 6 | 7 | import json 8 | import os 9 | import sys 10 | 11 | 12 | def main(): 13 | target_subproject_env = os.environ.get('TARGET_SUBPROJECT', '') 14 | target_subprojects = list(filter(None, target_subproject_env.split(',') if target_subproject_env != '' else [])) 15 | print('target_subprojects: {}'.format(target_subprojects)) 16 | 17 | with open('settings.json') as f: 18 | settings: dict = json.load(f) 19 | 20 | if len(target_subprojects) == 0: 21 | subprojects = settings['versions'] 22 | else: 23 | subprojects = [] 24 | for subproject in settings['versions']: 25 | if subproject in target_subprojects: 26 | subprojects.append(subproject) 27 | target_subprojects.remove(subproject) 28 | if len(target_subprojects) > 0: 29 | print('Unexpected subprojects: {}'.format(target_subprojects), file=sys.stderr) 30 | sys.exit(1) 31 | 32 | matrix_entries = [] 33 | for subproject in subprojects: 34 | matrix_entries.append({ 35 | 'subproject': subproject, 36 | }) 37 | matrix = {'include': matrix_entries} 38 | with open(os.environ['GITHUB_OUTPUT'], 'w') as f: 39 | f.write('matrix={}\n'.format(json.dumps(matrix))) 40 | 41 | print('matrix:') 42 | print(json.dumps(matrix, indent=2)) 43 | 44 | 45 | if __name__ == '__main__': 46 | main() -------------------------------------------------------------------------------- /.github/workflows/scripts/summary.py: -------------------------------------------------------------------------------- 1 | """ 2 | A script to scan through all valid mod jars in build-artifacts.zip/$version/build/libs, 3 | and generate an artifact summary table for that to GitHub action step summary 4 | """ 5 | __author__ = 'Fallen_Breath' 6 | 7 | import functools 8 | import glob 9 | import hashlib 10 | import json 11 | import os 12 | 13 | 14 | def read_prop(file_name: str, key: str) -> str: 15 | with open(file_name) as prop: 16 | return next(filter( 17 | lambda l: l.split('=', 1)[0].strip() == key, 18 | prop.readlines() 19 | )).split('=', 1)[1].lstrip() 20 | 21 | 22 | def get_sha256_hash(file_path: str) -> str: 23 | sha256_hash = hashlib.sha256() 24 | 25 | with open(file_path, 'rb') as f: 26 | for buf in iter(functools.partial(f.read, 4096), b''): 27 | sha256_hash.update(buf) 28 | 29 | return sha256_hash.hexdigest() 30 | 31 | 32 | def main(): 33 | target_subproject_env = os.environ.get('TARGET_SUBPROJECT', '') 34 | target_subprojects = list(filter(None, target_subproject_env.split(',') if target_subproject_env != '' else [])) 35 | print('target_subprojects: {}'.format(target_subprojects)) 36 | 37 | with open('settings.json') as f: 38 | settings: dict = json.load(f) 39 | 40 | with open(os.environ['GITHUB_STEP_SUMMARY'], 'w') as f: 41 | f.write('## Build Artifacts Summary\n\n') 42 | f.write('| Subproject | for Minecraft | File | Size | SHA-256 |\n') 43 | f.write('| --- | --- | --- | --- | --- |\n') 44 | 45 | warnings = [] 46 | for subproject in settings['versions']: 47 | if len(target_subprojects) > 0 and subproject not in target_subprojects: 48 | print('skipping {}'.format(subproject)) 49 | continue 50 | game_versions = read_prop('versions/{}/gradle.properties'.format(subproject), 'game_versions') 51 | game_versions = game_versions.strip().replace('\\n', ', ') 52 | file_paths = glob.glob('build-artifacts/{}/build/libs/*.jar'.format(subproject)) 53 | file_paths = list(filter(lambda fp: not fp.endswith('-sources.jar') and not fp.endswith('-dev.jar') and not fp.endswith('-shadow.jar'), file_paths)) 54 | if len(file_paths) == 0: 55 | file_name = '*not found*' 56 | sha256 = '*N/A*' 57 | else: 58 | file_name = '`{}`'.format(os.path.basename(file_paths[0])) 59 | file_size = '{} B'.format(os.path.getsize(file_paths[0])) 60 | sha256 = '`{}`'.format(get_sha256_hash(file_paths[0])) 61 | if len(file_paths) > 1: 62 | warnings.append('Found too many build files in subproject {}: {}'.format(subproject, ', '.join(file_paths))) 63 | 64 | f.write('| {} | {} | {} | {} | {} |\n'.format(subproject, game_versions, file_name, file_size, sha256)) 65 | 66 | if len(warnings) > 0: 67 | f.write('\n### Warnings\n\n') 68 | for warning in warnings: 69 | f.write('- {}\n'.format(warning)) 70 | 71 | 72 | if __name__ == '__main__': 73 | main() -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # macos 28 | 29 | *.DS_Store 30 | 31 | # fabric 32 | 33 | run/ 34 | 35 | # java 36 | 37 | hs_err_*.log 38 | replay_*.log 39 | *.hprof 40 | *.jfr 41 | 42 | # preprocess 43 | /.run/ 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 SkyDynamic 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/github/license/SkyDynamic/QuickBackupM-Fabric.svg)](https://www.apache.org/licenses/LICENSE-2.0) 2 | [![Issues](https://img.shields.io/github/issues/SkyDynamic/QuickBackupM-Fabric.svg)](https://github.com/SkyDynamic/QuickBackupM-Fabric/issues) 3 | [![Modrinth](https://img.shields.io/modrinth/dt/DgWBIBY5?label=Modrinth%20Downloads)](https://modrinth.com/mod/quickbackupmulti) 4 | 5 |
6 | Logo 7 |
8 |
9 | 10 | # QuickBackupMulti-Fabric 11 | 12 | **简体中文** | [English](README_EN.MD) 13 | 14 | _✨ MC备份 / 回档模组 ✨_ 15 | 重构自MCDR插件: [QuickBackupMulti](https://github.com/TISUnion/QuickBackupM) 16 | 17 |
18 | 19 | > [!WARNING] 20 | > 当前Mod大版本为`v2`, 与`v1`的实现代码有很大差异 21 | > 22 | > 如果使用的是`v1`版本, 请勿直接更新到`v2`. 使用`v2`的也请勿随意降级到`v1` 23 | 24 | [//]: # (> 本 Mod 对于客户端单人游戏兼容性较差,使用时请谨慎,若造成存档损坏本 Mod 不负任何责任) 25 | 26 | ## 本Mod优势 27 | - 支持回档自动重启服务器, 不再是只备份不回档 28 | - 支持定时备份, 并支持自定义cron表达式 29 | 30 | ## 使用方式 31 | > [!WARNING] 32 | > 严禁自行删除备份文件夹内的所有备份文件, 如需删除请进入游戏内进行手动删除! 33 | 34 | > 在使用mod前请确保你已安装Fabric Loader 35 | 36 | 将本mod放进`mods`文件夹即可 37 | 38 | ## 指令 39 | `/qb` 或 `/quickbackupmulti`均可触发mod 40 | 41 | ## todo 42 | - [x] 定时备份 43 | - [x] 无限槽位 44 | - [x] Hash对比并仅备份差异文件 45 | - [ ] 个性化设置 46 | 47 | ## 许可 48 | 本项目遵循 [Apache-2.0 license](https://www.apache.org/licenses/LICENSE-2.0) 许可 49 | -------------------------------------------------------------------------------- /README_EN.MD: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/github/license/SkyDynamic/QuickBackupM-Fabric.svg)](https://www.apache.org/licenses/LICENSE-2.0) 2 | [![Issues](https://img.shields.io/github/issues/SkyDynamic/QuickBackupM-Fabric.svg)](https://github.com/SkyDynamic/QuickBackupM-Fabric/issues) 3 | [![Modrinth](https://img.shields.io/modrinth/dt/DgWBIBY5?label=Modrinth%20Downloads)](https://modrinth.com/mod/quickbackupmulti) 4 | 5 |
6 | Logo 7 |
8 |
9 | 10 | # QuickBackupMulti-Fabric 11 | 12 | [简体中文](README.MD) | **English** 13 | 14 | _✨ Minecraft Backup / Restore Mod ✨_ 15 | Idea come from: [QuickBackupMulti](https://github.com/TISUnion/QuickBackupM) 16 | 17 |
18 | 19 | > [!WARNING] 20 | > Now the Mod version is `v2`, and there are great differences backup implementation method compared to the `v1` version 21 | > 22 | > If you use `v1` now, please don't to upgrade to `v2`. Also, if you use `v2` now, please don't downgrade to `v1` 23 | 24 | [//]: # (> This mod has poor support in SinglePlay. So use it with great caution. If your save break, This mod will not be responsible for you.) 25 | 26 | ## Advantage 27 | - Support auto start server after restore backup 28 | - Support schedule backup and custom cron expression. 29 | 30 | ## How to use 31 | > [!WARNING] 32 | > It is strictly prohibited to delete all backup files in the backup folder by oneself. 33 | > If you need to delete them, please enter the game and use the command to delete them! 34 | 35 | > Please install Fabric Loader before install this mod 36 | 37 | Go to [release](https://github.com/SkyDynamic/QuickBackupM-Fabric/releases) to download the latest mod and put the `.jar` file in `mods` dir 38 | 39 | ## Command 40 | `/qb` or `/quickbackupmulti` can trigger the mod 41 | 42 | ## todo 43 | - [x] Schedule Backup 44 | - [x] Infinite Slot 45 | - [x] Compare files hash and save what files had change 46 | - [ ] Personal Setting 47 | 48 | ## LICENSE 49 | This Project follow [Apache-2.0 license](https://www.apache.org/licenses/LICENSE-2.0) 50 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "fabric-loom" version "1.7-SNAPSHOT" apply false 3 | id "maven-publish" 4 | id "com.replaymod.preprocess" version "SNAPSHOT" 5 | } 6 | 7 | preprocess { 8 | def mc118 = createNode("1.18.2", 1_18_02, "yarn") 9 | def mc119 = createNode("1.19.4", 1_19_04, "yarn") 10 | def mc1200 = createNode("1.20", 1_20_00, "yarn") 11 | def mc1203 = createNode("1.20.3", 1_20_03, "yarn") 12 | def mc1205 = createNode("1.20.5", 1_20_05, "yarn") 13 | def mc1210 = createNode("1.21", 1_21_00, "yarn") 14 | 15 | mc118.link(mc119, file("versions/mapping-1.18-1.19.txt")) 16 | mc119.link(mc1200, file("versions/mapping-empty.txt")) 17 | mc1200.link(mc1203, file("versions/mapping-empty.txt")) 18 | mc1203.link(mc1205, file("versions/mapping-empty.txt")) 19 | mc1205.link(mc1210, file("versions/mapping-empty.txt")) 20 | } 21 | 22 | tasks.register("buildAndGather") { 23 | subprojects { 24 | dependsOn project.name + ":remapJar" 25 | } 26 | doFirst { 27 | delete fileTree(project.projectDir.toPath().resolve('build/libs')) { 28 | include '*' 29 | } 30 | copy { 31 | subprojects { 32 | def libDir = project.projectDir.toPath().resolve('build/libs') 33 | from(libDir) { 34 | include '*.jar' 35 | exclude '*-dev.jar', '*-sources.jar' 36 | } 37 | into 'build/libs/' 38 | duplicatesStrategy DuplicatesStrategy.INCLUDE 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /common.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'fabric-loom' 2 | apply plugin: 'com.replaymod.preprocess' 3 | 4 | int mcVersion = 1 5 | 6 | ext { 7 | getVersionSuffix = { 8 | String versionSuffix = '' 9 | if (System.getenv("BUILD_RELEASE") != "true") { 10 | def PRE_VERSION = System.getenv("PRE_VERSION") 11 | if (PRE_VERSION != null && PRE_VERSION != "" && PRE_VERSION != "null") { 12 | versionSuffix += ("+pre-release." + PRE_VERSION) 13 | } else { 14 | String buildNumber = System.getenv("BUILD_ID") 15 | versionSuffix += buildNumber != null ? ('+build.' + buildNumber) : '+SNAPSHOT' 16 | } 17 | } 18 | return versionSuffix 19 | } 20 | } 21 | 22 | preprocess { 23 | mcVersion = vars.get()["MC"] as int 24 | tabIndentation = true 25 | } 26 | 27 | configurations { 28 | modRuntimeOnly.exclude group: 'net.fabricmc', module: 'fabric-loader' 29 | } 30 | 31 | repositories { 32 | mavenLocal() 33 | maven { 34 | name 'aliyunMavenCentralMirror' 35 | url 'https://maven.aliyun.com/repository/central' 36 | } 37 | maven { 38 | name 'mavenCentral' 39 | url 'https://repo1.maven.org/maven2/' 40 | } 41 | maven { 42 | name 'nekoMaven' 43 | url 'https://maven.takeneko.icu/releases' 44 | } 45 | maven { 46 | name 'CjsahMaven' 47 | url 'https://server.cjsah.net:1002/maven/' 48 | } 49 | } 50 | 51 | loom { 52 | accessWidenerPath = file("qbm.accesswidener") 53 | } 54 | 55 | remapJar { 56 | remapperIsolation = true 57 | } 58 | 59 | JavaVersion SOURCE_JAVA_VERSION 60 | JavaVersion TARGET_JAVA_VERSION 61 | if (mcVersion >= 12005) { 62 | SOURCE_JAVA_VERSION = JavaVersion.VERSION_21 63 | TARGET_JAVA_VERSION = JavaVersion.VERSION_21 64 | } else if (mcVersion >= 11800){ 65 | SOURCE_JAVA_VERSION = JavaVersion.VERSION_17 66 | TARGET_JAVA_VERSION = JavaVersion.VERSION_17 67 | } else if (mcVersion >= 11700) { 68 | SOURCE_JAVA_VERSION = JavaVersion.VERSION_16 69 | TARGET_JAVA_VERSION = JavaVersion.VERSION_16 70 | } else { 71 | SOURCE_JAVA_VERSION = JavaVersion.VERSION_1_8 72 | TARGET_JAVA_VERSION = JavaVersion.VERSION_17 73 | } 74 | JavaVersion MIXIN_COMPATIBILITY_LEVEL = TARGET_JAVA_VERSION 75 | 76 | dependencies { 77 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 78 | mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 79 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 80 | modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 81 | 82 | implementation('commons-io:commons-io:2.15.1') 83 | 84 | include(implementation('org.yaml:snakeyaml:2.0')) 85 | 86 | include(implementation('io.github.skydynamic:incremental-storage-lib:1.0.4')) 87 | 88 | include(implementation('org.quartz-scheduler:quartz:2.3.2')) 89 | 90 | include(implementation('dev.morphia.morphia:morphia-core:2.3.9')) 91 | include(implementation('de.bwaldvogel:mongo-java-server:1.45.0')) 92 | include(implementation('de.bwaldvogel:mongo-java-server-core:1.45.0')) 93 | include(implementation('de.bwaldvogel:mongo-java-server-h2-backend:1.45.0')) 94 | include(implementation('org.mongodb:mongodb-driver-sync:4.11.1')) 95 | include(implementation('org.mongodb:mongodb-driver-core:4.11.1')) 96 | include(implementation('org.mongodb:bson:4.11.1')) 97 | include(implementation('com.h2database:h2:2.2.224')) 98 | include(implementation('org.lz4:lz4-java:1.8.0')) 99 | 100 | } 101 | 102 | String versionSuffix = project.getVersionSuffix() 103 | 104 | String fullModVersion = project.mod_version + versionSuffix 105 | 106 | version = 'v' + fullModVersion 107 | group = project.maven_group 108 | archivesBaseName = project.archives_base_name + '-mc' + project.minecraft_version 109 | 110 | String fabric_id = "fabric-api" 111 | if (mcVersion < 11800) { 112 | fabric_id = "fabric" 113 | } 114 | 115 | processResources { 116 | from "qbm.accesswidener" 117 | 118 | inputs.property "version", project.version 119 | 120 | filesMatching("fabric.mod.json") { 121 | def valueMap = [ 122 | "version" : fullModVersion, 123 | "id" : project.mod_id, 124 | "name" : project.archives_base_name, 125 | "minecraft_support": project.minecraft_support_version, 126 | "fabric_loader" : project.loader_version, 127 | "fabric_api_id" : fabric_id 128 | ] 129 | expand valueMap 130 | } 131 | 132 | filesMatching("quickbackupmulti.mixins.json") { 133 | filter { 134 | text -> text.replace("/*JAVA_VERSION*/", "JAVA_${MIXIN_COMPATIBILITY_LEVEL.ordinal() + 1}") 135 | } 136 | } 137 | } 138 | 139 | java { 140 | withSourcesJar() 141 | sourceCompatibility = SOURCE_JAVA_VERSION 142 | targetCompatibility = TARGET_JAVA_VERSION 143 | } 144 | 145 | jar { 146 | from "LICENSE" 147 | } 148 | 149 | tasks.withType(JavaCompile) { 150 | options.encoding = "UTF-8" 151 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx6G 2 | org.gradle.parallel=true 3 | 4 | mod_id=quickbackupmulti 5 | mod_name=QuickBackupMulti 6 | mod_version=2.1.0 7 | maven_group=dev.skydynamic 8 | archives_base_name=QuickBackupMulti -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyDynamic/QuickBackupM-Fabric/bfb0a1999a89b3fd48193b47f6b75198732eda00/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | org.gradle.jvmargs=-Dfile.encoding=UTF-8 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command; 206 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 207 | # shell script including quotes and variable substitutions, so put them in 208 | # double quotes to make sure that they get re-expanded; and 209 | # * put everything else in single quotes, so that it's not re-expanded. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /indexImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyDynamic/QuickBackupM-Fabric/bfb0a1999a89b3fd48193b47f6b75198732eda00/indexImg.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | maven { 8 | name = 'Jitpack' 9 | url = "https://jitpack.io" 10 | } 11 | maven { 12 | url 'https://maven.aliyun.com/repository/public/' 13 | } 14 | resolutionStrategy { 15 | eachPlugin { 16 | switch (requested.id.id) { 17 | case "com.replaymod.preprocess": { 18 | useModule("com.github.SkyDynamic:preprocessor:${requested.version}") 19 | break 20 | } 21 | } 22 | } 23 | } 24 | mavenCentral() 25 | gradlePluginPortal() 26 | } 27 | } 28 | 29 | def versions = Arrays.asList( 30 | "1.18.2", 31 | "1.19.4", 32 | "1.20", 33 | "1.20.3", 34 | "1.20.5", 35 | "1.21" 36 | ) 37 | 38 | for (String version : versions) { 39 | include(":$version") 40 | 41 | def proj = project(":$version") 42 | proj.projectDir = file("versions/$version") 43 | proj.buildFileName = "../../common.gradle" 44 | } -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "versions": [ 3 | "1.18.2", 4 | "1.19.4", 5 | "1.20", 6 | "1.20.3", 7 | "1.20.5", 8 | "1.21" 9 | ] 10 | } -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/DataBaseManager.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti; 2 | 3 | import io.github.skydynamic.increment.storage.lib.Interface.IDataBaseManager; 4 | 5 | import java.nio.file.Path; 6 | 7 | public class DataBaseManager implements IDataBaseManager { 8 | String fileName; 9 | String collectionName; 10 | Path dataBasePath; 11 | 12 | public DataBaseManager(String fileName, String collectionName, Path dataBasePath) { 13 | this.fileName = fileName; 14 | this.collectionName = collectionName; 15 | this.dataBasePath = dataBasePath; 16 | } 17 | 18 | @Override 19 | public void setFileName(String s) { 20 | this.fileName = s; 21 | } 22 | 23 | @Override 24 | public void setCollectionName(String s) { 25 | this.collectionName = s; 26 | } 27 | 28 | @Override 29 | public void setDataBasePath(Path path) { 30 | this.dataBasePath = path; 31 | } 32 | 33 | @Override 34 | public String getFileName() { 35 | return this.fileName; 36 | } 37 | 38 | @Override 39 | public String getCollectionName() { 40 | return this.collectionName; 41 | } 42 | 43 | @Override 44 | public Path getDataBasePath() { 45 | return this.dataBasePath; 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/JavaUtilLog4jFilter.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti; 2 | 3 | import org.apache.logging.log4j.Level; 4 | import org.apache.logging.log4j.core.LogEvent; 5 | import org.apache.logging.log4j.core.filter.AbstractFilter; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.util.logging.Filter; 9 | import java.util.logging.LogRecord; 10 | 11 | public class JavaUtilLog4jFilter extends AbstractFilter implements Filter { 12 | public boolean isLoggable(@NotNull LogRecord record) { 13 | return !QuickBackupMulti.shouldFilterMessage( 14 | Level.valueOf(record.getLevel().getName()), record.getLoggerName() 15 | ); 16 | } 17 | 18 | public Result filter(@NotNull LogEvent event) { 19 | return QuickBackupMulti.shouldFilterMessage( 20 | event.getLevel(), event.getLoggerName() 21 | ) ? Result.DENY : Result.NEUTRAL; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/QbmConstant.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import io.github.skydynamic.quickbackupmulti.utils.ServerPathUtils; 6 | import net.minecraft.util.Identifier; 7 | 8 | public final class QbmConstant { 9 | public static final Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create(); 10 | public static final ServerPathUtils pathGetter = new ServerPathUtils(); 11 | public static final Identifier REQUEST_OPEN_CONFIG_GUI_PACKET_ID = Identifier.tryParse("quickbackupmulti:request_open_config_gui"); 12 | public static final Identifier OPEN_CONFIG_GUI_PACKET_ID = Identifier.tryParse("quickbackupmulti:open_config_gui"); 13 | public static final Identifier SAVE_CONFIG_PACKET_ID = Identifier.tryParse("quickbackupmulti:save_config"); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/QuickBackupMulti.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti; 2 | 3 | import io.github.skydynamic.increment.storage.lib.util.IndexUtil; 4 | import io.github.skydynamic.increment.storage.lib.util.Storager; 5 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 6 | import io.github.skydynamic.quickbackupmulti.config.Config; 7 | 8 | import io.github.skydynamic.increment.storage.lib.database.DataBase; 9 | import io.github.skydynamic.quickbackupmulti.config.ConfigStorage; 10 | import io.github.skydynamic.quickbackupmulti.command.QuickBackupMultiCommand; 11 | import io.github.skydynamic.quickbackupmulti.utils.QbmManager; 12 | import net.fabricmc.api.EnvType; 13 | import net.fabricmc.api.ModInitializer; 14 | import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 15 | import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; 16 | import net.fabricmc.loader.api.FabricLoader; 17 | //#if MC>=11900 18 | import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; 19 | //#else 20 | //$$ import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; 21 | //#endif 22 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 23 | 24 | //#if MC>=12005 25 | //$$ import net.minecraft.server.network.ServerPlayerEntity; 26 | //#endif 27 | import net.minecraft.network.PacketByteBuf; 28 | 29 | import org.apache.logging.log4j.Level; 30 | import org.apache.logging.log4j.LogManager; 31 | //#if MC>=11900 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | //#else 35 | //$$ import org.apache.logging.log4j.Logger; 36 | //#endif 37 | import java.nio.file.Path; 38 | 39 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.gson; 40 | 41 | 42 | public class QuickBackupMulti implements ModInitializer { 43 | 44 | //#if MC>=11900 45 | public static final Logger LOGGER = LoggerFactory.getLogger(QuickBackupMulti.class); 46 | //#else 47 | //$$ public static final Logger LOGGER = LogManager.getLogger(QuickBackupMulti.class); 48 | //#endif 49 | public static final String modName = "QuickBackupMulti"; 50 | 51 | EnvType env = FabricLoader.getInstance().getEnvironmentType(); 52 | 53 | private static DataBase dataBase; 54 | private static Storager storager; 55 | 56 | @Override 57 | public void onInitialize() { 58 | //#if MC>=12005 59 | //$$ Packets.registerPacketCodec(); 60 | //#endif 61 | final JavaUtilLog4jFilter filter = new JavaUtilLog4jFilter(); 62 | java.util.logging.Logger.getLogger("").setFilter(filter); 63 | ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addFilter(filter); 64 | 65 | Config.INSTANCE.load(); 66 | Translate.handleResourceReload(Config.INSTANCE.getLang()); 67 | 68 | //#if MC>=11900 69 | CommandRegistrationCallback.EVENT.register( 70 | (dispatcher, registryAccess, environment) -> QuickBackupMultiCommand.RegisterCommand(dispatcher) 71 | ); 72 | //#else 73 | //$$ CommandRegistrationCallback.EVENT.register( 74 | //$$ (dispatcher, registryAccess) -> QuickBackupMultiCommand.RegisterCommand(dispatcher) 75 | //$$ ); 76 | //#endif 77 | registerPacketHandler(); 78 | ServerLifecycleEvents.SERVER_STARTED.register(server -> { 79 | Config.TEMP_CONFIG.setServerValue(server); 80 | Config.TEMP_CONFIG.setEnv(env); 81 | }); 82 | 83 | ServerLifecycleEvents.SERVER_STOPPED.register(server -> { 84 | if (env == EnvType.SERVER) { 85 | if (Config.TEMP_CONFIG.isBackup) { 86 | QbmManager.restore(Config.TEMP_CONFIG.backupSlot); 87 | getDataBase().stopInternalMongoServer(); 88 | Config.TEMP_CONFIG.setIsBackupValue(false); 89 | Config.TEMP_CONFIG.server.stopped = false; 90 | Config.TEMP_CONFIG.server.running = true; 91 | Config.TEMP_CONFIG.server.runServer(); 92 | } else { 93 | getDataBase().stopInternalMongoServer(); 94 | } 95 | } 96 | Config.TEMP_CONFIG.server = null; 97 | }); 98 | } 99 | 100 | public static void registerPacketHandler() { 101 | //#if MC>=12005 102 | //$$ ServerPlayNetworking.registerGlobalReceiver( 103 | //$$ Packets.RequestOpenConfigGuiPacket.PACKET_ID, (payload, context) -> { 104 | //#else 105 | ServerPlayNetworking.registerGlobalReceiver( 106 | QbmConstant.REQUEST_OPEN_CONFIG_GUI_PACKET_ID, (server, player, handler, buf, responseSender) -> { 107 | //#endif 108 | //#if MC>=12005 109 | //$$ ServerPlayerEntity player = context.player(); 110 | //$$ if (player.hasPermissionLevel(2)) ServerPlayNetworking.send( 111 | //$$ player, new Packets.OpenConfigGuiPacket(gson.toJson(Config.INSTANCE.getConfigStorage())) 112 | //$$ ); 113 | //#else 114 | if (player.hasPermissionLevel(2)) { 115 | PacketByteBuf sendBuf = PacketByteBufs.create(); 116 | sendBuf.writeString(gson.toJson(Config.INSTANCE.getConfigStorage())); 117 | ServerPlayNetworking.send(player, QbmConstant.OPEN_CONFIG_GUI_PACKET_ID, sendBuf); 118 | } 119 | //#endif 120 | }); 121 | 122 | //#if MC>=12005 123 | //$$ ServerPlayNetworking.registerGlobalReceiver(Packets.SaveConfigPacket.PACKET_ID, (payload, context) -> { 124 | //#else 125 | ServerPlayNetworking.registerGlobalReceiver( 126 | QbmConstant.SAVE_CONFIG_PACKET_ID, (server, player, handler, buf, responseSender) -> { 127 | //#endif 128 | //#if MC>=12005 129 | //$$ ServerPlayerEntity player = context.player(); 130 | //#endif 131 | if (player.hasPermissionLevel(2)) { 132 | //#if MC>=12005 133 | //$$ String configStorage = payload.config(); 134 | //#else 135 | String configStorage = buf.readString(); 136 | //#endif 137 | ConfigStorage c = QbmConstant.gson.fromJson(configStorage, ConfigStorage.class); 138 | // Verify config 139 | ConfigStorage result = QbmManager.verifyConfig(c, player); 140 | Config.INSTANCE.setConfigStorage(result); 141 | } 142 | }); 143 | } 144 | 145 | public static boolean shouldFilterMessage(Level level, String packetName) { 146 | // 仅过滤INFO,Debug / ERROR不过滤 147 | if (level == Level.INFO) { 148 | return packetName.contains("de.bwaldvogel.mongo") 149 | || packetName.contains("org.mongodb.driver") 150 | || packetName.contains("org.quartz"); 151 | } 152 | return false; 153 | } 154 | 155 | public static DataBase getDataBase() { 156 | return dataBase; 157 | } 158 | 159 | public static Storager getStorager() { 160 | return storager; 161 | } 162 | 163 | public static void setDataBase(String worldName) { 164 | DataBaseManager dataBaseManager = new DataBaseManager( 165 | "QuickBackupMulti", 166 | modName + "-" + worldName, 167 | Path.of(Config.INSTANCE.getStoragePath()) 168 | ); 169 | dataBase = new DataBase(dataBaseManager, Config.INSTANCE.getConfigStorage()); 170 | storager = new Storager(dataBase); 171 | IndexUtil.setDataBase(dataBase); 172 | } 173 | } -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/QuickBackupMultiClient.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti; 2 | 3 | import io.github.skydynamic.quickbackupmulti.screen.ConfigScreen; 4 | import io.github.skydynamic.quickbackupmulti.config.ConfigStorage; 5 | 6 | import net.fabricmc.api.ClientModInitializer; 7 | //#if MC>=12005 8 | //$$ import net.minecraft.client.MinecraftClient; 9 | //#endif 10 | //#if MC>=11900 11 | import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; 12 | import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; 13 | //#else 14 | //$$ import net.fabricmc.fabric.api.client.command.v1.ClientCommandManager; 15 | //$$ import net.fabricmc.fabric.api.client.command.v1.FabricClientCommandSource; 16 | //$$ import com.mojang.brigadier.CommandDispatcher; 17 | //#endif 18 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 19 | import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 20 | 21 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.*; 22 | 23 | public class QuickBackupMultiClient implements ClientModInitializer { 24 | /** 25 | * Runs the mod initializer on the client environment. 26 | */ 27 | @Override 28 | public void onInitializeClient() { 29 | //#if MC>=11900 30 | ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> { 31 | dispatcher.register(ClientCommandManager.literal("open-qb-config-screen").executes( 32 | context -> { 33 | //#if MC>=12005 34 | //$$ ClientPlayNetworking.send(new Packets.RequestOpenConfigGuiPacket("")); 35 | //#else 36 | ClientPlayNetworking.send(REQUEST_OPEN_CONFIG_GUI_PACKET_ID, PacketByteBufs.empty()); 37 | //#endif 38 | return 1; 39 | } 40 | )); 41 | }); 42 | //#else 43 | //$$ CommandDispatcher dispatcher = ClientCommandManager.DISPATCHER; 44 | //$$ dispatcher.register(ClientCommandManager.literal("open-qb-config-screen").executes( 45 | //$$ context -> { 46 | //$$ ClientPlayNetworking.send(REQUEST_OPEN_CONFIG_GUI_PACKET_ID, PacketByteBufs.empty()); 47 | //$$ return 1; 48 | //$$ })); 49 | //#endif 50 | 51 | registerPacketHandler(); 52 | } 53 | 54 | public static void registerPacketHandler() { 55 | //#if MC<12005 56 | ClientPlayNetworking.registerGlobalReceiver(OPEN_CONFIG_GUI_PACKET_ID, (client, handler, buf, responseSender) -> { 57 | String config = buf.readString(); 58 | ConfigStorage c = gson.fromJson(config, ConfigStorage.class); 59 | client.execute(() -> client.setScreen(new ConfigScreen(client.currentScreen, c))); 60 | }); 61 | //#else 62 | //$$ ClientPlayNetworking.registerGlobalReceiver(Packets.OpenConfigGuiPacket.PACKET_ID, (payload, context) -> { 63 | //$$ String config = payload.config(); 64 | //$$ ConfigStorage c = gson.fromJson(config, ConfigStorage.class); 65 | //$$ MinecraftClient client = context.client(); 66 | //$$ client.execute(() -> client.setScreen(new ConfigScreen(client.currentScreen, c))); 67 | //$$ }); 68 | //#endif 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/api/ServerPathGetter.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.api; 2 | 3 | import java.nio.file.Path; 4 | 5 | public interface ServerPathGetter { 6 | Path getConfigPath(); 7 | Path getGamePath(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/backup/ClientRestoreDelegate.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.backup; 2 | 3 | import io.github.skydynamic.quickbackupmulti.config.Config; 4 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 5 | import net.fabricmc.api.EnvType; 6 | import net.fabricmc.api.Environment; 7 | import net.minecraft.client.MinecraftClient; 8 | import net.minecraft.client.gui.screen.MessageScreen; 9 | import net.minecraft.client.toast.SystemToast; 10 | import net.minecraft.server.network.ServerPlayerEntity; 11 | import net.minecraft.text.Text; 12 | 13 | import java.util.List; 14 | import java.util.concurrent.CompletableFuture; 15 | 16 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getDataBase; 17 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.restoreClient; 18 | 19 | @Environment(EnvType.CLIENT) 20 | public class ClientRestoreDelegate { 21 | 22 | private final List playerList; 23 | private final String slot; 24 | 25 | public ClientRestoreDelegate(List playerList, String slot) { 26 | this.playerList = playerList; 27 | this.slot = slot; 28 | } 29 | 30 | public void run() { 31 | MinecraftClient minecraftClient = MinecraftClient.getInstance(); 32 | minecraftClient.execute(() -> { 33 | minecraftClient.world.disconnect(); 34 | minecraftClient.disconnect(new MessageScreen(Text.of("Restore backup"))); 35 | CompletableFuture.runAsync(() -> { 36 | try { 37 | Thread.sleep(1000); 38 | } catch (InterruptedException e) { 39 | throw new RuntimeException(e); 40 | } 41 | minecraftClient.execute(() -> minecraftClient.setScreen(null)); 42 | restoreClient(slot); 43 | Config.TEMP_CONFIG.setIsBackupValue(false); 44 | getDataBase().stopInternalMongoServer(); 45 | minecraftClient.execute(() -> { 46 | Text title = Text.of(Translate.tr("quickbackupmulti.toast.end_title")); 47 | Text content = Text.of(Translate.tr("quickbackupmulti.toast.end_content")); 48 | //#if MC>=11800 49 | SystemToast.show(minecraftClient.toastManager, SystemToast.Type.PERIODIC_NOTIFICATION, title, content); 50 | //#else 51 | //$$ SystemToast.show(minecraftClient.toastManager, SystemToast.Type.WORLD_BACKUP, title, content); 52 | //#endif 53 | }); 54 | }); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/backup/RestoreTask.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.backup; 2 | 3 | import io.github.skydynamic.quickbackupmulti.config.Config; 4 | import io.github.skydynamic.quickbackupmulti.command.QuickBackupMultiCommand; 5 | import net.fabricmc.api.EnvType; 6 | import net.minecraft.server.network.ServerPlayerEntity; 7 | import net.minecraft.text.Text; 8 | 9 | import java.util.List; 10 | import java.util.TimerTask; 11 | 12 | public class RestoreTask extends TimerTask { 13 | 14 | private final EnvType env; 15 | private final List playerList; 16 | private final String slot; 17 | 18 | public RestoreTask(EnvType env, List playerList, String slot) { 19 | this.env = env; 20 | this.playerList = playerList; 21 | this.slot = slot; 22 | } 23 | 24 | @Override 25 | public void run() { 26 | QuickBackupMultiCommand.QbDataHashMap.clear(); 27 | Config.TEMP_CONFIG.setIsBackupValue(true); 28 | if (env == EnvType.SERVER) { 29 | for (ServerPlayerEntity player : playerList) { 30 | player.networkHandler.disconnect(Text.of("Server restore backup")); 31 | } 32 | Config.TEMP_CONFIG.setIsBackupValue(true); 33 | Config.TEMP_CONFIG.server.stop(true); 34 | } else { 35 | //不分到另一个class中执行 会找不到Screen然后炸( 36 | new ClientRestoreDelegate(playerList, slot).run(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/command/MakeCommand.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.command; 2 | 3 | import com.mojang.brigadier.arguments.StringArgumentType; 4 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 5 | import net.minecraft.server.command.CommandManager; 6 | import net.minecraft.server.command.ServerCommandSource; 7 | 8 | import java.text.SimpleDateFormat; 9 | 10 | import static io.github.skydynamic.quickbackupmulti.utils.MakeUtils.make; 11 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 12 | import static net.minecraft.server.command.CommandManager.literal; 13 | 14 | public class MakeCommand { 15 | 16 | static class makeRunnable implements Runnable { 17 | ServerCommandSource commandSource; 18 | String name; 19 | String desc; 20 | 21 | makeRunnable(ServerCommandSource commandSource, String name, String desc) { 22 | this.commandSource = commandSource; 23 | this.name = name; 24 | this.desc = desc; 25 | } 26 | 27 | @Override 28 | public void run() { 29 | long l = System.currentTimeMillis(); 30 | LOGGER.info("Make Backup thread started..."); 31 | make(commandSource, name, desc); 32 | LOGGER.info("Make Backup thread close => {}ms", System.currentTimeMillis() - l); 33 | } 34 | } 35 | 36 | private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HHmmss"); 37 | 38 | public static LiteralArgumentBuilder makeCommand = literal("make").requires(QuickBackupMultiCommand::checkPermission) 39 | .executes(it -> makeSaveBackup(it.getSource(), dateFormat.format(System.currentTimeMillis()), "")) 40 | .then(CommandManager.argument("name", StringArgumentType.string()) 41 | .executes(it -> makeSaveBackup(it.getSource(), StringArgumentType.getString(it, "name"), "")) 42 | .then(CommandManager.argument("desc", StringArgumentType.string()) 43 | .executes(it -> makeSaveBackup(it.getSource(), StringArgumentType.getString(it, "name"), StringArgumentType.getString(it, "desc")))) 44 | ); 45 | // .then(CommandManager.argument("desc", StringArgumentType.string()) 46 | // .executes(it -> makeSaveBackup(it.getSource(), String.valueOf(System.currentTimeMillis()), StringArgumentType.getString(it, "desc")))); 47 | 48 | private static int makeSaveBackup(ServerCommandSource commandSource, String name, String desc) { 49 | new Thread(new makeRunnable(commandSource, name, desc)).start(); 50 | return 1; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/command/QuickBackupMultiCommand.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.command; 2 | 3 | import com.mojang.brigadier.CommandDispatcher; 4 | import com.mojang.brigadier.arguments.IntegerArgumentType; 5 | import com.mojang.brigadier.arguments.StringArgumentType; 6 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 7 | import com.mojang.brigadier.tree.LiteralCommandNode; 8 | //#if MC<=11820 9 | //$$ import com.mojang.brigadier.exceptions.CommandSyntaxException; 10 | //#endif 11 | import io.github.skydynamic.quickbackupmulti.backup.RestoreTask; 12 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 13 | import io.github.skydynamic.quickbackupmulti.config.Config; 14 | import net.fabricmc.api.EnvType; 15 | import net.fabricmc.loader.api.FabricLoader; 16 | import net.minecraft.server.MinecraftServer; 17 | import net.minecraft.server.command.CommandManager; 18 | import net.minecraft.server.command.ServerCommandSource; 19 | import net.minecraft.server.network.ServerPlayerEntity; 20 | import net.minecraft.text.ClickEvent; 21 | import net.minecraft.text.HoverEvent; 22 | import net.minecraft.text.MutableText; 23 | import net.minecraft.text.Text; 24 | import org.apache.commons.lang3.StringUtils; 25 | import org.jetbrains.annotations.NotNull; 26 | 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | import java.util.Timer; 30 | import java.util.concurrent.ConcurrentHashMap; 31 | import java.util.concurrent.Executors; 32 | import java.util.concurrent.ScheduledExecutorService; 33 | import java.util.concurrent.TimeUnit; 34 | import java.util.concurrent.atomic.AtomicInteger; 35 | 36 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getStorager; 37 | import static io.github.skydynamic.quickbackupmulti.command.SettingCommand.settingCommand; 38 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 39 | import static io.github.skydynamic.quickbackupmulti.utils.ListUtils.list; 40 | import static io.github.skydynamic.quickbackupmulti.utils.ListUtils.search; 41 | import static io.github.skydynamic.quickbackupmulti.utils.ListUtils.show; 42 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 43 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.delete; 44 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.getBackupsList; 45 | import static net.minecraft.server.command.CommandManager.literal; 46 | 47 | public class QuickBackupMultiCommand { 48 | public static void RegisterCommand(CommandDispatcher dispatcher) { 49 | LiteralCommandNode QuickBackupMultiShortCommand = dispatcher.register(literal("qb") 50 | .then(literal("list").executes(it -> listSaveBackups(it.getSource(), 1)) 51 | .then(CommandManager.argument("page", IntegerArgumentType.integer(1)) 52 | .executes(it -> listSaveBackups(it.getSource(), IntegerArgumentType.getInteger(it, "page"))))) 53 | 54 | .then(literal("search") 55 | .then(CommandManager.argument("name", StringArgumentType.string()) 56 | .executes(it -> searchSaveBackups(it.getSource(), StringArgumentType.getString(it, "name"))))) 57 | 58 | .then(MakeCommand.makeCommand) 59 | 60 | .then(literal("back").requires(QuickBackupMultiCommand::checkPermission) 61 | .then(CommandManager.argument("name", StringArgumentType.string()) 62 | .executes(it -> restoreSaveBackup(it.getSource(), StringArgumentType.getString(it, "name"))))) 63 | 64 | .then(literal("confirm").requires(QuickBackupMultiCommand::checkPermission) 65 | .executes(it -> { 66 | try { 67 | executeRestore(it.getSource()); 68 | } catch (Exception e) { 69 | LOGGER.info(e.toString()); 70 | } 71 | return 0; 72 | })) 73 | 74 | .then(literal("cancel").requires(QuickBackupMultiCommand::checkPermission) 75 | .executes(it -> cancelRestore(it.getSource()))) 76 | 77 | .then(literal("delete").requires(QuickBackupMultiCommand::checkPermission) 78 | .then(CommandManager.argument("name", StringArgumentType.string()) 79 | .executes(it -> deleteSaveBackup(it.getSource(), StringArgumentType.getString(it, "name"))))) 80 | 81 | 82 | .then(settingCommand) 83 | 84 | .then(literal("show") 85 | .then(CommandManager.argument("name", StringArgumentType.string()) 86 | .executes(it -> showBackupDetail(it.getSource(), StringArgumentType.getString(it, "name"))))) 87 | ); 88 | 89 | dispatcher.register(literal("quickbackupm").redirect(QuickBackupMultiShortCommand)); 90 | } 91 | 92 | public static final ConcurrentHashMap> QbDataHashMap = new ConcurrentHashMap<>(); 93 | 94 | private static int showBackupDetail(ServerCommandSource commandSource, String name) { 95 | Messenger.sendMessage(commandSource, show(name)); 96 | return 1; 97 | } 98 | 99 | private static int searchSaveBackups(ServerCommandSource commandSource, String string) { 100 | List backupsList = getBackupsList(); 101 | List result = backupsList.stream() 102 | .filter(it -> StringUtils.containsIgnoreCase(it, string)) 103 | .toList(); 104 | if (result.isEmpty()) { 105 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.search.fail"))); 106 | } else { 107 | Messenger.sendMessage(commandSource, search(result)); 108 | } 109 | return 1; 110 | } 111 | 112 | private static int deleteSaveBackup(ServerCommandSource commandSource, String name) { 113 | if (delete(name)) Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.delete.success", name))); 114 | else Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.delete.fail", name))); 115 | return 1; 116 | } 117 | 118 | private static int restoreSaveBackup(ServerCommandSource commandSource, String name) { 119 | if (!getStorager().storageExists(name)) { 120 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.restore.fail"))); 121 | return 0; 122 | } 123 | ConcurrentHashMap restoreDataHashMap = new ConcurrentHashMap<>(); 124 | restoreDataHashMap.put("Slot", name); 125 | restoreDataHashMap.put("Timer", new Timer()); 126 | restoreDataHashMap.put("Countdown", Executors.newSingleThreadScheduledExecutor()); 127 | synchronized (QbDataHashMap) { 128 | QbDataHashMap.put("QBM", restoreDataHashMap); 129 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.restore.confirm_hint"))); 130 | return 1; 131 | } 132 | } 133 | 134 | //#if MC>11900 135 | private static void executeRestore(ServerCommandSource commandSource) { 136 | //#else 137 | //$$ private static void executeRestore(ServerCommandSource commandSource) throws CommandSyntaxException { 138 | //#endif 139 | synchronized (QbDataHashMap) { 140 | if (QbDataHashMap.containsKey("QBM")) { 141 | if (!getStorager().storageExists((String) QbDataHashMap.get("QBM").get("Slot"))) { 142 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.restore.fail"))); 143 | QbDataHashMap.clear(); 144 | return; 145 | } 146 | EnvType env = FabricLoader.getInstance().getEnvironmentType(); 147 | String executePlayerName; 148 | if (commandSource.getPlayer() != null) { 149 | executePlayerName = commandSource.getPlayer().getGameProfile().getName(); 150 | } else { 151 | executePlayerName = "Console"; 152 | } 153 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.restore.abort_hint"))); 154 | MinecraftServer server = commandSource.getServer(); 155 | for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { 156 | player.sendMessage(Text.of(tr("quickbackupmulti.restore.countdown.intro", executePlayerName)), false); 157 | } 158 | String slot = (String) QbDataHashMap.get("QBM").get("Slot"); 159 | Config.TEMP_CONFIG.setBackupSlot(slot); 160 | Timer timer = (Timer) QbDataHashMap.get("QBM").get("Timer"); 161 | ScheduledExecutorService countdown = (ScheduledExecutorService) QbDataHashMap.get("QBM").get("Countdown"); 162 | AtomicInteger countDown = new AtomicInteger(11); 163 | List finalPlayerList = new ArrayList<>(server.getPlayerManager().getPlayerList()); 164 | countdown.scheduleAtFixedRate(() -> { 165 | int remaining = countDown.decrementAndGet(); 166 | if (remaining >= 1) { 167 | for (ServerPlayerEntity player : finalPlayerList) { 168 | //#if MC>11900 169 | MutableText content = Messenger.literal(tr("quickbackupmulti.restore.countdown.text", remaining, slot)) 170 | //#else 171 | //$$ BaseText content = (BaseText) Messenger.literal(tr("quickbackupmulti.restore.countdown.text", remaining, slot)) 172 | //#endif 173 | .append(Messenger.literal(tr("quickbackupmulti.restore.countdown.hover")) 174 | .styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/qb cancel"))) 175 | .styled(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.of(tr("quickbackupmulti.restore.countdown.hover")))))); 176 | player.sendMessage(content, false); 177 | LOGGER.info(content.getString()); 178 | } 179 | } else { 180 | countdown.shutdown(); 181 | } 182 | }, 0, 1, TimeUnit.SECONDS); 183 | timer.schedule(new RestoreTask(env, finalPlayerList, slot), 10000); 184 | } else { 185 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.confirm_restore.nothing_to_confirm"))); 186 | } 187 | } 188 | } 189 | 190 | private static int cancelRestore(ServerCommandSource commandSource) { 191 | if (QbDataHashMap.containsKey("QBM")) { 192 | synchronized (QbDataHashMap) { 193 | Timer timer = (Timer) QbDataHashMap.get("QBM").get("Timer"); 194 | ScheduledExecutorService countdown = (ScheduledExecutorService) QbDataHashMap.get("QBM").get("Countdown"); 195 | timer.cancel(); 196 | countdown.shutdown(); 197 | QbDataHashMap.clear(); 198 | Config.TEMP_CONFIG.setIsBackupValue(false); 199 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.restore.abort"))); 200 | } 201 | } else { 202 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.confirm_restore.nothing_to_confirm"))); 203 | } 204 | return 1; 205 | } 206 | 207 | private static int listSaveBackups(ServerCommandSource commandSource, int page) { 208 | MutableText resultText = list(page); 209 | Messenger.sendMessage(commandSource, resultText); 210 | return 1; 211 | } 212 | 213 | public static boolean checkPermission(@NotNull ServerCommandSource source) { 214 | try { 215 | return getPermission(source); 216 | } catch (CommandSyntaxException e) { 217 | return false; 218 | } 219 | } 220 | 221 | private static boolean getPermission(ServerCommandSource source) throws CommandSyntaxException{ 222 | boolean flag = source.hasPermissionLevel(2); 223 | ServerPlayerEntity player; 224 | MinecraftServer server; 225 | if (!flag && (server = source.getServer()).isSingleplayer() && (player = source.getPlayer()) != null) { 226 | flag = server.isHost(player.getGameProfile()); 227 | } 228 | return flag; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/command/SettingCommand.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.command; 2 | 3 | import com.mojang.brigadier.arguments.BoolArgumentType; 4 | import com.mojang.brigadier.arguments.IntegerArgumentType; 5 | import com.mojang.brigadier.arguments.StringArgumentType; 6 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 7 | import io.github.skydynamic.quickbackupmulti.i18n.LangSuggestionProvider; 8 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 9 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 10 | import io.github.skydynamic.quickbackupmulti.utils.ScheduleUtils; 11 | import io.github.skydynamic.quickbackupmulti.config.Config; 12 | import net.minecraft.server.command.CommandManager; 13 | import net.minecraft.server.command.ServerCommandSource; 14 | import net.minecraft.text.Text; 15 | import org.quartz.SchedulerException; 16 | 17 | import java.text.SimpleDateFormat; 18 | 19 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getDataBase; 20 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.setDataBase; 21 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.supportLanguage; 22 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 23 | import static io.github.skydynamic.quickbackupmulti.utils.ScheduleUtils.disableSchedule; 24 | import static io.github.skydynamic.quickbackupmulti.utils.ScheduleUtils.startSchedule; 25 | import static io.github.skydynamic.quickbackupmulti.utils.ScheduleUtils.switchScheduleMode; 26 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.CronUtil.getNextExecutionTime; 27 | import static net.minecraft.server.command.CommandManager.literal; 28 | 29 | public class SettingCommand { 30 | 31 | public static LiteralArgumentBuilder settingCommand = literal("setting").requires(QuickBackupMultiCommand::checkPermission) 32 | .then(literal("lang") 33 | .then(literal("get").executes(it -> getLang(it.getSource()))) 34 | .then(literal("set").requires(QuickBackupMultiCommand::checkPermission) 35 | .then(CommandManager.argument("lang", StringArgumentType.string()) 36 | .suggests(new LangSuggestionProvider()) 37 | .executes(it -> setLang(it.getSource(), StringArgumentType.getString(it, "lang")))))) 38 | .then(literal("schedule") 39 | .then(literal("enable").executes(it -> enableScheduleBackup(it.getSource()))) 40 | .then(literal("disable").executes(it -> disableScheduleBackup(it.getSource()))) 41 | .then(literal("set") 42 | .then(literal("interval") 43 | .then(literal("second") 44 | .then(CommandManager.argument("second", IntegerArgumentType.integer(1)) 45 | .executes(it -> setScheduleInterval( 46 | it.getSource(), 47 | IntegerArgumentType.getInteger(it, "second"), "s") 48 | ) 49 | ) 50 | ).then(literal("minute") 51 | .then(CommandManager.argument("minute", IntegerArgumentType.integer(1)) 52 | .executes(it -> setScheduleInterval( 53 | it.getSource(), 54 | IntegerArgumentType.getInteger(it, "minute"), "m") 55 | ) 56 | ) 57 | ).then(literal("hour") 58 | .then(CommandManager.argument("hour", IntegerArgumentType.integer(1)) 59 | .executes(it -> setScheduleInterval( 60 | it.getSource(), 61 | IntegerArgumentType.getInteger(it, "hour"), "h") 62 | ) 63 | ) 64 | ).then(literal("day") 65 | .then(CommandManager.argument("day", IntegerArgumentType.integer(1)) 66 | .executes(it -> setScheduleInterval( 67 | it.getSource(), 68 | IntegerArgumentType.getInteger(it, "day"), "d") 69 | ) 70 | ) 71 | ) 72 | ) 73 | .then(literal("cron") 74 | .then(CommandManager.argument("cron", StringArgumentType.string()) 75 | .executes(it -> setScheduleCron(it.getSource(), StringArgumentType.getString(it, "cron"))) 76 | ) 77 | ) 78 | 79 | .then(literal("mode") 80 | .then(literal("set") 81 | .then(literal("interval") 82 | .executes(it -> switchMode(it.getSource(), "interval"))) 83 | .then(literal("cron") 84 | .executes(it -> switchMode(it.getSource(), "cron")))) 85 | .then(literal("get").executes(it -> getScheduleMode(it.getSource())))) 86 | ) 87 | .then(literal("get") 88 | .executes(it -> getNextBackupTime(it.getSource())) 89 | ) 90 | ) 91 | .then(literal("dataBase") 92 | .requires(QuickBackupMultiCommand::checkPermission) 93 | .then(literal("useInternalDataBase") 94 | .then(literal("set") 95 | .then(CommandManager.argument("value", BoolArgumentType.bool()) 96 | .executes(it -> setUseInternalDataBase(it.getSource(), BoolArgumentType.getBool(it, "value"))) 97 | ) 98 | ) 99 | ) 100 | ); 101 | 102 | private static int setUseInternalDataBase(ServerCommandSource commandSource, Boolean value) { 103 | if (value != Config.INSTANCE.getUseInternalDataBase()) { 104 | Config.INSTANCE.setUseInternalDataBase(value); 105 | try { 106 | if (value) { 107 | setDataBase(Config.TEMP_CONFIG.worldName); 108 | } else { 109 | getDataBase().stopInternalMongoServer(); 110 | setDataBase(Config.TEMP_CONFIG.worldName); 111 | } 112 | Messenger.sendMessage(commandSource, 113 | Messenger.literal(tr("quickbackupmulti.database.set_success"))); 114 | return 1; 115 | } catch (Exception e) { 116 | Messenger.sendMessage(commandSource, 117 | Messenger.literal(tr("quickbackupmulti.database.set_success_but", e.getMessage()))); 118 | return 0; 119 | } 120 | } else { 121 | Messenger.sendMessage( 122 | commandSource, 123 | Messenger.literal( 124 | tr("quickbackupmulti.database.set_fail", tr("quickbackupmulti.database.value_equal_config", value))) 125 | ); 126 | return 0; 127 | } 128 | 129 | } 130 | 131 | private static int getLang(ServerCommandSource commandSource) { 132 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.lang.get", Config.INSTANCE.getLang()))); 133 | return 1; 134 | } 135 | 136 | private static int setLang(ServerCommandSource commandSource, String lang) { 137 | if (!supportLanguage.contains(lang)) { 138 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.lang.failed"))); 139 | return 0; 140 | } 141 | Translate.handleResourceReload(lang); 142 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.lang.set", lang))); 143 | Config.INSTANCE.setLang(lang); 144 | return 1; 145 | } 146 | 147 | private static int switchMode(ServerCommandSource commandSource, String mode) { 148 | Config.INSTANCE.setScheduleMode(mode); 149 | return switchScheduleMode(commandSource, mode); 150 | } 151 | 152 | private static int setScheduleCron(ServerCommandSource commandSource, String value) { 153 | try { 154 | return ScheduleUtils.setScheduleCron(commandSource, value); 155 | } catch (SchedulerException e) { 156 | return 0; 157 | } 158 | } 159 | 160 | private static int setScheduleInterval(ServerCommandSource commandSource, int value, String type) { 161 | try { 162 | return ScheduleUtils.setScheduleInterval(commandSource, value, type); 163 | } catch (SchedulerException e) { 164 | Messenger.sendMessage(commandSource, 165 | Messenger.literal(tr("quickbackupmulti.schedule.cron.set_fail", e))); 166 | return 0; 167 | } 168 | } 169 | 170 | private static int disableScheduleBackup(ServerCommandSource commandSource) { 171 | return disableSchedule(commandSource); 172 | } 173 | 174 | private static int enableScheduleBackup(ServerCommandSource commandSource) { 175 | try { 176 | Config.INSTANCE.setScheduleBackup(true); 177 | if (Config.TEMP_CONFIG.scheduler != null) Config.TEMP_CONFIG.scheduler.shutdown(); 178 | startSchedule(commandSource); 179 | return 1; 180 | } catch (SchedulerException e) { 181 | Messenger.sendMessage( 182 | commandSource, 183 | Messenger.literal(tr("quickbackupmulti.schedule.enable.fail", e.toString())) 184 | ); 185 | return 0; 186 | } 187 | } 188 | 189 | public static int getScheduleMode(ServerCommandSource commandSource) { 190 | Messenger.sendMessage( 191 | commandSource, 192 | Text.of(tr("quickbackupmulti.schedule.mode.get", Config.INSTANCE.getScheduleMode())) 193 | ); 194 | return 1; 195 | } 196 | 197 | public static int getNextBackupTime(ServerCommandSource commandSource) { 198 | if (Config.INSTANCE.getScheduleBackup()) { 199 | String nextBackupTimeString = ""; 200 | switch (Config.INSTANCE.getScheduleMode()) { 201 | case "cron" : { 202 | nextBackupTimeString = getNextExecutionTime(Config.INSTANCE.getScheduleCron(), false); 203 | break; 204 | } 205 | case "interval" : { 206 | nextBackupTimeString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 207 | .format( 208 | Config.TEMP_CONFIG.latestScheduleExecuteTime + Config.INSTANCE.getScheduleInterval() * 1000L 209 | ); 210 | break; 211 | } 212 | } 213 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.get", nextBackupTimeString))); 214 | return 1; 215 | } else { 216 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.get_fail"))); 217 | return 0; 218 | } 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/config/Config.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.config; 2 | 3 | public class Config { 4 | public static QuickBackupMultiConfig INSTANCE = new QuickBackupMultiConfig(); 5 | public static QbmTempConfig TEMP_CONFIG = new QbmTempConfig(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/config/ConfigStorage.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.config; 2 | 3 | import io.github.skydynamic.increment.storage.lib.Interface.IConfig; 4 | 5 | import java.util.ArrayList; 6 | 7 | public class ConfigStorage implements IConfig { 8 | @Ignore 9 | public static final ConfigStorage DEFAULT = new ConfigStorage( 10 | new ArrayList<>(), 11 | "zh_cn", 12 | false, 13 | "* * 0/4 * * ?", 14 | 14400, 15 | "interval", 16 | true, 17 | "mongodb://localhost:27017", 18 | "QuickBackupMulti" 19 | ); 20 | 21 | private ArrayList ignoredFiles; 22 | private String lang; 23 | private boolean scheduleBackup; 24 | private String scheduleCron; 25 | private int scheduleInterval; 26 | private String scheduleMode; 27 | 28 | private boolean useInternalDataBase; 29 | private String mongoDBUri; 30 | private String storagePath; 31 | 32 | public ConfigStorage( 33 | ArrayList IgnoredFiles, 34 | String lang, 35 | boolean scheduleBackup, 36 | String scheduleCron, 37 | int scheduleInterval, 38 | String scheduleMode, 39 | boolean useInternalDataBase, 40 | String mongoDBUri, 41 | String storagePath) { 42 | this.ignoredFiles = IgnoredFiles; 43 | this.lang = lang; 44 | this.scheduleBackup = scheduleBackup; 45 | this.scheduleCron = scheduleCron; 46 | this.scheduleInterval = scheduleInterval; 47 | this.scheduleMode = scheduleMode; 48 | this.useInternalDataBase = useInternalDataBase; 49 | this.mongoDBUri = mongoDBUri; 50 | this.storagePath = storagePath; 51 | } 52 | 53 | public ArrayList getIgnoredFiles() { 54 | return ignoredFiles; 55 | } 56 | 57 | public void setIgnoredFiles(ArrayList ignoredFiles) { 58 | this.ignoredFiles = ignoredFiles; 59 | } 60 | 61 | public String getLang() { 62 | return lang; 63 | } 64 | 65 | public void setLang(String lang) { 66 | this.lang = lang; 67 | } 68 | 69 | public boolean isScheduleBackup() { 70 | return scheduleBackup; 71 | } 72 | 73 | public void setScheduleBackup(boolean scheduleBackup) { 74 | this.scheduleBackup = scheduleBackup; 75 | } 76 | 77 | public String getScheduleCron() { 78 | return scheduleCron; 79 | } 80 | 81 | public void setScheduleCron(String scheduleCron) { 82 | this.scheduleCron = scheduleCron; 83 | } 84 | 85 | public int getScheduleInterval() { 86 | return scheduleInterval; 87 | } 88 | 89 | public void setScheduleInterval(int scheduleInterval) { 90 | this.scheduleInterval = scheduleInterval; 91 | } 92 | 93 | public String getScheduleMode() { 94 | return scheduleMode; 95 | } 96 | 97 | public void setScheduleMode(String scheduleMode) { 98 | this.scheduleMode = scheduleMode; 99 | } 100 | 101 | @Override 102 | public void setUseInternalDataBase(boolean b) { 103 | this.useInternalDataBase = b; 104 | } 105 | 106 | @Override 107 | public void setMongoDBUri(String s) { 108 | this.mongoDBUri = s; 109 | } 110 | 111 | @Override 112 | public boolean getUseInternalDataBase() { 113 | return this.useInternalDataBase; 114 | } 115 | 116 | @Override 117 | public String getMongoDBUri() { 118 | return this.mongoDBUri; 119 | } 120 | 121 | @Override 122 | public String getStoragePath() { 123 | return this.storagePath; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/config/Ignore.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.config; 2 | 3 | 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Target(ElementType.FIELD) 11 | @interface Ignore{ 12 | 13 | } -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/config/QbmTempConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.config; 2 | 3 | import net.fabricmc.api.EnvType; 4 | import net.minecraft.server.MinecraftServer; 5 | import org.quartz.Scheduler; 6 | 7 | public class QbmTempConfig { 8 | public Boolean isBackup = false; 9 | public MinecraftServer server; 10 | public String backupSlot; 11 | public EnvType env; 12 | public String worldName; 13 | public Scheduler scheduler; 14 | public long latestScheduleExecuteTime; 15 | 16 | public void setIsBackupValue(Boolean value) { 17 | this.isBackup = value; 18 | } 19 | 20 | public void setServerValue(MinecraftServer server) { 21 | this.server = server; 22 | } 23 | 24 | public void setBackupSlot(String slot) { 25 | this.backupSlot = slot; 26 | } 27 | 28 | public void setEnv(EnvType env) { 29 | this.env = env; 30 | } 31 | 32 | public void setWorldName(String worldName) { 33 | this.worldName = worldName; 34 | } 35 | 36 | public void setScheduler(Scheduler scheduler) { 37 | this.scheduler = scheduler; 38 | } 39 | 40 | public void setLatestScheduleExecuteTime(long time) { 41 | this.latestScheduleExecuteTime = time; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/config/QuickBackupMultiConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.config; 2 | 3 | import io.github.skydynamic.increment.storage.lib.util.IndexUtil; 4 | import io.github.skydynamic.quickbackupmulti.QbmConstant; 5 | 6 | import java.io.File; 7 | import java.io.FileReader; 8 | import java.io.FileWriter; 9 | import java.lang.reflect.Field; 10 | import java.nio.file.Path; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | 14 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.gson; 15 | 16 | public class QuickBackupMultiConfig { 17 | private final Object lock = new Object(); 18 | private final Path configPath = QbmConstant.pathGetter.getConfigPath(); 19 | private ConfigStorage configStorage; 20 | File path = configPath.toFile(); 21 | File config = configPath.resolve("QuickBackupMulti.json").toFile(); 22 | 23 | public void load() { 24 | synchronized (lock) { 25 | try { 26 | if (!path.exists() || !path.isDirectory()) { 27 | path.mkdirs(); 28 | } 29 | if (!config.exists()) { 30 | saveModifiedConfig(ConfigStorage.DEFAULT); 31 | } 32 | FileReader reader = new FileReader(config); 33 | ConfigStorage result = gson.fromJson(reader, ConfigStorage.class); 34 | this.configStorage = fixFields(result, ConfigStorage.DEFAULT); 35 | saveModifiedConfig(this.configStorage); 36 | reader.close(); 37 | } catch (Exception e) { 38 | throw new RuntimeException(e); 39 | } 40 | } 41 | } 42 | 43 | private void saveModifiedConfig(ConfigStorage c) { 44 | synchronized (lock) { 45 | try { 46 | if (config.exists()) config.delete(); 47 | if (!config.exists()) config.createNewFile(); 48 | FileWriter writer = new FileWriter(config); 49 | ConfigStorage fixConfig = fixFields(c, ConfigStorage.DEFAULT); 50 | gson.toJson(fixConfig, writer); 51 | writer.close(); 52 | IndexUtil.setConfig(fixConfig); 53 | } catch (Exception e) { 54 | throw new RuntimeException(e); 55 | } 56 | } 57 | } 58 | 59 | private ConfigStorage fixFields(ConfigStorage t, ConfigStorage defaultVal) { 60 | if (t == null) { 61 | throw new NullPointerException(); 62 | } 63 | if (t.equals(defaultVal)) { 64 | return t; 65 | } 66 | try { 67 | Class clazz = t.getClass(); 68 | for (Field declaredField : clazz.getDeclaredFields()) { 69 | if (Arrays.stream(declaredField.getDeclaredAnnotations()).anyMatch(it -> it.annotationType() == Ignore.class)) 70 | continue; 71 | declaredField.setAccessible(true); 72 | Object value = declaredField.get(t); 73 | Object dv = declaredField.get(defaultVal); 74 | if (value == null) { 75 | declaredField.set(t, dv); 76 | } 77 | } 78 | return t; 79 | } catch (Exception e) { 80 | throw new RuntimeException(e); 81 | } 82 | } 83 | 84 | public ConfigStorage getConfigStorage() { 85 | synchronized (lock) { 86 | return fixFields(configStorage, ConfigStorage.DEFAULT); 87 | } 88 | } 89 | 90 | public void setConfigStorage(ConfigStorage configStorage) { 91 | synchronized (lock) { 92 | this.configStorage = configStorage; 93 | saveModifiedConfig(configStorage); 94 | } 95 | } 96 | 97 | public List getIgnoredFiles() { 98 | synchronized (lock) { 99 | List list = configStorage.getIgnoredFiles(); 100 | list.add("session.lock"); 101 | return list; 102 | } 103 | } 104 | 105 | public String getLang() { 106 | synchronized (lock) { 107 | return configStorage.getLang(); 108 | } 109 | } 110 | 111 | public boolean getScheduleBackup() { 112 | synchronized (lock) { 113 | return configStorage.isScheduleBackup(); 114 | } 115 | } 116 | 117 | public String getScheduleCron() { 118 | synchronized (lock) { 119 | return configStorage.getScheduleCron(); 120 | } 121 | } 122 | 123 | public int getScheduleInterval() { 124 | synchronized (lock) { 125 | return configStorage.getScheduleInterval(); 126 | } 127 | } 128 | 129 | public String getScheduleMode() { 130 | synchronized (lock) { 131 | return configStorage.getScheduleMode(); 132 | } 133 | } 134 | 135 | public boolean getUseInternalDataBase() { 136 | synchronized (lock) { 137 | return configStorage.getUseInternalDataBase(); 138 | } 139 | } 140 | 141 | public String getMongoDBUri() { 142 | synchronized (lock) { 143 | return configStorage.getMongoDBUri(); 144 | } 145 | } 146 | 147 | public String getStoragePath() { 148 | synchronized (lock) { 149 | return configStorage.getStoragePath(); 150 | } 151 | } 152 | 153 | public void setLang(String lang) { 154 | synchronized (lock) { 155 | configStorage.setLang(lang); 156 | saveModifiedConfig(configStorage); 157 | } 158 | } 159 | 160 | public void setScheduleCron(String value) { 161 | synchronized (lock) { 162 | configStorage.setScheduleCron(value); 163 | saveModifiedConfig(configStorage); 164 | } 165 | } 166 | 167 | public void setScheduleInterval(int value) { 168 | synchronized (lock) { 169 | configStorage.setScheduleInterval(value); 170 | saveModifiedConfig(configStorage); 171 | } 172 | } 173 | 174 | public void setScheduleBackup(boolean value) { 175 | synchronized (lock) { 176 | configStorage.setScheduleBackup(value); 177 | saveModifiedConfig(configStorage); 178 | } 179 | } 180 | 181 | public void setScheduleMode(String mode) { 182 | synchronized (lock) { 183 | configStorage.setScheduleMode(mode); 184 | saveModifiedConfig(configStorage); 185 | } 186 | } 187 | 188 | public void setUseInternalDataBase(boolean value) { 189 | synchronized (lock) { 190 | configStorage.setUseInternalDataBase(value); 191 | saveModifiedConfig(configStorage); 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/i18n/LangSuggestionProvider.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.i18n; 2 | 3 | import com.mojang.brigadier.context.CommandContext; 4 | import com.mojang.brigadier.suggestion.SuggestionProvider; 5 | import com.mojang.brigadier.suggestion.Suggestions; 6 | import com.mojang.brigadier.suggestion.SuggestionsBuilder; 7 | import net.minecraft.server.command.ServerCommandSource; 8 | 9 | import java.util.concurrent.CompletableFuture; 10 | 11 | public class LangSuggestionProvider implements SuggestionProvider { 12 | @Override 13 | public CompletableFuture getSuggestions(CommandContext context, SuggestionsBuilder builder) { 14 | for (String lang : Translate.supportLanguage) { 15 | builder.suggest(lang); 16 | } 17 | return builder.buildFuture(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/i18n/Translate.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.i18n; 2 | 3 | import org.yaml.snakeyaml.Yaml; 4 | import org.apache.commons.io.IOUtils; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.*; 10 | 11 | public class Translate { 12 | 13 | private static Map translateMap = new HashMap<>(); 14 | public static final Collection supportLanguage = List.of("zh_cn", "en_us"); 15 | 16 | public static Map getTranslationFromResourcePath(String lang) { 17 | InputStream langFile = Translate.class.getClassLoader().getResourceAsStream("assets/quickbackupmulti/lang/%s.yml".formatted(lang)); 18 | if (langFile == null) { 19 | return Collections.emptyMap(); 20 | } 21 | String yamlData; 22 | try { 23 | yamlData = IOUtils.toString(langFile, StandardCharsets.UTF_8); 24 | } catch (IOException e) { 25 | return Collections.emptyMap(); 26 | } 27 | Yaml yaml = new Yaml(); 28 | Map obj = yaml.load(yamlData); 29 | return addMapToResult("", obj); 30 | } 31 | 32 | public static void handleResourceReload(String lang) { 33 | translateMap = getTranslationFromResourcePath(lang); 34 | } 35 | 36 | public static String translate(String key, Object... args) { 37 | String fmt = translateMap.getOrDefault(key, key); 38 | if (!translateMap.containsKey(key)) return key; 39 | return String.format(fmt, args); 40 | } 41 | 42 | public static String tr(String k, Object... o) { 43 | return translate(k, o); 44 | } 45 | 46 | @SuppressWarnings("unchecked") 47 | private static Map addMapToResult(String prefix, Map map) { 48 | Map resultMap = new HashMap<>(); 49 | for (Map.Entry entry : map.entrySet()) { 50 | String key = entry.getKey(); 51 | Object value = entry.getValue(); 52 | String newPrefix = prefix.isEmpty() ? key : prefix + "." + key; 53 | if (value instanceof Map) { 54 | resultMap.putAll(addMapToResult(newPrefix, (Map) value)); 55 | } else { 56 | resultMap.put(newPrefix, value.toString()); 57 | } 58 | } 59 | return resultMap; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/mixin/MinecraftClientMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.mixin; 2 | 3 | import io.github.skydynamic.quickbackupmulti.config.Config; 4 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 5 | import net.minecraft.client.MinecraftClient; 6 | import net.minecraft.client.toast.SystemToast; 7 | import net.minecraft.client.toast.ToastManager; 8 | import net.minecraft.text.Text; 9 | import org.spongepowered.asm.mixin.Final; 10 | import org.spongepowered.asm.mixin.Mixin; 11 | import org.spongepowered.asm.mixin.Shadow; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 15 | 16 | @Mixin(MinecraftClient.class) 17 | public class MinecraftClientMixin { 18 | @Shadow @Final 19 | public ToastManager toastManager; 20 | 21 | @Inject(method = "setScreen", at = @At("RETURN")) 22 | private void inj(CallbackInfo ci) { 23 | if (Config.TEMP_CONFIG.isBackup) { 24 | Text title = Text.of(Translate.tr("quickbackupmulti.toast.start_title")); 25 | Text content = Text.of(Translate.tr("quickbackupmulti.toast.start_content")); 26 | //#if MC>=11800 27 | SystemToast.show(this.toastManager, SystemToast.Type.PERIODIC_NOTIFICATION, title, content); 28 | //#else 29 | //$$ SystemToast.show(this.toastManager, SystemToast.Type.WORLD_BACKUP, title, content); 30 | //#endif 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/mixin/MinecraftServer_ClientMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.mixin; 2 | 3 | import io.github.skydynamic.quickbackupmulti.QbmConstant; 4 | import io.github.skydynamic.quickbackupmulti.config.Config; 5 | import net.fabricmc.api.EnvType; 6 | import net.fabricmc.api.Environment; 7 | import net.minecraft.server.MinecraftServer; 8 | import net.minecraft.util.WorldSavePath; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Inject; 12 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 13 | 14 | import java.nio.file.Path; 15 | 16 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getDataBase; 17 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.setDataBase; 18 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.createBackupDir; 19 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.ScheduleUtils.shutdownSchedule; 20 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.ScheduleUtils.startSchedule; 21 | 22 | @Environment(EnvType.CLIENT) 23 | @Mixin(MinecraftServer.class) 24 | public class MinecraftServer_ClientMixin { 25 | @Inject(method = "loadWorld", at = @At("RETURN")) 26 | private void initQuickBackupMultiClient(CallbackInfo ci) { 27 | MinecraftServer server = (MinecraftServer) (Object) this; 28 | Config.TEMP_CONFIG.setServerValue(server); 29 | Path saveDirectoryPath = server.getSavePath(WorldSavePath.ROOT); 30 | String worldName = saveDirectoryPath.getParent().getFileName().toString(); 31 | Config.TEMP_CONFIG.setWorldName(worldName); 32 | Path backupDir = Path.of(QbmConstant.pathGetter.getGamePath() + "/QuickBackupMulti/").resolve(worldName); 33 | createBackupDir(backupDir); 34 | setDataBase(worldName); 35 | startSchedule(); 36 | } 37 | 38 | @Inject(method = "shutdown", at = @At("HEAD")) 39 | private void stopSchedule(CallbackInfo ci) { 40 | shutdownSchedule(); 41 | if (!Config.TEMP_CONFIG.isBackup) getDataBase().stopInternalMongoServer(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/mixin/MinecraftServer_ServerMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.mixin; 2 | 3 | import io.github.skydynamic.quickbackupmulti.QbmConstant; 4 | import io.github.skydynamic.quickbackupmulti.config.Config; 5 | import net.fabricmc.api.Environment; 6 | import net.fabricmc.api.EnvType; 7 | import net.minecraft.server.MinecraftServer; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.Inject; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 12 | 13 | import java.nio.file.Path; 14 | 15 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getDataBase; 16 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.setDataBase; 17 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.createBackupDir; 18 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.ScheduleUtils.shutdownSchedule; 19 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.ScheduleUtils.startSchedule; 20 | 21 | @Environment(EnvType.SERVER) 22 | @Mixin(MinecraftServer.class) 23 | public class MinecraftServer_ServerMixin { 24 | @Inject(method = "", at = @At("TAIL")) 25 | private void setServer(CallbackInfo ci) { 26 | Config.TEMP_CONFIG.setServerValue((MinecraftServer)(Object)this); 27 | } 28 | 29 | @Inject(method = "loadWorld", at = @At("RETURN")) 30 | private void initQuickBackupMulti(CallbackInfo ci) { 31 | Path backupDir = Path.of(QbmConstant.pathGetter.getGamePath() + "/QuickBackupMulti/"); 32 | Config.TEMP_CONFIG.setWorldName(""); 33 | createBackupDir(backupDir); 34 | setDataBase("server"); 35 | startSchedule(); 36 | } 37 | 38 | @Inject(method = "shutdown", at = @At("HEAD")) 39 | private void injectShutDown(CallbackInfo ci) { 40 | shutdownSchedule(); 41 | if (!Config.TEMP_CONFIG.isBackup) getDataBase().stopInternalMongoServer(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/mixin/TitleScreenMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.mixin; 2 | 3 | import io.github.skydynamic.quickbackupmulti.config.Config; 4 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 5 | import net.minecraft.client.gui.Element; 6 | 7 | import net.minecraft.client.gui.screen.TitleScreen; 8 | //#if MC>=11900 9 | import net.minecraft.client.gui.tooltip.Tooltip; 10 | //#endif 11 | import net.minecraft.client.gui.widget.ButtonWidget; 12 | 13 | import net.minecraft.client.gui.widget.ClickableWidget; 14 | import net.minecraft.text.TranslatableTextContent; 15 | import org.spongepowered.asm.mixin.Mixin; 16 | import org.spongepowered.asm.mixin.injection.At; 17 | import org.spongepowered.asm.mixin.injection.Inject; 18 | import org.spongepowered.asm.mixin.injection.ModifyArg; 19 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 20 | 21 | @Mixin(TitleScreen.class) 22 | public class TitleScreenMixin { 23 | @ModifyArg( 24 | method = "initWidgetsNormal", 25 | at = @At( 26 | value = "INVOKE", 27 | target = "Lnet/minecraft/client/gui/screen/TitleScreen;addDrawableChild(Lnet/minecraft/client/gui/Element;)Lnet/minecraft/client/gui/Element;", 28 | ordinal = 0 29 | ) 30 | ) 31 | private Element setSinglePlayerButton(Element element) { 32 | if (Config.TEMP_CONFIG.isBackup) { 33 | //#if MC>=11900 34 | ((ButtonWidget) element).setTooltip(Tooltip.of(Messenger.literal("Restore now..."))); 35 | //#endif 36 | ((ButtonWidget) element).active = false; 37 | } 38 | return element; 39 | } 40 | 41 | @Inject(method = "tick", at = @At("RETURN")) 42 | private void setButtonActive(CallbackInfo ci) { 43 | if (!Config.TEMP_CONFIG.isBackup) { 44 | TitleScreen screen = (TitleScreen) (Object) this; 45 | screen.children() 46 | .stream() 47 | .filter(e -> e instanceof ButtonWidget) 48 | //#if MC>=11900 49 | .filter(it -> ((ButtonWidget) it).getMessage().getContent() instanceof TranslatableTextContent) 50 | //#else 51 | //$$ .filter(it -> ((ClickableWidget) it).getMessage() instanceof TranslatableText) 52 | //#endif 53 | .filter(it -> ((ClickableWidget) it).getMessage().toString().contains("menu.singleplayer")) 54 | .forEach(e -> { 55 | ((ButtonWidget) e).active = true; 56 | //#if MC>=11900 57 | ((ButtonWidget) e).setTooltip(null); 58 | //#endif 59 | }); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/screen/ConfigScreen.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.screen; 2 | 3 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 4 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 5 | import io.github.skydynamic.quickbackupmulti.config.ConfigStorage; 6 | 7 | import net.fabricmc.api.EnvType; 8 | import net.fabricmc.api.Environment; 9 | 10 | import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; 11 | import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; 12 | //#if MC>=12005 13 | //$$ import io.github.skydynamic.quickbackupmulti.Packets; 14 | //#endif 15 | //#if MC>=12000 16 | import net.minecraft.client.gui.DrawContext; 17 | //#else 18 | //$$ import net.minecraft.client.util.math.MatrixStack; 19 | //#endif 20 | import net.minecraft.client.gui.screen.Screen; 21 | import net.minecraft.client.gui.widget.ButtonWidget; 22 | import net.minecraft.client.gui.widget.TextFieldWidget; 23 | import net.minecraft.network.PacketByteBuf; 24 | import net.minecraft.text.Text; 25 | 26 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.SAVE_CONFIG_PACKET_ID; 27 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.gson; 28 | import static io.github.skydynamic.quickbackupmulti.screen.ScreenUtils.buildButton; 29 | 30 | @Environment(EnvType.CLIENT) 31 | public class ConfigScreen extends Screen { 32 | private final Screen parent; 33 | private TextFieldWidget langTextField; 34 | 35 | int totalWidth = 2 * 100 + 20; 36 | 37 | public ConfigScreen(Screen parent, ConfigStorage config) { 38 | super(Messenger.literal("ConfigScreen")); 39 | this.parent = parent; 40 | TempConfig.tempConfig.setConfig(config); 41 | } 42 | 43 | @Override 44 | protected void init() { 45 | // save & close 46 | ButtonWidget saveConfigButton = buildButton(Translate.tr("quickbackupmulti.config_page.save_button"), 47 | width / 2 - totalWidth / 2, height - 70, 105, 20, 48 | (button) -> { 49 | TempConfig.tempConfig.config.setLang(langTextField.getText()); 50 | //#if MC>=12005 51 | //$$ ClientPlayNetworking.send(new Packets.SaveConfigPacket(gson.toJson(TempConfig.tempConfig.config))); 52 | //#else 53 | PacketByteBuf sendBuf = PacketByteBufs.create(); 54 | sendBuf.writeString(gson.toJson(TempConfig.tempConfig.config)); 55 | ClientPlayNetworking.send(SAVE_CONFIG_PACKET_ID, sendBuf); 56 | //#endif 57 | }); 58 | ButtonWidget closeScreenButton = buildButton(Translate.tr("quickbackupmulti.config_page.close_button"), 59 | width / 2 - totalWidth / 2 + 135, height - 70, 105, 20, (button) -> this.close()); 60 | 61 | ButtonWidget openScheduleConfigScreenButton = buildButton( 62 | Translate.tr("quickbackupmulti.config_page.open_schedule_config_button"), 63 | width / 2 - 100, 55, 200, 20, (button) -> client.setScreen(new ScheduleConfigScreen(this)) 64 | ); 65 | 66 | langTextField = new TextFieldWidget(textRenderer, width / 2 - 38, 90, 105, 15, Text.of("")); 67 | langTextField.setText(TempConfig.tempConfig.config.getLang()); 68 | langTextField.setMaxLength(5); 69 | langTextField.setEditable(true); 70 | 71 | addChild(langTextField); 72 | addChild(saveConfigButton); 73 | addChild(closeScreenButton); 74 | addChild(openScheduleConfigScreenButton); 75 | } 76 | 77 | @Override 78 | //#if MC>=12000 79 | public void render(DrawContext context, int mouseX, int mouseY, float delta) { 80 | //#else 81 | //$$ public void render(MatrixStack context, int mouseX, int mouseY, float delta) { 82 | //#endif 83 | //#if MC>=12003 84 | //$$ this.renderBackground(context, mouseX, mouseY, delta); 85 | //#else 86 | this.renderBackground(context); 87 | //#endif 88 | drawCenteredTextWithShadow( 89 | context, 90 | Translate.tr("quickbackupmulti.config_page.title"), 91 | width / 2, 92 | 20, 93 | 0xFFFFFF 94 | ); 95 | drawCenteredTextWithShadow( 96 | context, 97 | Translate.tr("quickbackupmulti.config_page.lang"), 98 | width / 2 - 70, 99 | 93, 100 | 0xFFFFFF 101 | ); 102 | langTextField.render(context, mouseX, mouseY, delta); 103 | super.render(context, mouseX, mouseY, delta); 104 | } 105 | 106 | @Override 107 | public void close() { 108 | client.execute(() -> client.setScreen(parent)); 109 | TempConfig.tempConfig.config = null; 110 | } 111 | 112 | //#if MC>=12000 113 | private void drawCenteredTextWithShadow(DrawContext context, String text, int x, int y, int color) { 114 | context.drawCenteredTextWithShadow(textRenderer, text, x, y, color); 115 | //#else 116 | //$$ private void drawCenteredTextWithShadow(MatrixStack context, String text, int x, int y, int color) { 117 | //$$ drawCenteredTextWithShadow(context, textRenderer, text, x, y, color); 118 | //#endif 119 | } 120 | 121 | //#if MC>=11701 122 | private void addChild(ButtonWidget value) { 123 | addDrawableChild(value); 124 | } 125 | 126 | private void addChild(TextFieldWidget value) { 127 | addDrawableChild(value); 128 | } 129 | //#endif 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/screen/ScheduleConfigScreen.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.screen; 2 | 3 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 4 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 5 | 6 | import net.fabricmc.api.EnvType; 7 | import net.fabricmc.api.Environment; 8 | //#if MC>=12000 9 | import net.minecraft.client.gui.DrawContext; 10 | //#else 11 | //$$ import net.minecraft.client.util.math.MatrixStack; 12 | //#endif 13 | import net.minecraft.client.gui.screen.Screen; 14 | import net.minecraft.client.gui.widget.ButtonWidget; 15 | import net.minecraft.client.gui.widget.TextFieldWidget; 16 | import net.minecraft.text.Text; 17 | 18 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 19 | import static io.github.skydynamic.quickbackupmulti.screen.ScreenUtils.buildButton; 20 | import static io.github.skydynamic.quickbackupmulti.screen.TempConfig.tempConfig; 21 | 22 | @Environment(EnvType.CLIENT) 23 | public class ScheduleConfigScreen extends Screen { 24 | private final Screen parent; 25 | private TextFieldWidget cronTextField; 26 | private TextFieldWidget intervalTextField; 27 | 28 | public ScheduleConfigScreen(Screen parent) { 29 | super(Messenger.literal("ScheduleSetting")); 30 | this.parent = parent; 31 | } 32 | 33 | @Override 34 | protected void init() { 35 | int totalWidth = 2 * 120 + 20; 36 | 37 | ButtonWidget backButton = buildButton( 38 | Translate.tr("quickbackupmulti.config_page.back"), 39 | width / 2 - 100, 40 | height - 70, 200, 20, 41 | (button) -> { 42 | tempConfig.config.setScheduleCron(cronTextField.getText()); 43 | tempConfig.config.setScheduleInterval(Integer.parseInt(intervalTextField.getText())); 44 | this.client.setScreen(parent); 45 | }); 46 | 47 | ButtonWidget enableScheduleBackupButton = buildButton( 48 | Translate.tr( 49 | "quickbackupmulti.config_page.schedule.switch", 50 | tempConfig.config.isScheduleBackup() ? "On" : "Off" 51 | ), 52 | width / 2 - totalWidth / 2, 50, 120, 20, 53 | (button) -> { 54 | if (button.getMessage().toString().contains("On")) { 55 | button.setMessage( 56 | Messenger.literal(Translate.tr("quickbackupmulti.config_page.schedule.switch", "Off")) 57 | ); 58 | tempConfig.config.setScheduleBackup(false); 59 | } else { 60 | button.setMessage( 61 | Messenger.literal(Translate.tr("quickbackupmulti.config_page.schedule.switch", "On")) 62 | ); 63 | tempConfig.config.setScheduleBackup(true); 64 | } 65 | }); 66 | ButtonWidget switchScheduleModeButton = buildButton( 67 | tr("quickbackupmulti.config_page.schedule.mode.switch", tempConfig.config.getScheduleMode()), 68 | width / 2 - totalWidth / 2 + 105 + 20, 50, 120, 20, 69 | (button) -> { 70 | if (button.getMessage().toString().contains("interval")) { 71 | button.setMessage( 72 | Messenger.literal(Translate.tr("quickbackupmulti.config_page.schedule.mode.switch", "cron")) 73 | ); 74 | tempConfig.config.setScheduleMode("cron"); 75 | } else { 76 | button.setMessage( 77 | Messenger.literal(Translate.tr("quickbackupmulti.config_page.schedule.mode.switch", "interval")) 78 | ); 79 | tempConfig.config.setScheduleMode("interval"); 80 | } 81 | }); 82 | 83 | cronTextField = new TextFieldWidget(textRenderer, width / 2, 80, 105, 15, Text.of("")); 84 | cronTextField.setText(tempConfig.config.getScheduleCron()); 85 | cronTextField.setEditable(true); 86 | 87 | intervalTextField = new TextFieldWidget(textRenderer, width / 2, 120, 105, 15, Text.of("")) { 88 | @Override 89 | public void write(String text) { 90 | if (text.matches("\\d*")) { 91 | super.write(text); 92 | } 93 | } 94 | }; 95 | intervalTextField.setText(String.valueOf(tempConfig.config.getScheduleInterval())); 96 | intervalTextField.setEditable(true); 97 | 98 | addChild(cronTextField); 99 | addChild(intervalTextField); 100 | addChild(enableScheduleBackupButton); 101 | addChild(switchScheduleModeButton); 102 | addChild(backButton); 103 | } 104 | 105 | @Override 106 | //#if MC>=12000 107 | public void render(DrawContext context, int mouseX, int mouseY, float delta) { 108 | //#else 109 | //$$ public void render(MatrixStack context, int mouseX, int mouseY, float delta) { 110 | //#endif 111 | //#if MC>=12003 112 | //$$ this.renderBackground(context, mouseX, mouseY, delta); 113 | //#else 114 | this.renderBackground(context); 115 | //#endif 116 | drawCenteredTextWithShadow( 117 | context, 118 | Translate.tr("quickbackupmulti.config_page.title"), 119 | width / 2, 120 | 20, 121 | 0xFFFFFF 122 | ); 123 | drawCenteredTextWithShadow( 124 | context, 125 | Translate.tr("quickbackupmulti.config_page.schedule.cron"), 126 | width / 2 - 46, 127 | 83, 128 | 0xFFFFFF 129 | ); 130 | drawCenteredTextWithShadow( 131 | context, 132 | Translate.tr("quickbackupmulti.config_page.schedule.interval"), 133 | width / 2 - 45, 134 | 123, 135 | 0xFFFFFF 136 | ); 137 | cronTextField.render(context, mouseX, mouseY, delta); 138 | intervalTextField.render(context, mouseX, mouseY, delta); 139 | super.render(context, mouseX, mouseY, delta); 140 | } 141 | 142 | @Override 143 | public void close() { 144 | client.execute(() -> client.setScreen(parent)); 145 | } 146 | 147 | //#if MC>=12000 148 | private void drawCenteredTextWithShadow(DrawContext context, String text, int x, int y, int color) { 149 | context.drawCenteredTextWithShadow(textRenderer, text, x, y, color); 150 | //#else 151 | //$$ private void drawCenteredTextWithShadow(MatrixStack context, String text, int x, int y, int color) { 152 | //$$ drawCenteredTextWithShadow(context, textRenderer, text, x, y, color); 153 | //#endif 154 | } 155 | 156 | //#if MC>=11701 157 | private void addChild(ButtonWidget value) { 158 | addDrawableChild(value); 159 | } 160 | 161 | private void addChild(TextFieldWidget value) { 162 | addDrawableChild(value); 163 | } 164 | //#endif 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/screen/ScreenUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.screen; 2 | 3 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 4 | import net.minecraft.client.gui.widget.ButtonWidget; 5 | 6 | public class ScreenUtils { 7 | public static ButtonWidget buildButton(String text, int x, int y, int width, int height, ButtonWidget.PressAction action) { 8 | //#if MC>=11903 9 | return ButtonWidget.builder(Messenger.literal(text), action).dimensions(x, y, width, height).build(); 10 | //#else 11 | //$$ return new ButtonWidget(x, y, width, height, Messenger.literal(text), action); 12 | //#endif 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/screen/TempConfig.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.screen; 2 | 3 | import io.github.skydynamic.quickbackupmulti.config.ConfigStorage; 4 | import net.fabricmc.api.EnvType; 5 | import net.fabricmc.api.Environment; 6 | 7 | @Environment(EnvType.CLIENT) 8 | public class TempConfig { 9 | public ConfigStorage config; 10 | public static TempConfig tempConfig = new TempConfig(); 11 | public void setConfig(ConfigStorage config) { 12 | this.config = config; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/utils/ListUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils; 2 | 3 | import io.github.skydynamic.increment.storage.lib.database.index.type.StorageInfo; 4 | import net.minecraft.text.ClickEvent; 5 | import net.minecraft.text.HoverEvent; 6 | import net.minecraft.text.MutableText; 7 | import net.minecraft.text.Text; 8 | import net.minecraft.util.Formatting; 9 | import org.apache.commons.io.FileUtils; 10 | 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.nio.file.Path; 14 | import java.text.SimpleDateFormat; 15 | import java.util.List; 16 | 17 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 18 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getDataBase; 19 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getStorager; 20 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 21 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.getBackupDir; 22 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.getBackupsList; 23 | 24 | public class ListUtils { 25 | private static long getDirSize(File dir) { 26 | return FileUtils.sizeOf(dir); 27 | } 28 | 29 | public static String truncateString(String str, int maxLength) { 30 | if (str.length() > maxLength) { 31 | return str.substring(0, maxLength - 3) + "..."; 32 | } else { 33 | return str; 34 | } 35 | } 36 | 37 | private static int getPageCount(ListbackupsDirList, int page) { 38 | int size = backupsDirList.size(); 39 | if (!(size < 5*page)) { 40 | return 5; 41 | } else if (size < 5*page && (size < 5 && size > 0)){ 42 | return size; 43 | } else { 44 | return Math.max(size - 5 * (page - 1), 0); 45 | } 46 | } 47 | 48 | public static int getTotalPage(List backupsList) { 49 | return (int) Math.ceil(backupsList.size() / 5.0); 50 | } 51 | 52 | private static MutableText getBackPageText(int page, int totalPage) { 53 | MutableText backPageText; 54 | backPageText = Messenger.literal("[<-]"); 55 | backPageText.styled(style -> 56 | style.withHoverEvent( 57 | new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.of(tr("quickbackupmulti.list_backup.back_page"))) 58 | ) 59 | ); 60 | if (page != 1 && totalPage > 1) { 61 | backPageText.styled(style -> 62 | style.withClickEvent( 63 | new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/qb list " + (page - 1)) 64 | ) 65 | ).styled(style -> style.withColor(Formatting.AQUA)); 66 | } else if (page == 1) { 67 | backPageText.styled(style -> style.withColor(Formatting.DARK_GRAY)); 68 | } 69 | return backPageText; 70 | } 71 | 72 | private static MutableText getNextPageText(int page, int totalPage) { 73 | MutableText nextPageText; 74 | nextPageText = Messenger.literal("[->]"); 75 | nextPageText.styled(style -> 76 | style.withHoverEvent( 77 | new HoverEvent( 78 | HoverEvent.Action.SHOW_TEXT, 79 | Text.of(tr("quickbackupmulti.list_backup.next_page"))) 80 | ) 81 | ); 82 | if (page != totalPage && totalPage > 1) { 83 | nextPageText.styled(style -> 84 | style.withClickEvent( 85 | new ClickEvent( 86 | ClickEvent.Action.RUN_COMMAND, 87 | "/qb list " + (page + 1)) 88 | ) 89 | ).styled(style -> style.withColor(Formatting.AQUA)); 90 | } else if (page == totalPage) { 91 | nextPageText.styled(style -> style.withColor(Formatting.DARK_GRAY)); 92 | } 93 | return nextPageText; 94 | } 95 | 96 | private static MutableText getSlotText(String name, int page, int num, long backupSizeB) throws IOException { 97 | MutableText backText = Messenger.literal("§2[▷] "); 98 | MutableText deleteText = Messenger.literal("§c[×] "); 99 | MutableText nameText = Messenger.literal("§6" + truncateString(name, 8) + "§r "); 100 | MutableText resultText = Messenger.literal(""); 101 | StorageInfo result = getDataBase().getStorageInfo(name); 102 | 103 | backText.styled(style -> 104 | style.withClickEvent( 105 | new ClickEvent( 106 | ClickEvent.Action.RUN_COMMAND, 107 | "/qb back " + name) 108 | ) 109 | ).styled(style -> 110 | style.withHoverEvent( 111 | new HoverEvent( 112 | HoverEvent.Action.SHOW_TEXT, 113 | Text.of(tr("quickbackupmulti.list_backup.slot.restore", name))) 114 | ) 115 | ); 116 | 117 | deleteText.styled(style -> 118 | style.withClickEvent( 119 | new ClickEvent( 120 | ClickEvent.Action.SUGGEST_COMMAND, 121 | "/qb delete " + name) 122 | ) 123 | ).styled(style -> 124 | style.withHoverEvent( 125 | new HoverEvent( 126 | HoverEvent.Action.SHOW_TEXT, 127 | Text.of(tr("quickbackupmulti.list_backup.slot.delete", name))) 128 | ) 129 | ); 130 | 131 | nameText.styled(style -> 132 | style.withClickEvent( 133 | new ClickEvent( 134 | ClickEvent.Action.RUN_COMMAND, 135 | "/qb show " + name) 136 | ) 137 | ).styled(style -> 138 | style.withHoverEvent( 139 | new HoverEvent( 140 | HoverEvent.Action.SHOW_TEXT, 141 | Text.of(tr("quickbackupmulti.list_backup.slot.show", name))) 142 | ) 143 | ); 144 | 145 | String desc = result.getDesc(); 146 | if (desc.isEmpty()) desc = tr("quickbackupmulti.empty_comment"); 147 | double backupSizeMB = (double) backupSizeB / FileUtils.ONE_MB; 148 | double backupSizeGB = (double) backupSizeB / FileUtils.ONE_GB; 149 | String sizeString = (backupSizeMB >= 1000) ? String.format("%.2fGB", backupSizeGB) : String.format("%.2fMB", backupSizeMB); 150 | resultText.append("\n" + tr("quickbackupmulti.list_backup.slot.header", num + (5 * (page - 1))) + " ") 151 | .append(nameText) 152 | .append(backText) 153 | .append(deleteText) 154 | .append("§a" + sizeString) 155 | .append( 156 | String.format( 157 | " §b%s§7: §r%s", 158 | new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(result.getTimestamp()), 159 | truncateString(desc, 10) 160 | ) 161 | ); 162 | return resultText; 163 | } 164 | 165 | public static MutableText list(int page) { 166 | long totalBackupSizeB = 0; 167 | Path backupDir = getBackupDir(); 168 | List backupsList = getBackupsList(); 169 | if (backupsList.isEmpty() || getPageCount(backupsList, page) == 0) { 170 | return Messenger.literal(tr("quickbackupmulti.list_empty")); 171 | } 172 | int totalPage = getTotalPage(backupsList); 173 | 174 | MutableText resultText = Messenger.literal(tr("quickbackupmulti.list_backup.title", page)); 175 | MutableText backPageText = getBackPageText(page, totalPage); 176 | MutableText nextPageText = getNextPageText(page, totalPage); 177 | resultText.append("\n") 178 | .append(backPageText) 179 | .append(" ") 180 | .append(tr("quickbackupmulti.list_backup.page_msg", page, totalPage)) 181 | .append(" ") 182 | .append(nextPageText); 183 | 184 | for (int j=1;j<=getPageCount(backupsList, page);j++) { 185 | try { 186 | String name = backupsList.get(((j-1)+5*(page-1))); 187 | long backupSizeB = getDirSize(backupDir.resolve(name).toFile()); 188 | totalBackupSizeB += backupSizeB; 189 | resultText.append(getSlotText(name, page, j, backupSizeB)); 190 | } catch (IOException e) { 191 | LOGGER.error("", e); 192 | } 193 | } 194 | double totalBackupSizeMB = (double) totalBackupSizeB / FileUtils.ONE_MB; 195 | double totalBackupSizeGB = (double) totalBackupSizeB / FileUtils.ONE_GB; 196 | String sizeString = 197 | (totalBackupSizeMB >= 1000) 198 | ? String.format("%.2fGB", totalBackupSizeGB) 199 | : String.format("%.2fMB", totalBackupSizeMB); 200 | resultText.append("\n" + tr("quickbackupmulti.list_backup.slot.total_space", sizeString)); 201 | return resultText; 202 | } 203 | 204 | public static MutableText search(List searchResultList) { 205 | MutableText resultText = Messenger.literal(tr("quickbackupmulti.search.success")); 206 | Path backupDir = getBackupDir(); 207 | for (int i=1;i<=searchResultList.size();i++) { 208 | try { 209 | String name = searchResultList.get(i-1); 210 | long backupSizeB = getDirSize(backupDir.resolve(name).toFile()); 211 | resultText.append(getSlotText(name, 1, i, backupSizeB)); 212 | } catch (IOException e) { 213 | LOGGER.error("", e); 214 | } 215 | } 216 | return resultText; 217 | } 218 | 219 | public static MutableText show(String name) { 220 | MutableText resultText; 221 | if (getStorager().storageExists(name)) { 222 | StorageInfo backupInfo = getDataBase().getStorageInfo(name); 223 | resultText = Messenger.literal(tr("quickbackupmulti.show.header")); 224 | String desc = backupInfo.getDesc(); 225 | if (desc.isEmpty()) desc = tr("quickbackupmulti.empty_comment"); 226 | 227 | MutableText backText = Messenger.literal(tr("quickbackupmulti.show.back_button")); 228 | MutableText deleteText = Messenger.literal(tr("quickbackupmulti.show.delete_button")); 229 | backText.styled(style -> 230 | style.withClickEvent( 231 | new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/qb back " + name) 232 | ) 233 | ).styled(style -> 234 | style.withHoverEvent( 235 | new HoverEvent( 236 | HoverEvent.Action.SHOW_TEXT, 237 | Text.of(tr("quickbackupmulti.list_backup.slot.restore", name))) 238 | ) 239 | ); 240 | deleteText.styled(style -> 241 | style.withClickEvent( 242 | new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/qb delete " + name)) 243 | ).styled(style -> 244 | style.withHoverEvent( 245 | new HoverEvent( 246 | HoverEvent.Action.SHOW_TEXT, 247 | Text.of(tr("quickbackupmulti.list_backup.slot.delete", name))) 248 | ) 249 | ); 250 | 251 | resultText.append("\n") 252 | .append(tr("quickbackupmulti.show.name") + ": §r" + backupInfo.getName() + "\n") 253 | .append(tr("quickbackupmulti.show.desc") + ": §r" + desc + "\n") 254 | .append( 255 | tr("quickbackupmulti.show.time") 256 | + ": §r" 257 | + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(backupInfo.getTimestamp()) 258 | ) 259 | .append("\n") 260 | .append(backText) 261 | .append(" ") 262 | .append(deleteText); 263 | 264 | } else { 265 | resultText = Messenger.literal(tr("quickbackupmulti.show.fail")); 266 | resultText.styled(style -> style.withColor(Formatting.RED)); 267 | } 268 | return resultText; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/utils/MakeUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils; 2 | 3 | import io.github.skydynamic.increment.storage.lib.database.index.type.StorageInfo; 4 | import io.github.skydynamic.quickbackupmulti.config.Config; 5 | import net.minecraft.server.MinecraftServer; 6 | import net.minecraft.server.command.ServerCommandSource; 7 | import net.minecraft.server.world.ServerWorld; 8 | import net.minecraft.text.Text; 9 | 10 | import java.util.ArrayList; 11 | 12 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 13 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getStorager; 14 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 15 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.*; 16 | import static io.github.skydynamic.quickbackupmulti.utils.ScheduleUtils.startSchedule; 17 | 18 | public class MakeUtils { 19 | public static int make(ServerCommandSource commandSource, String name, String desc) { 20 | long startTime = System.currentTimeMillis(); 21 | if (getStorager().storageExists(name)) { 22 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.make.fail_exists"))); 23 | return 0; 24 | } 25 | try { 26 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.make.start"))); 27 | MinecraftServer server = commandSource.getServer(); 28 | server.executeSync(() -> server.saveAll(true, true, true)); 29 | for (ServerWorld serverWorld : server.getWorlds()) { 30 | if (serverWorld == null || serverWorld.savingDisabled) continue; 31 | serverWorld.savingDisabled = true; 32 | } 33 | 34 | StorageInfo storageInfo = new StorageInfo(name, desc, System.currentTimeMillis(), true, new ArrayList<>()); 35 | 36 | getStorager().incrementalStorage(storageInfo, savePath, getBackupDir().resolve(name), fileFilter, null); 37 | 38 | long endTime = System.currentTimeMillis(); 39 | double intervalTime = (endTime - startTime) / 1000.0; 40 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.make.success", intervalTime))); 41 | 42 | if (Config.INSTANCE.getScheduleBackup()) startSchedule(commandSource); 43 | 44 | for (ServerWorld serverWorld : server.getWorlds()) { 45 | if (serverWorld == null || !serverWorld.savingDisabled) continue; 46 | serverWorld.savingDisabled = false; 47 | } 48 | } catch (Exception e) { 49 | LOGGER.error("Make Backup Failed", e); 50 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.make.fail", e.toString()))); 51 | backupDir.resolve(name).toFile().deleteOnExit(); 52 | } 53 | return 1; 54 | } 55 | 56 | public static boolean scheduleMake(ServerCommandSource commandSource, String name) { 57 | if (getStorager().storageExists(name)) return false; 58 | make(commandSource, name, "Scheduled Backup"); 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/utils/Messenger.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils; 2 | 3 | import net.minecraft.server.command.ServerCommandSource; 4 | import net.minecraft.text.MutableText; 5 | import net.minecraft.text.Text; 6 | //#if MC<11900 7 | //$$ import net.minecraft.text.LiteralText; 8 | //#endif 9 | 10 | public class Messenger { 11 | 12 | public static void sendMessage(ServerCommandSource commandSource, Text text) { 13 | //#if MC>=11900 14 | commandSource.sendMessage(text); 15 | //#else 16 | //$$ commandSource.sendFeedback(text, false); 17 | //#endif 18 | } 19 | 20 | public static MutableText literal(String string) { 21 | //#if MC>=11900 22 | return Text.literal(string); 23 | //#else 24 | //$$ return new LiteralText(string); 25 | //#endif 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/utils/QbmManager.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils; 2 | 3 | import io.github.skydynamic.increment.storage.lib.util.IndexUtil; 4 | import io.github.skydynamic.quickbackupmulti.QbmConstant; 5 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 6 | import io.github.skydynamic.quickbackupmulti.config.Config; 7 | 8 | import io.github.skydynamic.quickbackupmulti.config.ConfigStorage; 9 | import net.fabricmc.api.EnvType; 10 | import net.minecraft.entity.player.PlayerEntity; 11 | import net.minecraft.server.command.ServerCommandSource; 12 | import net.minecraft.text.Text; 13 | import net.minecraft.util.WorldSavePath; 14 | 15 | import org.apache.commons.io.FileUtils; 16 | import org.apache.commons.io.filefilter.IOFileFilter; 17 | import org.apache.commons.io.filefilter.NameFileFilter; 18 | import org.apache.commons.io.filefilter.NotFileFilter; 19 | import org.quartz.SchedulerException; 20 | 21 | import java.io.File; 22 | import java.io.FilenameFilter; 23 | import java.io.IOException; 24 | import java.nio.file.Path; 25 | import java.util.ArrayList; 26 | import java.util.List; 27 | import java.util.Objects; 28 | 29 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 30 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getStorager; 31 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.supportLanguage; 32 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 33 | 34 | public class QbmManager { 35 | public static Path backupDir = Path.of(QbmConstant.pathGetter.getGamePath() + "/QuickBackupMulti/"); 36 | public static Path savePath = Config.TEMP_CONFIG.server.getSavePath(WorldSavePath.ROOT); 37 | public static IOFileFilter fileFilter = new NotFileFilter(new NameFileFilter(Config.INSTANCE.getIgnoredFiles())); 38 | // public static IOFileFilter dirFilter = new NonRecursiveDirFilter(); 39 | 40 | public static Path getBackupDir() { 41 | if (Config.TEMP_CONFIG.env == EnvType.SERVER) { 42 | return backupDir; 43 | } else { 44 | return backupDir.resolve(Config.TEMP_CONFIG.worldName); 45 | } 46 | } 47 | 48 | public static void restoreClient(String slot) { 49 | File targetBackupSlot = getBackupDir().resolve(slot).toFile(); 50 | try { 51 | FileUtils.deleteDirectory(savePath.toFile()); 52 | FileUtils.copyDirectory(targetBackupSlot, savePath.toFile()); 53 | IndexUtil.copyIndexFile( 54 | slot, 55 | Path.of(Config.INSTANCE.getStoragePath()).resolve(Config.TEMP_CONFIG.worldName), 56 | savePath.toFile() 57 | ); 58 | } catch (IOException e) { 59 | LOGGER.error("Restore Failed", e); 60 | } 61 | } 62 | 63 | public static void restore(String slot) { 64 | File targetBackupSlot = getBackupDir().resolve(slot).toFile(); 65 | try { 66 | for (File file : Objects.requireNonNull(savePath.toFile().listFiles((FilenameFilter) fileFilter))) { 67 | FileUtils.forceDelete(file); 68 | } 69 | FileUtils.copyDirectory(targetBackupSlot, savePath.toFile()); 70 | IndexUtil.copyIndexFile( 71 | slot, 72 | Path.of(Config.INSTANCE.getStoragePath()).resolve(Config.TEMP_CONFIG.worldName), 73 | savePath.toFile() 74 | ); 75 | } catch (IOException e) { 76 | LOGGER.error("Restore Failed", e); 77 | } 78 | } 79 | 80 | public static List getBackupsList() { 81 | List backupsDirList = new ArrayList<>(); 82 | for (File file : Objects.requireNonNull(getBackupDir().toFile().listFiles())) { 83 | if (file.isDirectory() && getStorager().storageExists(file.getName())) { 84 | backupsDirList.add(file.getName()); 85 | } 86 | } 87 | return backupsDirList; 88 | } 89 | 90 | public static boolean delete(String name) { 91 | if (getStorager().storageExists(name)) { 92 | try { 93 | IndexUtil.reIndex(name, Config.TEMP_CONFIG.worldName); 94 | getStorager().deleteStorage(name); 95 | FileUtils.deleteDirectory(getBackupDir().resolve(name).toFile()); 96 | return true; 97 | } catch (SecurityException | IOException e) { 98 | LOGGER.error("Delete Backup Failed", e); 99 | return false; 100 | } 101 | } else return false; 102 | } 103 | 104 | public static void createBackupDir(Path path) { 105 | if (!path.toFile().exists()) path.toFile().mkdirs(); 106 | } 107 | 108 | public static ConfigStorage verifyConfig(ConfigStorage c, PlayerEntity player) { 109 | ServerCommandSource commandSource = player.getCommandSource(); 110 | 111 | // schedule enable 112 | if (c.isScheduleBackup() && !Config.INSTANCE.getScheduleBackup()) { 113 | ScheduleUtils.startSchedule(commandSource); 114 | } else if (!c.isScheduleBackup() && Config.INSTANCE.getScheduleBackup()){ 115 | ScheduleUtils.disableSchedule(commandSource); 116 | } 117 | 118 | // schedule backup mode switch 119 | if (!c.getScheduleMode().equals(Config.INSTANCE.getScheduleMode())) 120 | ScheduleUtils.switchScheduleMode(commandSource, c.getScheduleMode()); 121 | 122 | // schedule set cron 123 | if (!c.getScheduleCron().equals(Config.INSTANCE.getScheduleCron())) { 124 | try { 125 | ScheduleUtils.setScheduleCron(commandSource, c.getScheduleCron()); 126 | } catch (SchedulerException e) { 127 | Messenger.sendMessage(commandSource, 128 | Messenger.literal(tr("quickbackupmulti.schedule.cron.set_fail", e))); 129 | } 130 | } 131 | 132 | // schedule set interval 133 | if (!((Integer) c.getScheduleInterval()).equals(Config.INSTANCE.getScheduleInterval())) { 134 | try { 135 | ScheduleUtils.setScheduleInterval(commandSource, c.getScheduleInterval()); 136 | } catch (SchedulerException e) { 137 | Messenger.sendMessage(commandSource, 138 | Messenger.literal(tr("quickbackupmulti.schedule.cron.set_fail", e))); 139 | } 140 | } 141 | 142 | // lang 143 | if (!c.getLang().equals(Config.INSTANCE.getLang())) { 144 | if (!supportLanguage.contains(c.getLang())) { 145 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.lang.failed"))); 146 | c.setLang(Config.INSTANCE.getLang()); 147 | } else { 148 | Translate.handleResourceReload(c.getLang()); 149 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.lang.set", c.getLang()))); 150 | } 151 | } 152 | 153 | return c; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/utils/ScheduleUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils; 2 | 3 | import io.github.skydynamic.quickbackupmulti.config.Config; 4 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 5 | import io.github.skydynamic.quickbackupmulti.utils.schedule.CronUtil; 6 | import net.minecraft.server.command.ServerCommandSource; 7 | import org.quartz.SchedulerException; 8 | 9 | import java.text.SimpleDateFormat; 10 | 11 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 12 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 13 | 14 | public class ScheduleUtils { 15 | 16 | public static void startSchedule(ServerCommandSource commandSource) { 17 | String nextBackupTimeString = ""; 18 | try { 19 | // 照顾Java8 20 | switch (Config.INSTANCE.getScheduleMode()) { 21 | case "cron": { 22 | nextBackupTimeString = CronUtil.getNextExecutionTime(Config.INSTANCE.getScheduleCron(), false); 23 | break; 24 | } 25 | case "interval": { 26 | nextBackupTimeString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis() + Config.INSTANCE.getScheduleInterval() * 1000L); 27 | break; 28 | } 29 | } 30 | CronUtil.buildScheduler(); 31 | Config.TEMP_CONFIG.scheduler.start(); 32 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.enable.success", nextBackupTimeString))); 33 | } catch (SchedulerException e) { 34 | LOGGER.error("Start schedule backup fail: ", e); 35 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.enable.fail", e.toString()))); 36 | } 37 | } 38 | 39 | public static int switchScheduleMode(ServerCommandSource commandSource, String mode) { 40 | try { 41 | if (Config.INSTANCE.getScheduleBackup()) { 42 | if (Config.TEMP_CONFIG.scheduler.isStarted()) Config.TEMP_CONFIG.scheduler.shutdown(); 43 | startSchedule(commandSource); 44 | } 45 | } catch (SchedulerException e) { 46 | LOGGER.error("Switch schedule mode backup fail: ", e); 47 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.switch.fail", e.toString()))); 48 | return 0; 49 | } 50 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.switch.set", mode))); 51 | return 1; 52 | } 53 | 54 | public static int disableSchedule(ServerCommandSource commandSource) { 55 | try { 56 | Config.TEMP_CONFIG.scheduler.shutdown(); 57 | Config.INSTANCE.setScheduleBackup(false); 58 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.disable.success"))); 59 | return 1; 60 | } catch (SchedulerException e) { 61 | LOGGER.error("Close schedule backup fail: ", e); 62 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.disable.fail", e.toString()))); 63 | return 0; 64 | } 65 | } 66 | 67 | public static int setScheduleCron(ServerCommandSource commandSource, String value) throws SchedulerException { 68 | if (CronUtil.cronIsValid(value)) { 69 | if (Config.TEMP_CONFIG.scheduler != null) { 70 | if (Config.TEMP_CONFIG.scheduler.isStarted()) Config.TEMP_CONFIG.scheduler.shutdown(); 71 | } 72 | Config.INSTANCE.setScheduleCron(value); 73 | if (Config.INSTANCE.getScheduleBackup()) { 74 | startSchedule(commandSource); 75 | if (Config.INSTANCE.getScheduleMode().equals("cron")) { 76 | Messenger.sendMessage(commandSource, 77 | Messenger.literal(Translate.tr("quickbackupmulti.schedule.cron.set_custom_success", CronUtil.getNextExecutionTime(Config.INSTANCE.getScheduleCron(), false)))); 78 | } 79 | } else { 80 | Messenger.sendMessage(commandSource, 81 | Messenger.literal(tr("quickbackupmulti.schedule.cron.set_success_only"))); 82 | } 83 | } else { 84 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.cron.expression_error"))); 85 | return 0; 86 | } 87 | return 1; 88 | } 89 | 90 | public static int setScheduleInterval(ServerCommandSource commandSource, int value, String type) throws SchedulerException { 91 | if (Config.TEMP_CONFIG.scheduler != null) { 92 | if (Config.TEMP_CONFIG.scheduler.isStarted()) Config.TEMP_CONFIG.scheduler.shutdown(); 93 | } 94 | switch (type) { 95 | case "s" : { 96 | Config.INSTANCE.setScheduleInterval(value); 97 | break; 98 | } 99 | case "m" : { 100 | Config.INSTANCE.setScheduleInterval(CronUtil.getSeconds(value, 0, 0)); 101 | break; 102 | } 103 | case "h" : { 104 | Config.INSTANCE.setScheduleInterval(CronUtil.getSeconds(0, value, 0)); 105 | break; 106 | } 107 | case "d" : { 108 | Config.INSTANCE.setScheduleInterval(CronUtil.getSeconds(0, 0, value)); 109 | break; 110 | } 111 | } 112 | if (Config.INSTANCE.getScheduleBackup()) { 113 | startSchedule(commandSource); 114 | if (Config.INSTANCE.getScheduleMode().equals("interval")) { 115 | Messenger.sendMessage(commandSource, 116 | Messenger.literal(tr("quickbackupmulti.schedule.cron.set_success", 117 | new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 118 | .format(System.currentTimeMillis() + Config.INSTANCE.getScheduleInterval() * 1000L)) 119 | ) 120 | ); 121 | } 122 | } else { 123 | Messenger.sendMessage(commandSource, 124 | Messenger.literal(tr("quickbackupmulti.schedule.cron.set_success_only"))); 125 | } 126 | return 1; 127 | } 128 | 129 | public static void setScheduleInterval(ServerCommandSource commandSource, int value) throws SchedulerException { 130 | setScheduleInterval(commandSource, value, "s"); 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/utils/ServerPathUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils; 2 | 3 | import io.github.skydynamic.quickbackupmulti.api.ServerPathGetter; 4 | import net.fabricmc.loader.api.FabricLoader; 5 | 6 | import java.nio.file.Path; 7 | 8 | public class ServerPathUtils implements ServerPathGetter { 9 | @Override 10 | public Path getConfigPath() { 11 | return FabricLoader.getInstance().getConfigDir(); 12 | } 13 | 14 | @Override 15 | public Path getGamePath() { 16 | return FabricLoader.getInstance().getGameDir(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/utils/schedule/CronUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils.schedule; 2 | 3 | import io.github.skydynamic.quickbackupmulti.config.Config; 4 | 5 | import org.quartz.*; 6 | import org.quartz.impl.StdSchedulerFactory; 7 | 8 | import java.text.ParseException; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | 12 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 13 | 14 | public class CronUtil { 15 | 16 | public static Trigger buildTrigger() { 17 | try { 18 | if (Config.INSTANCE.getScheduleMode().equals("cron")) { 19 | return TriggerBuilder.newTrigger() 20 | .withSchedule(CronScheduleBuilder.cronSchedule(Config.INSTANCE.getScheduleCron())) 21 | .startAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(getNextExecutionTime(Config.INSTANCE.getScheduleCron(), false))) 22 | .build(); 23 | } else { 24 | return TriggerBuilder.newTrigger() 25 | .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(Config.INSTANCE.getScheduleInterval()).repeatForever()) 26 | .startAt(new Date(System.currentTimeMillis() + Config.INSTANCE.getScheduleInterval() * 1000L)) 27 | .build(); 28 | } 29 | } catch (ParseException e) { 30 | throw new RuntimeException(e); 31 | } 32 | } 33 | 34 | public static void buildScheduler() { 35 | try { 36 | JobDetail jb = JobBuilder.newJob(ScheduleBackup.class).withIdentity("ScheduleBackup").build(); 37 | Trigger t = buildTrigger(); 38 | StdSchedulerFactory sf = new StdSchedulerFactory(); 39 | Config.TEMP_CONFIG.setScheduler(sf.getScheduler()); 40 | Config.TEMP_CONFIG.scheduler.scheduleJob(jb, t); 41 | } catch (SchedulerException e) { 42 | LOGGER.error(e.toString()); 43 | } 44 | } 45 | 46 | public static int getSeconds(int minute, int hour, int day) { 47 | return minute*60 + hour*3600 + day*3600*24; 48 | } 49 | 50 | public static String getNextExecutionTime(String cronExpress, boolean get) { 51 | CronExpression cronExpression; 52 | SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 53 | try { 54 | cronExpression = new CronExpression(cronExpress); 55 | if (get) { 56 | return simpleDateFormat.format(cronExpression.getNextValidTimeAfter(new Date(Config.TEMP_CONFIG.latestScheduleExecuteTime))); 57 | } 58 | Date nextValidTime = cronExpression.getNextValidTimeAfter(new Date()); 59 | return simpleDateFormat.format(nextValidTime); 60 | } catch (ParseException e) { 61 | return simpleDateFormat.format(new Date()); 62 | } 63 | } 64 | 65 | public static boolean cronIsValid(String cronExpression) { 66 | try { 67 | new CronExpression(cronExpression); 68 | return true; 69 | } catch (Exception e) { 70 | return false; 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/utils/schedule/ScheduleBackup.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils.schedule; 2 | 3 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 4 | import io.github.skydynamic.quickbackupmulti.config.Config; 5 | import net.minecraft.server.MinecraftServer; 6 | import net.minecraft.server.network.ServerPlayerEntity; 7 | import org.quartz.Job; 8 | import org.quartz.JobExecutionContext; 9 | 10 | import java.text.SimpleDateFormat; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 15 | import static io.github.skydynamic.quickbackupmulti.utils.MakeUtils.scheduleMake; 16 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.CronUtil.getNextExecutionTime; 17 | 18 | public class ScheduleBackup implements Job { 19 | public static String generateName() { 20 | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); 21 | return "ScheduleBackup-" + dateFormat.format(System.currentTimeMillis()); 22 | } 23 | @Override 24 | public void execute(JobExecutionContext context) { 25 | if (Config.TEMP_CONFIG.server != null) { 26 | MinecraftServer server = Config.TEMP_CONFIG.server; 27 | if (scheduleMake(server.getCommandSource(), generateName())) { 28 | List finalPlayerList = new ArrayList<>(server.getPlayerManager().getPlayerList()); 29 | Config.TEMP_CONFIG.setLatestScheduleExecuteTime(System.currentTimeMillis()); 30 | String nextExecuteTime = ""; 31 | switch (Config.INSTANCE.getScheduleMode()) { 32 | case "interval" : { 33 | nextExecuteTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis() + Config.INSTANCE.getScheduleInterval() * 1000L); 34 | break; 35 | } 36 | case "cron" : { 37 | nextExecuteTime = getNextExecutionTime(Config.INSTANCE.getScheduleCron(), true); 38 | break; 39 | } 40 | } 41 | for (ServerPlayerEntity player : finalPlayerList) { 42 | player.sendMessage(Messenger.literal(tr("quickbackupmulti.schedule.execute.finish", nextExecuteTime)), false); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/utils/schedule/ScheduleUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils.schedule; 2 | 3 | import io.github.skydynamic.quickbackupmulti.config.Config; 4 | import org.quartz.SchedulerException; 5 | 6 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 7 | 8 | public class ScheduleUtils { 9 | 10 | public static void startSchedule() { 11 | if (Config.INSTANCE.getScheduleBackup()) { 12 | try { 13 | CronUtil.buildScheduler(); 14 | Config.TEMP_CONFIG.scheduler.start(); 15 | Config.TEMP_CONFIG.setLatestScheduleExecuteTime(System.currentTimeMillis()); 16 | LOGGER.info("QBM Schedule backup started."); 17 | } catch (SchedulerException e) { 18 | LOGGER.error("QBM schedule backup start error: " + e); 19 | } 20 | } 21 | } 22 | 23 | public static void shutdownSchedule() { 24 | try { 25 | if (Config.TEMP_CONFIG.scheduler != null && Config.TEMP_CONFIG.scheduler.isStarted()) Config.TEMP_CONFIG.scheduler.shutdown(); 26 | } catch (SchedulerException ignored) { 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/assets/quickbackupmulti/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyDynamic/QuickBackupM-Fabric/bfb0a1999a89b3fd48193b47f6b75198732eda00/src/main/resources/assets/quickbackupmulti/icon.png -------------------------------------------------------------------------------- /src/main/resources/assets/quickbackupmulti/lang/en_us.yml: -------------------------------------------------------------------------------- 1 | quickbackupmulti: 2 | empty_comment: §7Empty§r 3 | list_empty: "§7Do not have any backups now" 4 | 5 | config_page: 6 | save_button: "Save Config" 7 | close_button: "Close Screen" 8 | back: "Back" 9 | title: "QuickBackupMulti Config Screen" 10 | open_schedule_config_button: "Open Schedule Config Screen" 11 | lang: "Language" 12 | schedule: 13 | switch: "Schedule Backup: %s" 14 | cron: "Cron expression" 15 | interval: "Backup Interval" 16 | mode: 17 | switch: "Schedule backup mode: %s" 18 | 19 | schedule: 20 | get: "Next backup time is: §b%s" 21 | get_fail: "Schedule Backup do not enable" 22 | mode: 23 | get: "The current §aschedule mode§r is: §4§l%s§r" 24 | switch: 25 | set: "Schedule mode switch to: §a%s" 26 | fail: "Schedule mode switch fail: §c%s" 27 | enable: 28 | success: "Schedule backup started, the next execute time: §b%s" 29 | fail: "Schedule backup start fail: §c%s" 30 | disable: 31 | success: "Schedule backup shutdown!" 32 | fail: "Schedule backup shutdown fail: §c%s" 33 | execute: 34 | finish: "Finish to backup, the next execute time: §b%s" 35 | schedule_reset: "Execute Backup, Scheduler reset and the next execute time: §b%s" 36 | cron: 37 | set_custom_success: "Custom cron set success, the next execute time: §b%s" 38 | set_success: "Set success, the next execute time: §b%s" 39 | set_success_only: "Set success, but you do not enable schedule backup" 40 | set_fail: "Set fail: §c%s" 41 | expression_error: The Cron expression you entered is non-standard! 42 | 43 | database: 44 | set_success: "Set Success" 45 | set_fail: "Set fail, because: %s" 46 | set_success_but: "Set Success, but an error has occurred: %s" 47 | value_equal_config: "Use Internal DataBase is §b%s now!" 48 | 49 | search: 50 | success: "§aSearch success§r, the result: " 51 | fail: "§4Do not have any result" 52 | 53 | toast: 54 | start_title: Restoring now...Please wait a moment 55 | start_content: Don't join world in this time! 56 | end_title: Restore success! 57 | end_content: Now you can join your save! 58 | 59 | lang: 60 | failed: "§4Language do not exists" 61 | get: "The current §alanguage§r is: §4§l%s§r" 62 | set: "§aLanguage§r has been set to: §4§l%s§r" 63 | 64 | confirm_restore: 65 | nothing_to_confirm: Nothing to confirm 66 | 67 | init: 68 | start: Initializing QuickBackupMulti... 69 | finish: Initializing QuickBackupMulti completed! 70 | 71 | make: 72 | start: §aBackup§r now...Please wait a moment 73 | no_slot: No available slots found, §abackup§r abort! 74 | success: §aBackup §rcompleted, It takes §6%s§rs 75 | fail: §a备份§r失败,错误原因%s 76 | fail_exists: "§aMake backup §rFailed,error msg: Slot already exists" 77 | 78 | delete: 79 | success: §aSuccess delete slot §6%s 80 | fail: §4Fail§r to delete slot §6%s 81 | 82 | list_backup: 83 | title: §d[Page §b%s§d Slot Information]§r 84 | back_page: Last page 85 | next_page: Next page 86 | page_msg: "[§b%s§r / §b%s§r]" 87 | slot: 88 | header: "[§6#%s§r]" 89 | restore: Click to restore to slot §6%s§r 90 | delete: Click to delete slot §6%s§r 91 | show: Click to show §6%s§r detail 92 | total_space: "These backups total space consumed: §a%s§r" 93 | 94 | restore: 95 | countdown: 96 | intro: "%s execute restore backup, §cRestore§r after 10 second" 97 | text: "%s second later the world will be §crestored§r to slot §6%s§r, " 98 | hover: Click to ABORT restore! 99 | abort: §cRestore§r aborted! 100 | fail: "§4Fail§r ro restore: slot §4NotFount§r!" 101 | confirm_hint: Use §7/qb confirm§r to confirm §crestore§r 102 | abort_hint: Confirmed restore, If you want to abort, please enter §7/qb cancel§r 103 | 104 | show: 105 | header: "§d[Slot Information]§r" 106 | name: "§bName" 107 | desc: "§bBackup Desc" 108 | time: "§bBackup Time" 109 | back_button: "§2[Click to back]" 110 | delete_button: "§c[Click to delete]" 111 | fail: Backup Not Found -------------------------------------------------------------------------------- /src/main/resources/assets/quickbackupmulti/lang/zh_cn.yml: -------------------------------------------------------------------------------- 1 | quickbackupmulti: 2 | empty_comment: §7空§r 3 | list_empty: "§7当前没有任何备份" 4 | 5 | config_page: 6 | save_button: "保存" 7 | close_button: "关闭" 8 | back: "返回" 9 | title: "QuickBackupMulti 配置页面" 10 | open_schedule_config_button: "打开定时备份配置界面" 11 | lang: "语言" 12 | schedule: 13 | switch: "定时备份: %s" 14 | cron: "Cron表达式" 15 | interval: "备份间隔" 16 | mode: 17 | switch: "定时备份模式: %s" 18 | 19 | schedule: 20 | get: "下一次备份时间为: §b%s" 21 | get_fail: "定时备份未启用" 22 | mode: 23 | get: "当前设置的§a定时备份模式§r为: §4§l%s§r" 24 | switch: 25 | set: "定时备份模式切换到: §a%s" 26 | fail: "定时备份切换失败: §c%s" 27 | enable: 28 | success: "定时备份已开启, 下一次触发备份时间: §b%s" 29 | fail: "定时备份启用失败: §c%s" 30 | disable: 31 | success: "定时备份已关闭" 32 | fail: "定时备份关闭失败: §c%s" 33 | execute: 34 | finish: "定时备份完成, 下一次触发备份时间: §b%s" 35 | schedule_reset: "备份触发, 定时备份下一次触发时间: §b%s" 36 | cron: 37 | set_custom_success: "自定义定时备份设置成功, 下一次触发备份时间: §b%s" 38 | set_success: "设置成功, 下一次触发备份时间: §b%s" 39 | set_success_only: "设置成功, 但你尚未开启定时备份" 40 | set_fail: "设置失败: §c%s" 41 | expression_error: 你输入的Cron表达式有误 42 | 43 | database: 44 | set_success: "设置成功" 45 | set_fail: "设置失败, 原因: %s" 46 | set_success_but: "设置成功, 但发生了错误: %s" 47 | value_equal_config: "当前的值已经为§b%s" 48 | 49 | search: 50 | success: "§a搜索成功§r, 结果如下: " 51 | fail: "§4没有搜索到任何结果" 52 | 53 | toast: 54 | start_title: 正在进行回档...请稍等! 55 | start_content: 在此期间请勿进入存档! 56 | end_title: 回档完成! 57 | end_content: 你现在可以进入存档了! 58 | 59 | lang: 60 | failed: "§4语言不存在" 61 | get: "当前设置的§a语言§r为: §4§l%s§r" 62 | set: "已将§a语言§r设置为: §4§l%s§r" 63 | 64 | confirm_restore: 65 | nothing_to_confirm: 没有什么需要确认的 66 | 67 | init: 68 | start: 初始化QuickBackupMulti中... 69 | finish: 初始化QuickBackupMulti完成! 70 | 71 | make: 72 | start: §a备份§r中...请稍等 73 | no_slot: 未找到可用槽位,§a备份§r中断! 74 | success: §a备份§r完成,耗时§6%s§r秒 75 | fail: §a备份§r失败,错误原因%s 76 | fail_exists: "§a备份§r失败,错误原因: 槽位已存在" 77 | 78 | delete: 79 | success: 删除槽位§6%s§r§a完成§r 80 | fail: 删除槽位§6%s§r§4失败§r 81 | 82 | list_backup: 83 | title: "§d[第§b%s§d页槽位信息]§r" 84 | back_page: 上一页 85 | next_page: 下一页 86 | page_msg: "[第§b%s§r页 / 共§b%s§r页]" 87 | slot: 88 | header: "[§6#%s§r]" 89 | restore: 点击回档至槽位§6%s§r 90 | delete: 点击删除槽位§6%s§r 91 | show: 点击查看槽位§6%s§r详情 92 | total_space: "当前页面备份总占用空间: §a%s§r" 93 | 94 | restore: 95 | countdown: 96 | intro: "%s 执行回档, 10秒后关闭服务器§c回档§r" 97 | text: "还有%s秒,将§c回档§r为槽位§6%s§r, " 98 | hover: 点击终止回档! 99 | abort: 已取消§c回档§r任务! 100 | fail: "回档§4失败§r: 槽位§4不存在§r!" 101 | confirm_hint: 使用§7/qb confirm§r 确认§c回档§r 102 | abort_hint: 已确认回档, 如需取消请输入§7/qb cancel§r 103 | 104 | show: 105 | header: "§d[槽位信息]§r" 106 | name: "§b名称" 107 | desc: "§b描述" 108 | time: "§b备份时间" 109 | back_button: "§2[点击回档]" 110 | delete_button: "§c[点击删除]" 111 | fail: 槽位不存在 -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "${id}", 4 | "version": "${version}", 5 | "name": "${name}", 6 | "description": "A mod for creating world-save backups", 7 | "authors": [ 8 | "SkyDynamic" 9 | ], 10 | "contact": { 11 | "sources": "https://github.com/SkyDynamic/QuickBackupM-Fabric/", 12 | "issues": "https://github.com/SkyDynamic/QuickBackupM-Fabric/issues/" 13 | }, 14 | "license": "Apache License 2.0", 15 | "icon": "assets/quickbackupmulti/icon.png", 16 | "environment": "*", 17 | "entrypoints": { 18 | "main": [ 19 | "io.github.skydynamic.quickbackupmulti.QuickBackupMulti" 20 | ], 21 | "client": [ 22 | "io.github.skydynamic.quickbackupmulti.QuickBackupMultiClient" 23 | ] 24 | }, 25 | "mixins": [ 26 | "quickbackupmulti.mixins.json" 27 | ], 28 | "accessWidener": "qbm.accesswidener", 29 | "depends": { 30 | "fabricloader": ">=${fabric_loader}", 31 | "minecraft": "${minecraft_support}", 32 | "${fabric_api_id}": "*" 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/resources/quickbackupmulti.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "io.github.skydynamic.quickbackupmulti.mixin", 4 | "compatibilityLevel": "/*JAVA_VERSION*/", 5 | "mixins": [], 6 | "injectors": { 7 | "defaultRequire": 1 8 | }, 9 | "server": [ 10 | "MinecraftServer_ServerMixin" 11 | ], 12 | "client": [ 13 | "MinecraftClientMixin", 14 | "MinecraftServer_ClientMixin", 15 | "TitleScreenMixin" 16 | ] 17 | } -------------------------------------------------------------------------------- /updateindex.json: -------------------------------------------------------------------------------- 1 | { 2 | "newVersion": "1.1.2" 3 | } -------------------------------------------------------------------------------- /versions/1.18.2/gradle.properties: -------------------------------------------------------------------------------- 1 | minecraft_version=1.18.2 2 | minecraft_support_version=1.18.2 3 | yarn_mappings=1.18.2+build.4 4 | loader_version=0.14.24 5 | fabric_version=0.77.0+1.18.2 6 | game_versions = 1.18.2 -------------------------------------------------------------------------------- /versions/1.18.2/qbm.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v2 named 2 | 3 | accessible method net/minecraft/server/MinecraftServer runServer ()V 4 | accessible field net/minecraft/server/MinecraftServer stopped Z 5 | accessible field net/minecraft/server/MinecraftServer running Z 6 | accessible field net/minecraft/client/MinecraftClient toastManager Lnet/minecraft/client/toast/ToastManager; 7 | accessible field net/minecraft/client/gui/tooltip/Tooltip content Lnet/minecraft/text/Text; -------------------------------------------------------------------------------- /versions/1.19.4/gradle.properties: -------------------------------------------------------------------------------- 1 | minecraft_version=1.19.4 2 | minecraft_support_version=1.19.4 3 | yarn_mappings=1.19.4+build.2 4 | loader_version=0.14.24 5 | fabric_version=0.87.2+1.19.4 6 | game_versions = 1.19.4 7 | -------------------------------------------------------------------------------- /versions/1.19.4/qbm.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v2 named 2 | 3 | accessible method net/minecraft/server/MinecraftServer runServer ()V 4 | accessible field net/minecraft/server/MinecraftServer stopped Z 5 | accessible field net/minecraft/server/MinecraftServer running Z 6 | accessible field net/minecraft/client/MinecraftClient toastManager Lnet/minecraft/client/toast/ToastManager; 7 | accessible field net/minecraft/client/gui/tooltip/Tooltip content Lnet/minecraft/text/Text; -------------------------------------------------------------------------------- /versions/1.20.3/gradle.properties: -------------------------------------------------------------------------------- 1 | minecraft_version=1.20.3 2 | minecraft_support_version=>=1.20.3 <=1.20.4 3 | yarn_mappings=1.20.3+build.1 4 | loader_version=0.14.24 5 | fabric_version=0.91.1+1.20.3 6 | game_versions = 1.20.3 7 | -------------------------------------------------------------------------------- /versions/1.20.3/qbm.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v2 named 2 | 3 | accessible method net/minecraft/server/MinecraftServer runServer ()V 4 | accessible field net/minecraft/server/MinecraftServer stopped Z 5 | accessible field net/minecraft/server/MinecraftServer running Z 6 | accessible field net/minecraft/client/MinecraftClient toastManager Lnet/minecraft/client/toast/ToastManager; 7 | accessible field net/minecraft/client/gui/tooltip/Tooltip content Lnet/minecraft/text/Text; -------------------------------------------------------------------------------- /versions/1.20.5/gradle.properties: -------------------------------------------------------------------------------- 1 | minecraft_version=1.20.5 2 | minecraft_support_version=>=1.20.5 <=1.20.6 3 | yarn_mappings=1.20.5+build.1 4 | loader_version=0.15.10 5 | fabric_version=0.97.8+1.20.5 6 | game_versions = 1.20.5 7 | -------------------------------------------------------------------------------- /versions/1.20.5/qbm.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v2 named 2 | 3 | accessible method net/minecraft/server/MinecraftServer runServer ()V 4 | accessible field net/minecraft/server/MinecraftServer stopped Z 5 | accessible field net/minecraft/server/MinecraftServer running Z 6 | accessible field net/minecraft/client/MinecraftClient toastManager Lnet/minecraft/client/toast/ToastManager; 7 | accessible field net/minecraft/client/gui/tooltip/Tooltip content Lnet/minecraft/text/Text; -------------------------------------------------------------------------------- /versions/1.20.5/src/main/java/io/github/skydynamic/quickbackupmulti/Packets.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; 5 | import net.minecraft.network.codec.PacketCodec; 6 | import net.minecraft.network.codec.PacketCodecs; 7 | import net.minecraft.network.packet.CustomPayload; 8 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.OPEN_CONFIG_GUI_PACKET_ID; 9 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.REQUEST_OPEN_CONFIG_GUI_PACKET_ID; 10 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.SAVE_CONFIG_PACKET_ID; 11 | 12 | public class Packets { 13 | public static void registerPacketCodec() { 14 | PayloadTypeRegistry.playC2S().register(Packets.RequestOpenConfigGuiPacket.PACKET_ID, Packets.RequestOpenConfigGuiPacket.PACKET_CODEC); 15 | PayloadTypeRegistry.playC2S().register(Packets.OpenConfigGuiPacket.PACKET_ID, Packets.OpenConfigGuiPacket.PACKET_CODEC); 16 | PayloadTypeRegistry.playC2S().register(Packets.SaveConfigPacket.PACKET_ID, Packets.SaveConfigPacket.PACKET_CODEC); 17 | PayloadTypeRegistry.playS2C().register(Packets.OpenConfigGuiPacket.PACKET_ID, Packets.OpenConfigGuiPacket.PACKET_CODEC); 18 | } 19 | public record RequestOpenConfigGuiPacket(String config) implements CustomPayload { 20 | public static final CustomPayload.Id PACKET_ID = new CustomPayload.Id<>(REQUEST_OPEN_CONFIG_GUI_PACKET_ID); 21 | public static final PacketCodec PACKET_CODEC = 22 | PacketCodecs.STRING.xmap(RequestOpenConfigGuiPacket::new, RequestOpenConfigGuiPacket::config); 23 | 24 | @Override 25 | public Id getId() { 26 | return PACKET_ID; 27 | } 28 | } 29 | 30 | public record OpenConfigGuiPacket(String config) implements CustomPayload { 31 | public static final CustomPayload.Id PACKET_ID = new CustomPayload.Id<>(OPEN_CONFIG_GUI_PACKET_ID); 32 | public static final PacketCodec PACKET_CODEC = 33 | PacketCodecs.STRING.xmap(OpenConfigGuiPacket::new, OpenConfigGuiPacket::config); 34 | 35 | @Override 36 | public Id getId() { 37 | return PACKET_ID; 38 | } 39 | } 40 | 41 | public record SaveConfigPacket(String config) implements CustomPayload { 42 | public static final CustomPayload.Id PACKET_ID = new CustomPayload.Id<>(SAVE_CONFIG_PACKET_ID); 43 | public static final PacketCodec PACKET_CODEC = 44 | PacketCodecs.STRING.xmap(SaveConfigPacket::new, SaveConfigPacket::config); 45 | 46 | @Override 47 | public Id getId() { 48 | return PACKET_ID; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /versions/1.20/gradle.properties: -------------------------------------------------------------------------------- 1 | minecraft_version=1.20 2 | minecraft_support_version=>=1.20 <=1.20.4 3 | yarn_mappings=1.20+build.1 4 | loader_version=0.14.24 5 | fabric_version=0.83.0+1.20 6 | game_versions = 1.20 7 | -------------------------------------------------------------------------------- /versions/1.20/qbm.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v2 named 2 | 3 | accessible method net/minecraft/server/MinecraftServer runServer ()V 4 | accessible field net/minecraft/server/MinecraftServer stopped Z 5 | accessible field net/minecraft/server/MinecraftServer running Z 6 | accessible field net/minecraft/client/MinecraftClient toastManager Lnet/minecraft/client/toast/ToastManager; 7 | accessible field net/minecraft/client/gui/tooltip/Tooltip content Lnet/minecraft/text/Text; -------------------------------------------------------------------------------- /versions/1.21/gradle.properties: -------------------------------------------------------------------------------- 1 | minecraft_version=1.21 2 | minecraft_support_version=>=1.21 3 | yarn_mappings=1.21+build.9 4 | loader_version=0.15.10 5 | fabric_version=0.100.6+1.21 6 | game_versions=1.21 7 | -------------------------------------------------------------------------------- /versions/1.21/qbm.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v2 named 2 | 3 | accessible method net/minecraft/server/MinecraftServer runServer ()V 4 | accessible field net/minecraft/server/MinecraftServer stopped Z 5 | accessible field net/minecraft/server/MinecraftServer running Z 6 | accessible field net/minecraft/client/MinecraftClient toastManager Lnet/minecraft/client/toast/ToastManager; 7 | accessible field net/minecraft/client/gui/tooltip/Tooltip content Lnet/minecraft/text/Text; -------------------------------------------------------------------------------- /versions/1.21/src/main/java/io/github/skydynamic/quickbackupmulti/Packets.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; 5 | import net.minecraft.network.codec.PacketCodec; 6 | import net.minecraft.network.codec.PacketCodecs; 7 | import net.minecraft.network.packet.CustomPayload; 8 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.OPEN_CONFIG_GUI_PACKET_ID; 9 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.REQUEST_OPEN_CONFIG_GUI_PACKET_ID; 10 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.SAVE_CONFIG_PACKET_ID; 11 | 12 | public class Packets { 13 | public static void registerPacketCodec() { 14 | PayloadTypeRegistry.playC2S().register(Packets.RequestOpenConfigGuiPacket.PACKET_ID, Packets.RequestOpenConfigGuiPacket.PACKET_CODEC); 15 | PayloadTypeRegistry.playC2S().register(Packets.OpenConfigGuiPacket.PACKET_ID, Packets.OpenConfigGuiPacket.PACKET_CODEC); 16 | PayloadTypeRegistry.playC2S().register(Packets.SaveConfigPacket.PACKET_ID, Packets.SaveConfigPacket.PACKET_CODEC); 17 | PayloadTypeRegistry.playS2C().register(Packets.OpenConfigGuiPacket.PACKET_ID, Packets.OpenConfigGuiPacket.PACKET_CODEC); 18 | } 19 | public record RequestOpenConfigGuiPacket(String config) implements CustomPayload { 20 | public static final CustomPayload.Id PACKET_ID = new CustomPayload.Id<>(REQUEST_OPEN_CONFIG_GUI_PACKET_ID); 21 | public static final PacketCodec PACKET_CODEC = 22 | PacketCodecs.STRING.xmap(RequestOpenConfigGuiPacket::new, RequestOpenConfigGuiPacket::config); 23 | 24 | @Override 25 | public Id getId() { 26 | return PACKET_ID; 27 | } 28 | } 29 | 30 | public record OpenConfigGuiPacket(String config) implements CustomPayload { 31 | public static final CustomPayload.Id PACKET_ID = new CustomPayload.Id<>(OPEN_CONFIG_GUI_PACKET_ID); 32 | public static final PacketCodec PACKET_CODEC = 33 | PacketCodecs.STRING.xmap(OpenConfigGuiPacket::new, OpenConfigGuiPacket::config); 34 | 35 | @Override 36 | public Id getId() { 37 | return PACKET_ID; 38 | } 39 | } 40 | 41 | public record SaveConfigPacket(String config) implements CustomPayload { 42 | public static final CustomPayload.Id PACKET_ID = new CustomPayload.Id<>(SAVE_CONFIG_PACKET_ID); 43 | public static final PacketCodec PACKET_CODEC = 44 | PacketCodecs.STRING.xmap(SaveConfigPacket::new, SaveConfigPacket::config); 45 | 46 | @Override 47 | public Id getId() { 48 | return PACKET_ID; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /versions/mainProject: -------------------------------------------------------------------------------- 1 | 1.20 -------------------------------------------------------------------------------- /versions/mapping-1.18-1.19.txt: -------------------------------------------------------------------------------- 1 | net.minecraft.text.BaseText net.minecraft.text.MutableText 2 | net.minecraft.text.TranslatableText net.minecraft.text.TranslatableTextContent -------------------------------------------------------------------------------- /versions/mapping-empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SkyDynamic/QuickBackupM-Fabric/bfb0a1999a89b3fd48193b47f6b75198732eda00/versions/mapping-empty.txt --------------------------------------------------------------------------------