├── .github ├── ISSUE_TEMPLATE │ └── bug_report.yml └── workflows │ ├── build.yml │ ├── gradle.yml │ ├── matrix_prep.yml │ ├── release.yml │ └── scripts │ ├── matrix.py │ └── summary.py ├── .gitignore ├── LICENSE ├── README.MD ├── README_EN.MD ├── build.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 │ │ ├── PermissionCommand.java │ │ ├── QuickBackupMultiCommand.java │ │ ├── SettingCommand.java │ │ ├── permission │ │ │ ├── PermissionManager.java │ │ │ └── PermissionType.java │ │ └── suggestion │ │ │ ├── AutoRestartModeSuggestionProvider.java │ │ │ ├── CustomSuggestionProvider.java │ │ │ └── LangSuggestionProvider.java │ │ ├── config │ │ ├── AutoRestartMode.java │ │ ├── QbmTempConfig.java │ │ └── QuickBackupMultiConfig.java │ │ ├── i18n │ │ └── Translate.java │ │ ├── mixin │ │ ├── client │ │ │ ├── ClientPlayNetworkHandlerMixin.java │ │ │ ├── MinecraftClientMixin.java │ │ │ ├── MinecraftServerMixin.java │ │ │ └── WorldEntryMixin.java │ │ └── server │ │ │ └── MinecraftServerMixin.java │ │ └── utils │ │ ├── ListUtils.java │ │ ├── MakeUtils.java │ │ ├── Messenger.java │ │ ├── QbmManager.java │ │ ├── ScheduleUtils.java │ │ ├── ServerPathUtils.java │ │ ├── UpdateChecker.java │ │ └── schedule │ │ ├── CronUtil.java │ │ ├── ScheduleBackup.java │ │ └── ScheduleUtils.java │ └── resources │ ├── assets │ └── quickbackupmulti │ │ ├── icon.png │ │ └── lang │ │ ├── en_us.yml │ │ ├── zh_cn.yml │ │ └── zh_tw.yml │ ├── fabric.mod.json │ └── quickbackupmulti.mixins.json └── versions ├── 1.18.2 ├── build.gradle ├── 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 ├── 1.20 ├── gradle.properties └── qbm.accesswidener ├── 1.21 ├── gradle.properties └── qbm.accesswidener ├── mainProject └── mapping-1.18-1.19.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/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 | release: 18 | types: 19 | - published 20 | 21 | jobs: 22 | show_action_parameters: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Show action parameters 26 | run: | 27 | cat < $GITHUB_STEP_SUMMARY 28 | ## Action Parameters 29 | - target_subproject: \`${{ github.event.inputs.target_subproject }}\` 30 | - target_release_tag: \`${{ github.event.inputs.target_release_tag }}\` 31 | EOF 32 | 33 | matrix_prep: 34 | uses: ./.github/workflows/matrix_prep.yml 35 | with: 36 | target_subproject: ${{ github.event.inputs.target_subproject }} 37 | 38 | build: 39 | uses: ./.github/workflows/build.yml 40 | with: 41 | target_subproject: ${{ github.event.inputs.target_subproject }} 42 | release: true 43 | 44 | release: 45 | needs: 46 | - matrix_prep 47 | - build 48 | runs-on: ubuntu-latest 49 | 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 minecraft_support_version' 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.minecraft_support_version }}" >> $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/QuickBackupMultiMod-Dev/QuickBackupM-Fabric.svg)](https://github.com/QuickBackupMultiMod-Dev/QuickBackupM-Fabric/issues) 3 | [![Modrinth](https://img.shields.io/modrinth/dt/DgWBIBY5?label=Modrinth%20Downloads)](https://modrinth.com/mod/quickbackupmulti) 4 | [![CurseForge](https://img.shields.io/curseforge/dt/951047?label=CurseForge%20Downloads)](https://www.curseforge.com/minecraft/mc-mods/quickbackupmulti) 5 | [![Github Release](https://img.shields.io/github/downloads/QuickBackupMultiMod-Dev/QuickBackupM-Fabric/total?label=Github%20Downloads)](https://github.com/QuickBackupMultiMod-Dev/QuickBackupM-Fabric/releases) 6 | 7 |
8 | Logo 9 |
10 |
11 | 12 | # QuickBackupMulti-Fabric 13 | 14 | **简体中文** | [English](README_EN.MD) 15 | 16 | _✨ MC备份 / 回档模组 ✨_ 17 | 重构自MCDR插件: [QuickBackupMulti](https://github.com/TISUnion/QuickBackupM) 18 | 19 |
20 | 21 | > [!WARNING] 22 | > 当前Mod大版本为`v2`, 与`v1`的实现代码有很大差异 23 | > 24 | > 如果使用的是`v1`版本, 请勿直接更新到`v2`. 使用`v2`的也请勿随意降级到`v1` 25 | 26 | [//]: # (> 本 Mod 对于客户端单人游戏兼容性较差,使用时请谨慎,若造成存档损坏本 Mod 不负任何责任) 27 | 28 | ## 本Mod优势 29 | - 支持回档自动重启服务器, 不再是只备份不回档 30 | - 支持定时备份, 并支持自定义cron表达式 31 | 32 | ## 使用方式 33 | > [!WARNING] 34 | > 严禁自行删除备份文件夹内的所有备份文件, 如需删除请进入游戏内进行手动删除! 35 | 36 | > 在使用mod前请确保你已安装Fabric Loader 37 | 38 | 将本mod放进`mods`文件夹即可 39 | 40 | ## 指令 41 | `/qb` 或 `/quickbackupmulti`均可触发mod 42 | 43 | ## todo 44 | - [x] 定时备份 45 | - [x] 无限槽位 46 | - [x] Hash对比并仅备份差异文件 47 | - [ ] 个性化设置 48 | 49 | ## 许可 50 | 本项目遵循 [Apache-2.0 license](https://www.apache.org/licenses/LICENSE-2.0) 许可 51 | -------------------------------------------------------------------------------- /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/QuickBackupMultiMod-Dev/QuickBackupM-Fabric.svg)](https://github.com/QuickBackupMultiMod-Dev/QuickBackupM-Fabric/issues) 3 | [![Modrinth](https://img.shields.io/modrinth/dt/DgWBIBY5?label=Modrinth%20Downloads)](https://modrinth.com/mod/quickbackupmulti) 4 | [![CurseForge](https://img.shields.io/curseforge/dt/951047?label=CurseForge%20Downloads)](https://www.curseforge.com/minecraft/mc-mods/quickbackupmulti) 5 | [![Github Release](https://img.shields.io/github/downloads/QuickBackupMultiMod-Dev/QuickBackupM-Fabric/total?label=Github%20Downloads)](https://github.com/QuickBackupMultiMod-Dev/QuickBackupM-Fabric/releases) 6 | 7 |
8 | Logo 9 |
10 |
11 | 12 | # QuickBackupMulti-Fabric 13 | 14 | [简体中文](README.MD) | **English** 15 | 16 | _✨ Minecraft Backup / Restore Mod ✨_ 17 | Idea come from: [QuickBackupMulti](https://github.com/TISUnion/QuickBackupM) 18 | 19 |
20 | 21 | > [!WARNING] 22 | > Now the Mod version is `v2`, and there are great differences backup implementation method compared to the `v1` version 23 | > 24 | > If you use `v1` now, please don't to upgrade to `v2`. Also, if you use `v2` now, please don't downgrade to `v1` 25 | 26 | [//]: # (> 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.) 27 | 28 | ## Advantage 29 | - Support auto start server after restore backup 30 | - Support schedule backup and custom cron expression. 31 | 32 | ## How to use 33 | > [!WARNING] 34 | > It is strictly prohibited to delete all backup files in the backup folder by oneself. 35 | > If you need to delete them, please enter the game and use the command to delete them! 36 | 37 | > Please install Fabric Loader before install this mod 38 | 39 | Go to [release](https://github.com/SkyDynamic/QuickBackupM-Fabric/releases) to download the latest mod and put the `.jar` file in `mods` dir 40 | 41 | ## Command 42 | `/qb` or `/quickbackupmulti` can trigger the mod 43 | 44 | ## todo 45 | - [x] Schedule Backup 46 | - [x] Infinite Slot 47 | - [x] Compare files hash and save what files had change 48 | - [ ] Personal Setting 49 | 50 | ## LICENSE 51 | This Project follow [Apache-2.0 license](https://www.apache.org/licenses/LICENSE-2.0) 52 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "fabric-loom" version "1.7-SNAPSHOT" apply false 3 | id "maven-publish" 4 | id "com.replaymod.preprocess" version "88169fcbc9" 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, null) 17 | mc1200.link(mc1203, null) 18 | mc1203.link(mc1205, null) 19 | mc1205.link(mc1210, null) 20 | } -------------------------------------------------------------------------------- /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.2.5 7 | maven_group=dev.skydynamic 8 | archives_base_name=QuickBackupMulti -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBackupMultiMod-Dev/QuickBackupM-Fabric/f2465577740b443ecf3a4a69623657c026f94231/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-bin.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/QuickBackupMultiMod-Dev/QuickBackupM-Fabric/f2465577740b443ecf3a4a69623657c026f94231/indexImg.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | import groovy.json.JsonSlurper 2 | 3 | pluginManagement { 4 | repositories { 5 | maven { 6 | name = 'Fabric' 7 | url = 'https://maven.fabricmc.net/' 8 | } 9 | maven { 10 | name = 'Jitpack' 11 | url = "https://jitpack.io" 12 | } 13 | maven { 14 | url 'https://maven.aliyun.com/repository/public/' 15 | } 16 | resolutionStrategy { 17 | eachPlugin { 18 | switch (requested.id.id) { 19 | case "com.replaymod.preprocess": { 20 | useModule("com.github.Fallen-Breath:preprocessor:${requested.version}") 21 | break 22 | } 23 | } 24 | } 25 | } 26 | mavenCentral() 27 | gradlePluginPortal() 28 | } 29 | } 30 | 31 | Map> settings = file("settings.json").withReader { 32 | new JsonSlurper().parse(it) as Map> 33 | } 34 | 35 | for (String version : settings.get("versions")) { 36 | include(":$version") 37 | ProjectDescriptor proj = project(":$version") 38 | proj.setProjectDir(file("versions/$version")) 39 | proj.setBuildFileName("../${settings.versions.get(0)}/build.gradle") 40 | } -------------------------------------------------------------------------------- /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 | Path dataBasePath; 10 | 11 | public DataBaseManager(String fileName, Path dataBasePath) { 12 | this.fileName = fileName; 13 | this.dataBasePath = dataBasePath; 14 | } 15 | 16 | @Override 17 | public void setFileName(String s) { 18 | this.fileName = s; 19 | } 20 | 21 | @Override 22 | public void setDataBasePath(Path path) { 23 | this.dataBasePath = path; 24 | } 25 | 26 | @Override 27 | public String getFileName() { 28 | return this.fileName; 29 | } 30 | 31 | @Override 32 | public Path getDataBasePath() { 33 | return this.dataBasePath; 34 | } 35 | } -------------------------------------------------------------------------------- /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.command.permission.PermissionManager; 6 | import io.github.skydynamic.quickbackupmulti.utils.ServerPathUtils; 7 | import net.minecraft.util.Identifier; 8 | 9 | public final class QbmConstant { 10 | public static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); 11 | public static final ServerPathUtils pathGetter = new ServerPathUtils(); 12 | public static final PermissionManager permissionManager = new PermissionManager(); 13 | } 14 | -------------------------------------------------------------------------------- /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.increment.storage.lib.database.DataBase; 6 | import io.github.skydynamic.quickbackupmulti.config.QbmTempConfig; 7 | import io.github.skydynamic.quickbackupmulti.config.QuickBackupMultiConfig; 8 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 9 | import io.github.skydynamic.quickbackupmulti.command.QuickBackupMultiCommand; 10 | import io.github.skydynamic.quickbackupmulti.utils.QbmManager; 11 | import io.github.skydynamic.quickbackupmulti.utils.UpdateChecker; 12 | import net.fabricmc.api.EnvType; 13 | import net.fabricmc.api.ModInitializer; 14 | import net.fabricmc.loader.api.FabricLoader; 15 | //#if MC>=11900 16 | import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; 17 | //#else 18 | //$$ import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; 19 | //#endif 20 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 21 | 22 | //#if MC>=12005 23 | //$$ import net.minecraft.server.network.ServerPlayerEntity; 24 | //#endif 25 | 26 | import org.apache.logging.log4j.Level; 27 | import org.apache.logging.log4j.LogManager; 28 | //#if MC>=11900 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | //#else 32 | //$$ import org.apache.logging.log4j.Logger; 33 | //#endif 34 | import java.nio.file.Path; 35 | 36 | 37 | public class QuickBackupMulti implements ModInitializer { 38 | 39 | //#if MC>=11900 40 | public static final Logger LOGGER = LoggerFactory.getLogger(QuickBackupMulti.class); 41 | //#else 42 | //$$ public static final Logger LOGGER = LogManager.getLogger(QuickBackupMulti.class); 43 | //#endif 44 | 45 | public static final UpdateChecker updateChecker = new UpdateChecker(); 46 | 47 | public static final String modName = "QuickBackupMulti"; 48 | public static final String modId = "quickbackupmulti"; 49 | public static QbmTempConfig TEMP_CONFIG = new QbmTempConfig(); 50 | 51 | EnvType env = FabricLoader.getInstance().getEnvironmentType(); 52 | 53 | private static DataBase dataBase; 54 | private static Storager storager; 55 | 56 | public static final QuickBackupMultiConfig config = new QuickBackupMultiConfig(QbmConstant.pathGetter.getConfigPath().resolve(modName + ".json")); 57 | 58 | @Override 59 | public void onInitialize() { 60 | config.load(); 61 | config.save(); 62 | 63 | FabricLoader.getInstance().getModContainer(modId).ifPresent(modContainer -> 64 | TEMP_CONFIG.setModVersion(modContainer.getMetadata().getVersion().getFriendlyString())); 65 | 66 | final JavaUtilLog4jFilter filter = new JavaUtilLog4jFilter(); 67 | java.util.logging.Logger.getLogger("").setFilter(filter); 68 | ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addFilter(filter); 69 | 70 | Translate.handleResourceReload(config.getLang()); 71 | 72 | initDataBase(); 73 | 74 | //#if MC>=11900 75 | CommandRegistrationCallback.EVENT.register( 76 | (dispatcher, registryAccess, environment) -> QuickBackupMultiCommand.RegisterCommand(dispatcher) 77 | ); 78 | //#else 79 | //$$ CommandRegistrationCallback.EVENT.register( 80 | //$$ (dispatcher, registryAccess) -> QuickBackupMultiCommand.RegisterCommand(dispatcher) 81 | //$$ ); 82 | //#endif 83 | 84 | ServerLifecycleEvents.SERVER_STARTED.register(server -> { 85 | TEMP_CONFIG.setServerValue(server); 86 | TEMP_CONFIG.setEnv(env); 87 | }); 88 | 89 | ServerLifecycleEvents.SERVER_STOPPED.register(server -> { 90 | if (env == EnvType.SERVER) { 91 | if (TEMP_CONFIG.isBackup) { 92 | QbmManager.restore(TEMP_CONFIG.backupSlot); 93 | TEMP_CONFIG.setIsBackupValue(false); 94 | switch (config.getAutoRestartMode()) { 95 | case DISABLE -> {} 96 | case DEFAULT -> { 97 | TEMP_CONFIG.server.stopped = false; 98 | TEMP_CONFIG.server.running = true; 99 | TEMP_CONFIG.server.runServer(); 100 | } 101 | case MCSM -> new Thread(() -> System.exit(-4000)).start(); 102 | } 103 | } else { 104 | getDataBase().stopInternalMongoServer(); 105 | } 106 | } 107 | TEMP_CONFIG.server = null; 108 | }); 109 | 110 | if (config.isCheckUpdate()) updateChecker.start(); 111 | } 112 | 113 | public void initDataBase() { 114 | DataBaseManager dataBaseManager = new DataBaseManager( 115 | "QuickBackupMulti", 116 | Path.of(config.getStoragePath()) 117 | ); 118 | 119 | dataBase = new DataBase(dataBaseManager, config); 120 | 121 | IndexUtil.setConfig(config); 122 | IndexUtil.setDataBase(dataBase); 123 | } 124 | 125 | public static boolean shouldFilterMessage(Level level, String packetName) { 126 | // 仅过滤INFO,Debug / ERROR不过滤 127 | if (level == Level.INFO) { 128 | return packetName.contains("de.bwaldvogel.mongo") 129 | || packetName.contains("org.mongodb.driver") 130 | || packetName.contains("org.quartz"); 131 | } 132 | return false; 133 | } 134 | 135 | public static DataBase getDataBase() { 136 | return dataBase; 137 | } 138 | 139 | public static Storager getStorager() { 140 | return storager; 141 | } 142 | 143 | public static void setDataStore(String worldName) { 144 | storager = new Storager(dataBase); 145 | dataBase.newDataStore(modName + "-" + worldName); 146 | } 147 | 148 | public static void deleteDataStore(String worldName) { 149 | dataBase.deleteCollection(modName + "-" + worldName); 150 | } 151 | } -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/QuickBackupMultiClient.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti; 2 | 3 | import net.fabricmc.api.ClientModInitializer; 4 | 5 | 6 | public class QuickBackupMultiClient implements ClientModInitializer { 7 | @Override 8 | public void onInitializeClient() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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.QuickBackupMulti; 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.restore; 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 | restore(slot); 43 | QuickBackupMulti.TEMP_CONFIG.setIsBackupValue(false); 44 | minecraftClient.execute(() -> { 45 | Text title = Text.of(Translate.tr("quickbackupmulti.toast.end_title")); 46 | Text content = Text.of(Translate.tr("quickbackupmulti.toast.end_content")); 47 | //#if MC>=11800 48 | SystemToast.show(minecraftClient.getToastManager(), SystemToast.Type.PERIODIC_NOTIFICATION, title, content); 49 | //#else 50 | //$$ SystemToast.show(minecraftClient.getToastManager(), SystemToast.Type.WORLD_BACKUP, title, content); 51 | //#endif 52 | }); 53 | }); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/backup/RestoreTask.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.backup; 2 | 3 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 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 | QuickBackupMulti.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 | QuickBackupMulti.TEMP_CONFIG.setIsBackupValue(true); 33 | QuickBackupMulti.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 io.github.skydynamic.quickbackupmulti.command.permission.PermissionManager; 6 | import io.github.skydynamic.quickbackupmulti.command.permission.PermissionType; 7 | import net.minecraft.server.command.CommandManager; 8 | import net.minecraft.server.command.ServerCommandSource; 9 | 10 | import java.text.SimpleDateFormat; 11 | 12 | import static io.github.skydynamic.quickbackupmulti.utils.MakeUtils.make; 13 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 14 | import static net.minecraft.server.command.CommandManager.literal; 15 | 16 | public class MakeCommand { 17 | 18 | static class makeRunnable implements Runnable { 19 | ServerCommandSource commandSource; 20 | String name; 21 | String desc; 22 | 23 | makeRunnable(ServerCommandSource commandSource, String name, String desc) { 24 | this.commandSource = commandSource; 25 | this.name = name; 26 | this.desc = desc; 27 | } 28 | 29 | @Override 30 | public void run() { 31 | long l = System.currentTimeMillis(); 32 | LOGGER.info("Make Backup thread started..."); 33 | make(commandSource, name, desc); 34 | LOGGER.info("Make Backup thread close => {}ms", System.currentTimeMillis() - l); 35 | } 36 | } 37 | 38 | private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HHmmss"); 39 | 40 | public static LiteralArgumentBuilder makeCommand = literal("make") 41 | .requires(it -> PermissionManager.hasPermission(it, 4, PermissionType.HELPER)) 42 | .executes(it -> makeSaveBackup(it.getSource(), dateFormat.format(System.currentTimeMillis()), "")) 43 | .then(CommandManager.argument("name", StringArgumentType.string()) 44 | .executes(it -> makeSaveBackup(it.getSource(), StringArgumentType.getString(it, "name"), "")) 45 | .then(CommandManager.argument("desc", StringArgumentType.string()) 46 | .executes(it -> makeSaveBackup( 47 | it.getSource(), 48 | StringArgumentType.getString(it, "name"), 49 | StringArgumentType.getString(it, "desc") 50 | ))) 51 | ); 52 | // .then(CommandManager.argument("desc", StringArgumentType.string()) 53 | // .executes(it -> makeSaveBackup(it.getSource(), String.valueOf(System.currentTimeMillis()), StringArgumentType.getString(it, "desc")))); 54 | 55 | private static int makeSaveBackup(ServerCommandSource commandSource, String name, String desc) { 56 | new Thread(new makeRunnable(commandSource, name, desc)).start(); 57 | return 1; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/command/PermissionCommand.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.command; 2 | 3 | import com.mojang.authlib.GameProfile; 4 | import com.mojang.brigadier.arguments.IntegerArgumentType; 5 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 6 | import io.github.skydynamic.quickbackupmulti.command.permission.PermissionManager; 7 | import io.github.skydynamic.quickbackupmulti.command.permission.PermissionType; 8 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 9 | import net.minecraft.command.argument.GameProfileArgumentType; 10 | import net.minecraft.server.command.CommandManager; 11 | import net.minecraft.server.command.ServerCommandSource; 12 | 13 | import java.util.Collection; 14 | 15 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.permissionManager; 16 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 17 | import static net.minecraft.server.command.CommandManager.literal; 18 | 19 | public class PermissionCommand { 20 | public static LiteralArgumentBuilder permissionCommand = literal("permission") 21 | .requires(it -> PermissionManager.hasPermission(it, 4, PermissionType.ADMIN)) 22 | .then(literal("set") 23 | .then(CommandManager.argument("player", GameProfileArgumentType.gameProfile()) 24 | .then(CommandManager.argument("level", IntegerArgumentType.integer(0, 2)) 25 | .executes(it -> setPermission( 26 | it.getSource(), 27 | GameProfileArgumentType.getProfileArgument(it, "player"), 28 | IntegerArgumentType.getInteger(it, "level") 29 | )) 30 | ) 31 | ) 32 | ) 33 | .then(literal("get") 34 | .then(CommandManager.argument("player", GameProfileArgumentType.gameProfile()) 35 | .executes(it -> getPermission( 36 | it.getSource(), 37 | GameProfileArgumentType.getProfileArgument(it, "player") 38 | )) 39 | ) 40 | ) 41 | .then(literal("reload") 42 | .executes(it -> reloadPermission(it.getSource())) 43 | ); 44 | 45 | private static int setPermission(ServerCommandSource commandSource, Collection players, int level) { 46 | players.forEach(player -> { 47 | permissionManager.setPermissionByPermissionLevelInt(level, player.getName()); 48 | Messenger.sendMessage(commandSource, 49 | Messenger.literal( 50 | tr("quickbackupmulti.permission.set", 51 | player.getName(), 52 | PermissionType.getByLevelInt(level).name())) 53 | ); 54 | }); 55 | return 1; 56 | } 57 | 58 | private static int getPermission(ServerCommandSource commandSource, Collection players) { 59 | players.forEach(player -> { 60 | Messenger.sendMessage(commandSource, 61 | Messenger.literal( 62 | tr("quickbackupmulti.permission.get", 63 | player.getName(), 64 | permissionManager.getPlayerPermission(player.getName()).name())) 65 | ); 66 | }); 67 | return 1; 68 | } 69 | 70 | private static int reloadPermission(ServerCommandSource commandSource) { 71 | permissionManager.reloadPermission(); 72 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.permission.reload"))); 73 | return 1; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /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.tree.LiteralCommandNode; 7 | //#if MC<=11820 8 | //$$ import com.mojang.brigadier.exceptions.CommandSyntaxException; 9 | //#endif 10 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 11 | import io.github.skydynamic.quickbackupmulti.backup.RestoreTask; 12 | import io.github.skydynamic.quickbackupmulti.command.permission.PermissionManager; 13 | import io.github.skydynamic.quickbackupmulti.command.permission.PermissionType; 14 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 15 | import net.fabricmc.api.EnvType; 16 | import net.fabricmc.loader.api.FabricLoader; 17 | import net.minecraft.server.MinecraftServer; 18 | import net.minecraft.server.command.CommandManager; 19 | import net.minecraft.server.command.ServerCommandSource; 20 | import net.minecraft.server.network.ServerPlayerEntity; 21 | import net.minecraft.text.ClickEvent; 22 | import net.minecraft.text.HoverEvent; 23 | import net.minecraft.text.MutableText; 24 | import net.minecraft.text.Text; 25 | import org.apache.commons.lang3.StringUtils; 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.PermissionCommand.permissionCommand; 38 | import static io.github.skydynamic.quickbackupmulti.command.SettingCommand.settingCommand; 39 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 40 | import static io.github.skydynamic.quickbackupmulti.utils.ListUtils.list; 41 | import static io.github.skydynamic.quickbackupmulti.utils.ListUtils.search; 42 | import static io.github.skydynamic.quickbackupmulti.utils.ListUtils.show; 43 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 44 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.delete; 45 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.getBackupsList; 46 | import static net.minecraft.server.command.CommandManager.literal; 47 | 48 | public class QuickBackupMultiCommand { 49 | public static void RegisterCommand(CommandDispatcher dispatcher) { 50 | LiteralCommandNode QuickBackupMultiShortCommand = dispatcher.register(literal("qb") 51 | .then(literal("list").executes(it -> listSaveBackups(it.getSource(), 1)) 52 | .then(CommandManager.argument("page", IntegerArgumentType.integer(1)) 53 | .executes(it -> listSaveBackups(it.getSource(), IntegerArgumentType.getInteger(it, "page"))))) 54 | 55 | .then(literal("search") 56 | .then(CommandManager.argument("name", StringArgumentType.string()) 57 | .executes(it -> searchSaveBackups(it.getSource(), StringArgumentType.getString(it, "name"))))) 58 | 59 | .then(MakeCommand.makeCommand) 60 | 61 | .then(literal("back").requires(it -> PermissionManager.hasPermission(it, 4, PermissionType.ADMIN)) 62 | .then(CommandManager.argument("name", StringArgumentType.string()) 63 | .executes(it -> restoreSaveBackup(it.getSource(), StringArgumentType.getString(it, "name"))))) 64 | 65 | .then(literal("confirm").requires(it -> PermissionManager.hasPermission(it, 4, PermissionType.ADMIN)) 66 | .executes(it -> { 67 | try { 68 | executeRestore(it.getSource()); 69 | } catch (Exception e) { 70 | LOGGER.info(e.toString()); 71 | } 72 | return 0; 73 | })) 74 | 75 | .then(literal("cancel").requires(it -> PermissionManager.hasPermission(it, 4, PermissionType.ADMIN)) 76 | .executes(it -> cancelRestore(it.getSource()))) 77 | 78 | .then(literal("delete").requires(it -> PermissionManager.hasPermission(it, 2, PermissionType.HELPER)) 79 | .then(CommandManager.argument("name", StringArgumentType.string()) 80 | .executes(it -> deleteSaveBackup(it.getSource(), StringArgumentType.getString(it, "name"))))) 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 | .then(permissionCommand) 89 | ); 90 | 91 | dispatcher.register(literal("quickbackupm").redirect(QuickBackupMultiShortCommand)); 92 | } 93 | 94 | public static final ConcurrentHashMap> QbDataHashMap = new ConcurrentHashMap<>(); 95 | 96 | private static int showBackupDetail(ServerCommandSource commandSource, String name) { 97 | Messenger.sendMessage(commandSource, show(name)); 98 | return 1; 99 | } 100 | 101 | private static int searchSaveBackups(ServerCommandSource commandSource, String string) { 102 | List backupsList = getBackupsList(); 103 | List result = backupsList.stream() 104 | .filter(it -> StringUtils.containsIgnoreCase(it, string)) 105 | .toList(); 106 | if (result.isEmpty()) { 107 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.search.fail"))); 108 | } else { 109 | Messenger.sendMessage(commandSource, search(result)); 110 | } 111 | return 1; 112 | } 113 | 114 | private static int deleteSaveBackup(ServerCommandSource commandSource, String name) { 115 | if (delete(name)) Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.delete.success", name))); 116 | else Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.delete.fail", name))); 117 | return 1; 118 | } 119 | 120 | private static int restoreSaveBackup(ServerCommandSource commandSource, String name) { 121 | if (!getStorager().storageExists(name)) { 122 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.restore.fail"))); 123 | return 0; 124 | } 125 | ConcurrentHashMap restoreDataHashMap = new ConcurrentHashMap<>(); 126 | restoreDataHashMap.put("Slot", name); 127 | restoreDataHashMap.put("Timer", new Timer()); 128 | restoreDataHashMap.put("Countdown", Executors.newSingleThreadScheduledExecutor()); 129 | synchronized (QbDataHashMap) { 130 | QbDataHashMap.put("QBM", restoreDataHashMap); 131 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.restore.confirm_hint"))); 132 | return 1; 133 | } 134 | } 135 | 136 | //#if MC>11900 137 | private static void executeRestore(ServerCommandSource commandSource) { 138 | //#else 139 | //$$ private static void executeRestore(ServerCommandSource commandSource) throws CommandSyntaxException { 140 | //#endif 141 | synchronized (QbDataHashMap) { 142 | if (QbDataHashMap.containsKey("QBM")) { 143 | if (!getStorager().storageExists((String) QbDataHashMap.get("QBM").get("Slot"))) { 144 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.restore.fail"))); 145 | QbDataHashMap.clear(); 146 | return; 147 | } 148 | EnvType env = FabricLoader.getInstance().getEnvironmentType(); 149 | String executePlayerName; 150 | if (commandSource.getPlayer() != null) { 151 | executePlayerName = commandSource.getPlayer().getGameProfile().getName(); 152 | } else { 153 | executePlayerName = "Console"; 154 | } 155 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.restore.abort_hint"))); 156 | MinecraftServer server = commandSource.getServer(); 157 | for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { 158 | player.sendMessage(Text.of(tr("quickbackupmulti.restore.countdown.intro", executePlayerName)), false); 159 | } 160 | String slot = (String) QbDataHashMap.get("QBM").get("Slot"); 161 | QuickBackupMulti.TEMP_CONFIG.setBackupSlot(slot); 162 | Timer timer = (Timer) QbDataHashMap.get("QBM").get("Timer"); 163 | ScheduledExecutorService countdown = (ScheduledExecutorService) QbDataHashMap.get("QBM").get("Countdown"); 164 | AtomicInteger countDown = new AtomicInteger(11); 165 | List finalPlayerList = new ArrayList<>(server.getPlayerManager().getPlayerList()); 166 | countdown.scheduleAtFixedRate(() -> { 167 | int remaining = countDown.decrementAndGet(); 168 | if (remaining >= 1) { 169 | for (ServerPlayerEntity player : finalPlayerList) { 170 | //#if MC>11900 171 | MutableText content = Messenger.literal(tr("quickbackupmulti.restore.countdown.text", remaining, slot)) 172 | //#else 173 | //$$ BaseText content = (BaseText) Messenger.literal(tr("quickbackupmulti.restore.countdown.text", remaining, slot)) 174 | //#endif 175 | .append(Messenger.literal(tr("quickbackupmulti.restore.countdown.hover")) 176 | .styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/qb cancel"))) 177 | .styled(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.of(tr("quickbackupmulti.restore.countdown.hover")))))); 178 | player.sendMessage(content, false); 179 | LOGGER.info(content.getString()); 180 | } 181 | } else { 182 | countdown.shutdown(); 183 | } 184 | }, 0, 1, TimeUnit.SECONDS); 185 | timer.schedule(new RestoreTask(env, finalPlayerList, slot), 10000); 186 | } else { 187 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.confirm_restore.nothing_to_confirm"))); 188 | } 189 | } 190 | } 191 | 192 | private static int cancelRestore(ServerCommandSource commandSource) { 193 | if (QbDataHashMap.containsKey("QBM")) { 194 | synchronized (QbDataHashMap) { 195 | Timer timer = (Timer) QbDataHashMap.get("QBM").get("Timer"); 196 | ScheduledExecutorService countdown = (ScheduledExecutorService) QbDataHashMap.get("QBM").get("Countdown"); 197 | timer.cancel(); 198 | countdown.shutdown(); 199 | QbDataHashMap.clear(); 200 | QuickBackupMulti.TEMP_CONFIG.setIsBackupValue(false); 201 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.restore.abort"))); 202 | } 203 | } else { 204 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.confirm_restore.nothing_to_confirm"))); 205 | } 206 | return 1; 207 | } 208 | 209 | private static int listSaveBackups(ServerCommandSource commandSource, int page) { 210 | MutableText resultText = list(page); 211 | Messenger.sendMessage(commandSource, resultText); 212 | return 1; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /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.QuickBackupMulti; 8 | import io.github.skydynamic.quickbackupmulti.command.permission.PermissionManager; 9 | import io.github.skydynamic.quickbackupmulti.command.permission.PermissionType; 10 | import io.github.skydynamic.quickbackupmulti.command.suggestion.AutoRestartModeSuggestionProvider; 11 | import io.github.skydynamic.quickbackupmulti.command.suggestion.LangSuggestionProvider; 12 | import io.github.skydynamic.quickbackupmulti.config.AutoRestartMode; 13 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 14 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 15 | import io.github.skydynamic.quickbackupmulti.utils.ScheduleUtils; 16 | import net.minecraft.server.command.CommandManager; 17 | import net.minecraft.server.command.ServerCommandSource; 18 | import net.minecraft.text.Text; 19 | import org.quartz.SchedulerException; 20 | 21 | import java.text.SimpleDateFormat; 22 | 23 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getDataBase; 24 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.setDataStore; 25 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.supportLanguage; 26 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 27 | import static io.github.skydynamic.quickbackupmulti.utils.ScheduleUtils.disableSchedule; 28 | import static io.github.skydynamic.quickbackupmulti.utils.ScheduleUtils.startSchedule; 29 | import static io.github.skydynamic.quickbackupmulti.utils.ScheduleUtils.switchScheduleMode; 30 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.CronUtil.getNextExecutionTime; 31 | import static net.minecraft.server.command.CommandManager.literal; 32 | 33 | public class SettingCommand { 34 | 35 | public static LiteralArgumentBuilder settingCommand = literal("setting") 36 | .requires(it -> PermissionManager.hasPermission(it, 2, PermissionType.HELPER)) 37 | .then(literal("lang") 38 | .then(literal("get").executes(it -> getLang(it.getSource()))) 39 | .then(literal("set") 40 | .requires(it -> PermissionManager.hasPermission(it, 2, PermissionType.HELPER)) 41 | .then(CommandManager.argument("lang", StringArgumentType.string()) 42 | .suggests(LangSuggestionProvider.lang()) 43 | .executes(it -> setLang(it.getSource(), StringArgumentType.getString(it, "lang")))))) 44 | .then(literal("schedule") 45 | .then(literal("enable").executes(it -> enableScheduleBackup(it.getSource()))) 46 | .then(literal("disable").executes(it -> disableScheduleBackup(it.getSource()))) 47 | .then(literal("set") 48 | .then(literal("interval") 49 | .then(literal("second") 50 | .then(CommandManager.argument("second", IntegerArgumentType.integer(1)) 51 | .executes(it -> setScheduleInterval( 52 | it.getSource(), 53 | IntegerArgumentType.getInteger(it, "second"), "s") 54 | ) 55 | ) 56 | ).then(literal("minute") 57 | .then(CommandManager.argument("minute", IntegerArgumentType.integer(1)) 58 | .executes(it -> setScheduleInterval( 59 | it.getSource(), 60 | IntegerArgumentType.getInteger(it, "minute"), "m") 61 | ) 62 | ) 63 | ).then(literal("hour") 64 | .then(CommandManager.argument("hour", IntegerArgumentType.integer(1)) 65 | .executes(it -> setScheduleInterval( 66 | it.getSource(), 67 | IntegerArgumentType.getInteger(it, "hour"), "h") 68 | ) 69 | ) 70 | ).then(literal("day") 71 | .then(CommandManager.argument("day", IntegerArgumentType.integer(1)) 72 | .executes(it -> setScheduleInterval( 73 | it.getSource(), 74 | IntegerArgumentType.getInteger(it, "day"), "d") 75 | ) 76 | ) 77 | ) 78 | ) 79 | .then(literal("cron") 80 | .then(CommandManager.argument("cron", StringArgumentType.string()) 81 | .executes(it -> setScheduleCron(it.getSource(), StringArgumentType.getString(it, "cron"))) 82 | ) 83 | ) 84 | 85 | .then(literal("mode") 86 | .then(literal("set") 87 | .then(literal("interval") 88 | .executes(it -> switchMode(it.getSource(), "interval"))) 89 | .then(literal("cron") 90 | .executes(it -> switchMode(it.getSource(), "cron")))) 91 | .then(literal("get").executes(it -> getScheduleMode(it.getSource())))) 92 | ) 93 | .then(literal("get") 94 | .executes(it -> getNextBackupTime(it.getSource())) 95 | ) 96 | ) 97 | .then(literal("dataBase") 98 | .requires(it -> PermissionManager.hasPermission(it, 2, PermissionType.HELPER)) 99 | .then(literal("useInternalDataBase") 100 | .then(literal("set") 101 | .then(CommandManager.argument("value", BoolArgumentType.bool()) 102 | .executes(it -> setUseInternalDataBase(it.getSource(), BoolArgumentType.getBool(it, "value"))) 103 | ) 104 | ) 105 | ) 106 | ) 107 | .then(literal("restartMode") 108 | .requires(it -> PermissionManager.hasPermission(it, 4, PermissionType.ADMIN)) 109 | .then(literal("set") 110 | .then(CommandManager.argument("mode", StringArgumentType.string()) 111 | .suggests(AutoRestartModeSuggestionProvider.mode()) 112 | .executes(it -> setAutoRestartMode(it.getSource(), StringArgumentType.getString(it, "mode"))) 113 | ) 114 | ) 115 | .then(literal("get").executes(it -> getAutoRestartMode(it.getSource()))) 116 | ); 117 | 118 | private static int getAutoRestartMode(ServerCommandSource commandSource) { 119 | Messenger.sendMessage(commandSource, 120 | Messenger.literal(tr("quickbackupmulti.restartmode.get", QuickBackupMulti.config.getAutoRestartMode().name()))); 121 | return 1; 122 | } 123 | 124 | private static int setAutoRestartMode(ServerCommandSource commandSource, String mode) { 125 | AutoRestartMode newMode = AutoRestartMode.valueOf(mode.toUpperCase()); 126 | QuickBackupMulti.config.setAutoRestartMode(newMode); 127 | Messenger.sendMessage(commandSource, 128 | Messenger.literal(tr("quickbackupmulti.restartmode.switch", newMode.name()))); 129 | return 1; 130 | } 131 | 132 | private static int setUseInternalDataBase(ServerCommandSource commandSource, Boolean value) { 133 | if (value != QuickBackupMulti.config.getUseInternalDataBase()) { 134 | QuickBackupMulti.config.setUseInternalDataBase(value); 135 | try { 136 | if (value) { 137 | setDataStore(QuickBackupMulti.TEMP_CONFIG.worldName); 138 | } else { 139 | getDataBase().stopInternalMongoServer(); 140 | setDataStore(QuickBackupMulti.TEMP_CONFIG.worldName); 141 | } 142 | Messenger.sendMessage(commandSource, 143 | Messenger.literal(tr("quickbackupmulti.database.set_success"))); 144 | return 1; 145 | } catch (Exception e) { 146 | Messenger.sendMessage(commandSource, 147 | Messenger.literal(tr("quickbackupmulti.database.set_success_but", e.getMessage()))); 148 | return 0; 149 | } 150 | } else { 151 | Messenger.sendMessage( 152 | commandSource, 153 | Messenger.literal( 154 | tr("quickbackupmulti.database.set_fail", tr("quickbackupmulti.database.value_equal_config", value))) 155 | ); 156 | return 0; 157 | } 158 | 159 | } 160 | 161 | private static int getLang(ServerCommandSource commandSource) { 162 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.lang.get", QuickBackupMulti.config.getLang()))); 163 | return 1; 164 | } 165 | 166 | private static int setLang(ServerCommandSource commandSource, String lang) { 167 | if (!supportLanguage.contains(lang)) { 168 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.lang.failed"))); 169 | return 0; 170 | } 171 | Translate.handleResourceReload(lang); 172 | Messenger.sendMessage(commandSource, Text.of(tr("quickbackupmulti.lang.set", lang))); 173 | QuickBackupMulti.config.setLang(lang); 174 | return 1; 175 | } 176 | 177 | private static int switchMode(ServerCommandSource commandSource, String mode) { 178 | QuickBackupMulti.config.setScheduleMode(mode); 179 | return switchScheduleMode(commandSource, mode); 180 | } 181 | 182 | private static int setScheduleCron(ServerCommandSource commandSource, String value) { 183 | try { 184 | return ScheduleUtils.setScheduleCron(commandSource, value); 185 | } catch (SchedulerException e) { 186 | return 0; 187 | } 188 | } 189 | 190 | private static int setScheduleInterval(ServerCommandSource commandSource, int value, String type) { 191 | try { 192 | return ScheduleUtils.setScheduleInterval(commandSource, value, type); 193 | } catch (SchedulerException e) { 194 | Messenger.sendMessage(commandSource, 195 | Messenger.literal(tr("quickbackupmulti.schedule.cron.set_fail", e))); 196 | return 0; 197 | } 198 | } 199 | 200 | private static int disableScheduleBackup(ServerCommandSource commandSource) { 201 | return disableSchedule(commandSource); 202 | } 203 | 204 | private static int enableScheduleBackup(ServerCommandSource commandSource) { 205 | try { 206 | QuickBackupMulti.config.setScheduleBackup(true); 207 | if (QuickBackupMulti.TEMP_CONFIG.scheduler != null) { 208 | QuickBackupMulti.TEMP_CONFIG.scheduler.shutdown(); 209 | } 210 | startSchedule(commandSource); 211 | return 1; 212 | } catch (SchedulerException e) { 213 | Messenger.sendMessage( 214 | commandSource, 215 | Messenger.literal(tr("quickbackupmulti.schedule.enable.fail", e.toString())) 216 | ); 217 | return 0; 218 | } 219 | } 220 | 221 | public static int getScheduleMode(ServerCommandSource commandSource) { 222 | Messenger.sendMessage( 223 | commandSource, 224 | Text.of(tr("quickbackupmulti.schedule.mode.get", QuickBackupMulti.config.getScheduleMode())) 225 | ); 226 | return 1; 227 | } 228 | 229 | public static int getNextBackupTime(ServerCommandSource commandSource) { 230 | if (QuickBackupMulti.config.isScheduleBackup()) { 231 | String nextBackupTimeString = ""; 232 | switch (QuickBackupMulti.config.getScheduleMode()) { 233 | case "cron" : { 234 | nextBackupTimeString = getNextExecutionTime(QuickBackupMulti.config.getScheduleCron(), false); 235 | break; 236 | } 237 | case "interval" : { 238 | nextBackupTimeString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 239 | .format( 240 | QuickBackupMulti.TEMP_CONFIG.latestScheduleExecuteTime + QuickBackupMulti.config.getScheduleInterval() * 1000L 241 | ); 242 | break; 243 | } 244 | } 245 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.get", nextBackupTimeString))); 246 | return 1; 247 | } else { 248 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.get_fail"))); 249 | return 0; 250 | } 251 | } 252 | 253 | } 254 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/command/permission/PermissionManager.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.command.permission; 2 | 3 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 4 | import io.github.skydynamic.quickbackupmulti.QbmConstant; 5 | import net.minecraft.server.MinecraftServer; 6 | import net.minecraft.server.command.ServerCommandSource; 7 | import net.minecraft.server.network.ServerPlayerEntity; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.io.File; 11 | import java.io.FileReader; 12 | import java.io.FileWriter; 13 | import java.io.IOException; 14 | import java.nio.file.Path; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.GSON; 19 | import static io.github.skydynamic.quickbackupmulti.QbmConstant.permissionManager; 20 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.getPlayerFromCommandSource; 21 | 22 | public class PermissionManager { 23 | private static final Path configPath = QbmConstant.pathGetter.getConfigPath(); 24 | private static final File config = configPath.resolve("QuickBackupMulti-Permission.json").toFile(); 25 | private PermissionConfig permissionConfig; 26 | 27 | public PermissionManager() { 28 | if (!config.exists()) { 29 | initPermission(); 30 | } else { 31 | loadPermissionByFile(); 32 | } 33 | } 34 | 35 | public void setPermissionByPermissionLevelInt(int level, String playerName) { 36 | this.permissionConfig.setByPermissionType(PermissionType.getByLevelInt(level), playerName); 37 | } 38 | 39 | public void setPermissionByPermissionType(PermissionType permission, String playerName) { 40 | this.permissionConfig.setByPermissionType(permission, playerName); 41 | } 42 | 43 | public PermissionType getPlayerPermission(String name) { 44 | return permissionConfig.perm.getOrDefault(name, PermissionType.USER); 45 | } 46 | 47 | public int getPlayerPermissionLevel(String player) { 48 | return getPlayerPermission(player).level; 49 | } 50 | 51 | private void loadPermissionByFile() { 52 | try { 53 | FileReader reader = new FileReader(config); 54 | this.permissionConfig = GSON.fromJson(reader, PermissionConfig.class); 55 | reader.close(); 56 | } catch (IOException e) { 57 | throw new RuntimeException(e); 58 | } 59 | } 60 | 61 | public void savePermissionToFile() { 62 | try { 63 | if (config.exists()) config.delete(); 64 | if (!config.exists()) config.createNewFile(); 65 | FileWriter writer = new FileWriter(config); 66 | GSON.toJson(this.permissionConfig, writer); 67 | writer.close(); 68 | } catch (IOException e) { 69 | throw new RuntimeException(e); 70 | } 71 | } 72 | 73 | public void reloadPermission() { 74 | loadPermissionByFile(); 75 | } 76 | 77 | public void initPermission() { 78 | try { 79 | this.permissionConfig = new PermissionConfig(); 80 | config.createNewFile(); 81 | FileWriter writer = new FileWriter(config); 82 | GSON.toJson(this.permissionConfig, writer); 83 | writer.close(); 84 | } catch (IOException e) { 85 | throw new RuntimeException(e); 86 | } 87 | } 88 | 89 | public static boolean hasPermission( 90 | @NotNull ServerCommandSource source, 91 | int mcPermission, 92 | PermissionType modPermission 93 | ) { 94 | ServerPlayerEntity player = getPlayerFromCommandSource(source); 95 | if (player != null) { 96 | if (checkLocalGamePermission(source)) { 97 | return true; 98 | } else { 99 | return source.hasPermissionLevel(mcPermission) 100 | || permissionManager.getPlayerPermissionLevel(player.getName().getString()) >= modPermission.level; 101 | } 102 | } 103 | return true; 104 | } 105 | 106 | public static boolean checkLocalGamePermission(@NotNull ServerCommandSource source) { 107 | try { 108 | return getPermission(source); 109 | } catch (CommandSyntaxException e) { 110 | return false; 111 | } 112 | } 113 | 114 | private static boolean getPermission(ServerCommandSource source) throws CommandSyntaxException { 115 | boolean flag = source.hasPermissionLevel(4); 116 | ServerPlayerEntity player; 117 | MinecraftServer server; 118 | if (!flag && (server = source.getServer()).isSingleplayer() && (player = source.getPlayer()) != null) { 119 | flag = server.isHost(player.getGameProfile()); 120 | } 121 | return flag; 122 | } 123 | 124 | static class PermissionConfig { 125 | private final Map perm = new HashMap<>(); 126 | public void setByPermissionType(PermissionType type, String name) { 127 | perm.put(name, type); 128 | permissionManager.savePermissionToFile(); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/command/permission/PermissionType.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.command.permission; 2 | 3 | public enum PermissionType { 4 | USER(0), 5 | HELPER(1), 6 | ADMIN(2); 7 | 8 | final int level; 9 | 10 | PermissionType(int level) { 11 | this.level = level; 12 | } 13 | 14 | public static PermissionType getByLevelInt(int level) { 15 | for (PermissionType type : PermissionType.values()) { 16 | if(type.level == level) { 17 | return type; 18 | } 19 | } 20 | throw new IllegalArgumentException("Level is invalid"); 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/command/suggestion/AutoRestartModeSuggestionProvider.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.command.suggestion; 2 | 3 | import io.github.skydynamic.quickbackupmulti.config.AutoRestartMode; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class AutoRestartModeSuggestionProvider { 9 | public static CustomSuggestionProvider mode() { 10 | List list = new ArrayList<>(); 11 | for (AutoRestartMode mode : AutoRestartMode.values()) { 12 | list.add(mode.name()); 13 | } 14 | return CustomSuggestionProvider.suggestion(list); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/command/suggestion/CustomSuggestionProvider.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.command.suggestion; 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.List; 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | public class CustomSuggestionProvider implements SuggestionProvider { 13 | List list; 14 | 15 | public CustomSuggestionProvider(List list) { 16 | this.list = list; 17 | } 18 | 19 | @Override 20 | public CompletableFuture getSuggestions(CommandContext context, SuggestionsBuilder builder) { 21 | for (String value : list) { 22 | builder.suggest(value); 23 | } 24 | return builder.buildFuture(); 25 | } 26 | 27 | public static CustomSuggestionProvider suggestion(List suggestionList) { 28 | return new CustomSuggestionProvider(suggestionList); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/command/suggestion/LangSuggestionProvider.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.command.suggestion; 2 | 3 | import io.github.skydynamic.quickbackupmulti.i18n.Translate; 4 | 5 | public class LangSuggestionProvider { 6 | public static CustomSuggestionProvider lang() { 7 | return CustomSuggestionProvider.suggestion(Translate.supportLanguage.stream().toList()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/config/AutoRestartMode.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.config; 2 | 3 | public enum AutoRestartMode { 4 | DISABLE, 5 | DEFAULT, 6 | MCSM 7 | } 8 | -------------------------------------------------------------------------------- /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.jetbrains.annotations.NotNull; 6 | import org.jetbrains.annotations.Nullable; 7 | import org.quartz.Scheduler; 8 | 9 | public class QbmTempConfig { 10 | public Boolean isBackup = false; 11 | public MinecraftServer server; 12 | public String backupSlot; 13 | public EnvType env; 14 | public String worldName; 15 | public @Nullable Scheduler scheduler; 16 | public long latestScheduleExecuteTime; 17 | public String modVersion; 18 | 19 | public void setIsBackupValue(Boolean value) { 20 | this.isBackup = value; 21 | } 22 | 23 | public void setServerValue(MinecraftServer server) { 24 | this.server = server; 25 | } 26 | 27 | public void setBackupSlot(String slot) { 28 | this.backupSlot = slot; 29 | } 30 | 31 | public void setEnv(EnvType env) { 32 | this.env = env; 33 | } 34 | 35 | public void setWorldName(String worldName) { 36 | this.worldName = worldName; 37 | } 38 | 39 | public void setScheduler(@NotNull Scheduler scheduler) { 40 | this.scheduler = scheduler; 41 | } 42 | 43 | public void setLatestScheduleExecuteTime(long time) { 44 | this.latestScheduleExecuteTime = time; 45 | } 46 | 47 | public void setModVersion(String modVersion) { 48 | this.modVersion = modVersion; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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.Interface.IConfig; 4 | import io.github.skydynamic.quickbackupmulti.QbmConstant; 5 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.BufferedWriter; 9 | import java.io.IOException; 10 | import java.nio.charset.StandardCharsets; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.util.ArrayList; 14 | 15 | public class QuickBackupMultiConfig implements IConfig { 16 | private ConfigStorage config = new ConfigStorage(); 17 | 18 | private final Path path; 19 | 20 | public QuickBackupMultiConfig(final Path path) { 21 | this.path = path; 22 | } 23 | 24 | public ConfigStorage getConfig() { 25 | return config; 26 | } 27 | 28 | public void setConfig(final ConfigStorage config) { 29 | this.config = config; 30 | } 31 | 32 | public boolean save() { 33 | if (!Files.exists(path)) { 34 | try { 35 | Files.createFile(path); 36 | } catch (IOException e) { 37 | QuickBackupMulti.LOGGER.error("Save {} error: create file failed.", path, e); 38 | return false; 39 | } 40 | } 41 | try (BufferedWriter bfw = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) { 42 | QbmConstant.GSON.toJson(getConfig(), bfw); 43 | } catch (IOException e) { 44 | QuickBackupMulti.LOGGER.error("Save {} error: write file failed.", path, e); 45 | return false; 46 | } 47 | return true; 48 | } 49 | 50 | public boolean load() { 51 | if (!Files.exists(path)) { 52 | return save(); 53 | } 54 | 55 | try (BufferedReader bfr = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { 56 | setConfig(QbmConstant.GSON.fromJson(bfr, ConfigStorage.class)); 57 | } catch (IOException e) { 58 | QuickBackupMulti.LOGGER.error("Load {} error: read file failed.", path, e); 59 | return false; 60 | } 61 | return true; 62 | } 63 | 64 | public boolean isCheckUpdate() { 65 | return config.checkUpdate; 66 | } 67 | 68 | public ArrayList getIgnoredFiles() { 69 | ArrayList ignoredFiles = new ArrayList<>(config.ignoredFiles); 70 | ignoredFiles.add("session.lock"); 71 | return ignoredFiles; 72 | } 73 | 74 | 75 | public ArrayList getIgnoredFolders() { 76 | return config.ignoredFolders; 77 | } 78 | 79 | 80 | public String getLang() { 81 | return config.lang; 82 | } 83 | 84 | public void setLang(String lang) { 85 | config.lang = lang; 86 | } 87 | 88 | public boolean isScheduleBackup() { 89 | return config.scheduleBackup; 90 | } 91 | 92 | public void setScheduleBackup(boolean scheduleBackup) { 93 | config.scheduleBackup = scheduleBackup; 94 | } 95 | 96 | public String getScheduleCron() { 97 | return config.scheduleCron; 98 | } 99 | 100 | public void setScheduleCron(String scheduleCron) { 101 | config.scheduleCron = scheduleCron; 102 | } 103 | 104 | public int getScheduleInterval() { 105 | return config.scheduleInterval; 106 | } 107 | 108 | public void setScheduleInterval(int scheduleInterval) { 109 | config.scheduleInterval = scheduleInterval; 110 | } 111 | 112 | public String getScheduleMode() { 113 | return config.scheduleMode; 114 | } 115 | 116 | public void setScheduleMode(String scheduleMode) { 117 | config.scheduleMode = scheduleMode; 118 | } 119 | 120 | public int getMaxScheduleBackup() { 121 | return config.maxScheduleBackup; 122 | } 123 | 124 | public AutoRestartMode getAutoRestartMode() { 125 | return config.autoRestartMode; 126 | } 127 | 128 | public void setAutoRestartMode(AutoRestartMode autoRestartMode) { 129 | config.autoRestartMode = autoRestartMode; 130 | } 131 | 132 | 133 | @Override 134 | public boolean getUseInternalDataBase() { 135 | return config.useInternalDataBase; 136 | } 137 | 138 | public void setUseInternalDataBase(boolean useInternalDataBase) { 139 | config.useInternalDataBase = useInternalDataBase; 140 | } 141 | 142 | @Override 143 | public String getMongoDBUri() { 144 | return config.mongoDBUri; 145 | } 146 | 147 | @Override 148 | public String getStoragePath() { 149 | return config.storagePath; 150 | } 151 | 152 | @SuppressWarnings("FieldMayBeFinal") 153 | public static class ConfigStorage { 154 | private boolean checkUpdate = true; 155 | private ArrayList ignoredFiles = new ArrayList<>(); 156 | private ArrayList ignoredFolders = new ArrayList<>(); 157 | private String lang = "zh_cn"; 158 | private boolean scheduleBackup = false; 159 | private String scheduleCron = "* * 0/4 * * ?"; 160 | private int scheduleInterval = 14400; 161 | private String scheduleMode = "interval"; 162 | private int maxScheduleBackup = 10; 163 | 164 | private AutoRestartMode autoRestartMode = AutoRestartMode.DEFAULT; 165 | 166 | private boolean useInternalDataBase = true; 167 | private String mongoDBUri = "mongodb://localhost:27017"; 168 | private String storagePath = "QuickBackupMulti"; 169 | 170 | public void setScheduleMode(String scheduleMode) { 171 | this.scheduleMode = scheduleMode; 172 | } 173 | 174 | public void setScheduleBackup(boolean scheduleBackup) { 175 | this.scheduleBackup = scheduleBackup; 176 | } 177 | 178 | public void setScheduleCron(String scheduleCron) { 179 | this.scheduleCron = scheduleCron; 180 | } 181 | 182 | public void setScheduleInterval(int scheduleInterval) { 183 | this.scheduleInterval = scheduleInterval; 184 | } 185 | 186 | public void setLang(String lang) { 187 | this.lang = lang; 188 | } 189 | 190 | public boolean isScheduleBackup() { 191 | return scheduleBackup; 192 | } 193 | 194 | public String getScheduleCron() { 195 | return scheduleCron; 196 | } 197 | 198 | public int getScheduleInterval() { 199 | return scheduleInterval; 200 | } 201 | 202 | public String getScheduleMode() { 203 | return scheduleMode; 204 | } 205 | 206 | public String getLang() { 207 | return lang; 208 | } 209 | 210 | @Override 211 | public String toString() { 212 | return "ConfigStorage [checkUpdate=" + checkUpdate + ", ignoredFiles=" + ignoredFiles 213 | + ", ignoredFolders=" + ignoredFolders + ", lang=" + lang + ", scheduleBackup=" + scheduleBackup 214 | + ", scheduleCron=" + scheduleCron + ", scheduleInterval=" + scheduleInterval 215 | + ", scheduleMode=" + scheduleMode + ", maxScheduleBackup=" + maxScheduleBackup 216 | + ", autoRestartMode=" + autoRestartMode + ", useInternalDataBase=" 217 | + useInternalDataBase + ", mongoDBUri=" + mongoDBUri + ", storagePath=" 218 | + storagePath + "]"; 219 | } 220 | } 221 | } 222 | 223 | -------------------------------------------------------------------------------- /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", "zh_tw", "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/client/ClientPlayNetworkHandlerMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.mixin.client; 2 | 3 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 4 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 5 | import io.github.skydynamic.quickbackupmulti.utils.UpdateChecker; 6 | import net.minecraft.client.MinecraftClient; 7 | import net.minecraft.client.network.ClientPlayNetworkHandler; 8 | import net.minecraft.client.network.ClientPlayerEntity; 9 | import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket; 10 | import net.minecraft.text.ClickEvent; 11 | import net.minecraft.text.HoverEvent; 12 | import net.minecraft.text.MutableText; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Inject; 16 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 17 | 18 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 19 | 20 | @Mixin(ClientPlayNetworkHandler.class) 21 | public class ClientPlayNetworkHandlerMixin { 22 | @Inject( 23 | method = "onGameJoin", 24 | at = @At("TAIL") 25 | ) 26 | private void showUpdateMsg(GameJoinS2CPacket packet, CallbackInfo ci) { 27 | if (MinecraftClient.getInstance().player == null) { 28 | return; 29 | } 30 | UpdateChecker checker = QuickBackupMulti.updateChecker; 31 | if (checker.needUpdate) { 32 | ClientPlayerEntity player = MinecraftClient.getInstance().player; 33 | MutableText updateText = Messenger.literal( 34 | tr("quickbackupmulti.check_update.on_player_join", checker.latestVersion) 35 | ); 36 | updateText.styled(style -> style 37 | .withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, checker.latestVersionHtmUrl)) 38 | .withHoverEvent(new HoverEvent( 39 | HoverEvent.Action.SHOW_TEXT, Messenger.literal(tr("quickbackupmulti.check_update.click_tooltip")) 40 | )) 41 | ); 42 | player.sendMessage(updateText, false); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/mixin/client/MinecraftClientMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.mixin.client; 2 | 3 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 4 | import net.minecraft.client.MinecraftClient; 5 | import net.minecraft.client.toast.SystemToast; 6 | import net.minecraft.client.toast.ToastManager; 7 | import net.minecraft.text.Text; 8 | import org.spongepowered.asm.mixin.Final; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.injection.At; 12 | import org.spongepowered.asm.mixin.injection.Inject; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | 15 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 16 | 17 | @Mixin(MinecraftClient.class) 18 | public abstract class MinecraftClientMixin { 19 | @Shadow @Final 20 | public ToastManager toastManager; 21 | 22 | @Inject(method = "setScreen", at = @At("RETURN")) 23 | private void inj(CallbackInfo ci) { 24 | if (QuickBackupMulti.TEMP_CONFIG.isBackup) { 25 | Text title = Text.of(tr("quickbackupmulti.toast.start_title")); 26 | Text content = Text.of(tr("quickbackupmulti.toast.start_content")); 27 | //#if MC>=11800 28 | SystemToast.show(this.toastManager, SystemToast.Type.PERIODIC_NOTIFICATION, title, content); 29 | //#else 30 | //$$ SystemToast.show(this.toastManager, SystemToast.Type.WORLD_BACKUP, title, content); 31 | //#endif 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/mixin/client/MinecraftServerMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.mixin.client; 2 | 3 | //#if MC<11900 4 | //$$ import com.mojang.authlib.GameProfileRepository; 5 | //$$ import com.mojang.authlib.minecraft.MinecraftSessionService; 6 | //$$ import net.minecraft.util.UserCache; 7 | //#else 8 | import net.minecraft.util.ApiServices; 9 | //#endif 10 | import com.mojang.datafixers.DataFixer; 11 | import io.github.skydynamic.quickbackupmulti.QbmConstant; 12 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 13 | import io.github.skydynamic.quickbackupmulti.utils.QbmManager; 14 | import net.fabricmc.api.EnvType; 15 | import net.fabricmc.api.Environment; 16 | import net.minecraft.resource.ResourcePackManager; 17 | import net.minecraft.server.MinecraftServer; 18 | import net.minecraft.server.SaveLoader; 19 | import net.minecraft.server.WorldGenerationProgressListenerFactory; 20 | import net.minecraft.server.integrated.IntegratedServer; 21 | import net.minecraft.util.WorldSavePath; 22 | import net.minecraft.world.level.storage.LevelStorage; 23 | import org.spongepowered.asm.mixin.Mixin; 24 | import org.spongepowered.asm.mixin.injection.At; 25 | import org.spongepowered.asm.mixin.injection.Inject; 26 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 27 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 28 | 29 | import java.net.Proxy; 30 | import java.nio.file.Path; 31 | 32 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.setDataStore; 33 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.createBackupDir; 34 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.ScheduleUtils.shutdownSchedule; 35 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.ScheduleUtils.startSchedule; 36 | 37 | @Environment(EnvType.CLIENT) 38 | @Mixin(IntegratedServer.class) 39 | public abstract class MinecraftServerMixin extends MinecraftServer { 40 | //#if MC>=11900 41 | public MinecraftServerMixin( 42 | Thread serverThread, LevelStorage.Session session, 43 | ResourcePackManager dataPackManager, SaveLoader saveLoader, 44 | Proxy proxy, DataFixer dataFixer, ApiServices apiServices, 45 | WorldGenerationProgressListenerFactory worldGenerationProgressListenerFactory 46 | ) { 47 | super(serverThread, session, dataPackManager, saveLoader, proxy, dataFixer, apiServices, worldGenerationProgressListenerFactory); 48 | } 49 | //#else 50 | //$$ public MinecraftServerMixin(Thread serverThread, LevelStorage.Session session, ResourcePackManager dataPackManager, SaveLoader saveLoader, Proxy proxy, DataFixer dataFixer, MinecraftSessionService sessionService, GameProfileRepository gameProfileRepo, UserCache userCache, WorldGenerationProgressListenerFactory worldGenerationProgressListenerFactory) { 51 | //$$ super(serverThread, session, dataPackManager, saveLoader, proxy, dataFixer, sessionService, gameProfileRepo, userCache, worldGenerationProgressListenerFactory); 52 | //$$ } 53 | //#endif 54 | 55 | @Inject( 56 | method = "setupServer", 57 | at = @At( 58 | value = "INVOKE", 59 | target = "Lnet/minecraft/server/integrated/IntegratedServer;loadWorld()V", 60 | shift = At.Shift.AFTER 61 | ) 62 | ) 63 | private void initQuickBackupMultiClient(CallbackInfoReturnable cir) { 64 | QuickBackupMulti.TEMP_CONFIG.setServerValue(this); 65 | Path saveDirectoryPath = this.getSavePath(WorldSavePath.ROOT); 66 | String worldName = saveDirectoryPath.getParent().getFileName().toString(); 67 | QuickBackupMulti.TEMP_CONFIG.setWorldName(worldName); 68 | Path backupDir = Path.of(QbmConstant.pathGetter.getGamePath() + "/QuickBackupMulti/").resolve(worldName); 69 | QbmManager.savePath = saveDirectoryPath; 70 | createBackupDir(backupDir); 71 | setDataStore(worldName); 72 | startSchedule(); 73 | } 74 | 75 | @Inject(method = "shutdown", at = @At("HEAD")) 76 | private void stopSchedule(CallbackInfo ci) { 77 | shutdownSchedule(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/mixin/client/WorldEntryMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.mixin.client; 2 | 3 | import io.github.skydynamic.quickbackupmulti.utils.QbmManager; 4 | import net.minecraft.client.gui.screen.world.WorldListWidget; 5 | import net.minecraft.world.level.storage.LevelSummary; 6 | import org.spongepowered.asm.mixin.Final; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.Shadow; 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 | @Mixin(WorldListWidget.WorldEntry.class) 14 | public class WorldEntryMixin { 15 | @Shadow @Final private LevelSummary level; 16 | 17 | @Inject( 18 | method = "delete", 19 | at = @At( 20 | value = "RETURN" 21 | ) 22 | ) 23 | private void onDelete(CallbackInfo ci) 24 | { 25 | String savePathName = this.level.getName(); 26 | QbmManager.deleteWorld(savePathName); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/mixin/server/MinecraftServerMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.mixin.server; 2 | 3 | //#if MC<11900 4 | //$$ import com.mojang.authlib.GameProfileRepository; 5 | //$$ import com.mojang.authlib.minecraft.MinecraftSessionService; 6 | //$$ import net.minecraft.util.UserCache; 7 | //#else 8 | import net.minecraft.util.ApiServices; 9 | //#endif 10 | import com.mojang.datafixers.DataFixer; 11 | import io.github.skydynamic.quickbackupmulti.QbmConstant; 12 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 13 | import io.github.skydynamic.quickbackupmulti.utils.QbmManager; 14 | import net.fabricmc.api.Environment; 15 | import net.fabricmc.api.EnvType; 16 | import net.minecraft.resource.ResourcePackManager; 17 | import net.minecraft.server.MinecraftServer; 18 | import net.minecraft.server.SaveLoader; 19 | import net.minecraft.server.WorldGenerationProgressListenerFactory; 20 | import net.minecraft.server.dedicated.MinecraftDedicatedServer; 21 | import net.minecraft.util.WorldSavePath; 22 | import net.minecraft.world.level.storage.LevelStorage; 23 | import org.spongepowered.asm.mixin.Mixin; 24 | import org.spongepowered.asm.mixin.injection.At; 25 | import org.spongepowered.asm.mixin.injection.Inject; 26 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 27 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 28 | 29 | import java.net.Proxy; 30 | import java.nio.file.Path; 31 | 32 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getDataBase; 33 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.setDataStore; 34 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.createBackupDir; 35 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.ScheduleUtils.shutdownSchedule; 36 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.ScheduleUtils.startSchedule; 37 | 38 | @Environment(EnvType.SERVER) 39 | @Mixin(MinecraftDedicatedServer.class) 40 | public abstract class MinecraftServerMixin extends MinecraftServer { 41 | //#if MC>=11900 42 | public MinecraftServerMixin( 43 | Thread serverThread, LevelStorage.Session session, 44 | ResourcePackManager dataPackManager, SaveLoader saveLoader, 45 | Proxy proxy, DataFixer dataFixer, ApiServices apiServices, 46 | WorldGenerationProgressListenerFactory worldGenerationProgressListenerFactory 47 | ) { 48 | super(serverThread, session, dataPackManager, saveLoader, proxy, dataFixer, apiServices, worldGenerationProgressListenerFactory); 49 | } 50 | //#else 51 | //$$ public MinecraftServerMixin(Thread serverThread, LevelStorage.Session session, ResourcePackManager dataPackManager, SaveLoader saveLoader, Proxy proxy, DataFixer dataFixer, MinecraftSessionService sessionService, GameProfileRepository gameProfileRepo, UserCache userCache, WorldGenerationProgressListenerFactory worldGenerationProgressListenerFactory) { 52 | //$$ super(serverThread, session, dataPackManager, saveLoader, proxy, dataFixer, sessionService, gameProfileRepo, userCache, worldGenerationProgressListenerFactory); 53 | //$$ } 54 | //#endif 55 | 56 | @Inject(method = "", at = @At("TAIL")) 57 | private void setServer(CallbackInfo ci) { 58 | QuickBackupMulti.TEMP_CONFIG.setServerValue(this); 59 | } 60 | 61 | @Inject( 62 | method = "setupServer", 63 | at = @At( 64 | value = "INVOKE", 65 | target = "Lnet/minecraft/server/dedicated/MinecraftDedicatedServer;loadWorld()V", 66 | shift = At.Shift.AFTER 67 | ) 68 | ) 69 | private void initQuickBackupMulti(CallbackInfoReturnable cir) { 70 | Path backupDir = Path.of(QbmConstant.pathGetter.getGamePath() + "/QuickBackupMulti/"); 71 | QuickBackupMulti.TEMP_CONFIG.setWorldName(""); 72 | QbmManager.savePath = this.getSavePath(WorldSavePath.ROOT); 73 | createBackupDir(backupDir); 74 | setDataStore("server"); 75 | startSchedule(); 76 | } 77 | 78 | @Inject(method = "shutdown", at = @At("HEAD")) 79 | private void injectShutDown(CallbackInfo ci) { 80 | shutdownSchedule(); 81 | if (!QuickBackupMulti.TEMP_CONFIG.isBackup) getDataBase().stopInternalMongoServer(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /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 | import java.util.Map; 17 | import java.util.stream.Collectors; 18 | 19 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 20 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getDataBase; 21 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getStorager; 22 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 23 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.getBackupDir; 24 | import static io.github.skydynamic.quickbackupmulti.utils.QbmManager.getBackupsList; 25 | 26 | public class ListUtils { 27 | private static long getDirSize(File dir) { 28 | return FileUtils.sizeOf(dir); 29 | } 30 | 31 | public static String truncateString(String str, int maxLength) { 32 | if (str.length() > maxLength) { 33 | return str.substring(0, maxLength - 3) + "..."; 34 | } else { 35 | return str; 36 | } 37 | } 38 | 39 | private static int getPageCount(List backupsDirList, int page) { 40 | int size = backupsDirList.size(); 41 | if (!(size < 5 * page)) { 42 | return 5; 43 | } else if (size < 5 * page && (size < 5 && size > 0)) { 44 | return size; 45 | } else { 46 | return Math.max(size - 5 * (page - 1), 0); 47 | } 48 | } 49 | 50 | public static int getTotalPage(List backupsList) { 51 | return (int) Math.ceil(backupsList.size() / 5.0); 52 | } 53 | 54 | private static MutableText getBackPageText(int page, int totalPage) { 55 | MutableText backPageText; 56 | backPageText = Messenger.literal("[<-]"); 57 | backPageText.styled(style -> 58 | style.withHoverEvent( 59 | new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.of(tr("quickbackupmulti.list_backup.back_page"))) 60 | ) 61 | ); 62 | if (page != 1 && totalPage > 1) { 63 | backPageText.styled(style -> 64 | style.withClickEvent( 65 | new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/qb list " + (page - 1)) 66 | ) 67 | ).styled(style -> style.withColor(Formatting.AQUA)); 68 | } else if (page == 1) { 69 | backPageText.styled(style -> style.withColor(Formatting.DARK_GRAY)); 70 | } 71 | return backPageText; 72 | } 73 | 74 | private static MutableText getNextPageText(int page, int totalPage) { 75 | MutableText nextPageText; 76 | nextPageText = Messenger.literal("[->]"); 77 | nextPageText.styled(style -> 78 | style.withHoverEvent( 79 | new HoverEvent( 80 | HoverEvent.Action.SHOW_TEXT, 81 | Text.of(tr("quickbackupmulti.list_backup.next_page"))) 82 | ) 83 | ); 84 | if (page != totalPage && totalPage > 1) { 85 | nextPageText.styled(style -> 86 | style.withClickEvent( 87 | new ClickEvent( 88 | ClickEvent.Action.RUN_COMMAND, 89 | "/qb list " + (page + 1)) 90 | ) 91 | ).styled(style -> style.withColor(Formatting.AQUA)); 92 | } else if (page == totalPage) { 93 | nextPageText.styled(style -> style.withColor(Formatting.DARK_GRAY)); 94 | } 95 | return nextPageText; 96 | } 97 | 98 | // private static MutableText getSlotText(String name, int page, int num, long backupSizeB) throws IOException { 99 | private static MutableText getSlotText(Map.Entry entry, int page, int num, long backupSizeB) throws IOException { 100 | String name = entry.getKey(); 101 | MutableText backText = Messenger.literal("§2[▷] "); 102 | MutableText deleteText = Messenger.literal("§c[×] "); 103 | MutableText nameText = Messenger.literal("§6" + truncateString(name, 8) + "§r "); 104 | MutableText resultText = Messenger.literal(""); 105 | // StorageInfo result = getDataBase().getStorageInfo(name); 106 | StorageInfo result = entry.getValue(); 107 | 108 | backText.styled(style -> 109 | style.withClickEvent( 110 | new ClickEvent( 111 | ClickEvent.Action.SUGGEST_COMMAND, 112 | "/qb back \"%s\"".formatted(name)) 113 | ) 114 | ).styled(style -> 115 | style.withHoverEvent( 116 | new HoverEvent( 117 | HoverEvent.Action.SHOW_TEXT, 118 | Text.of(tr("quickbackupmulti.list_backup.slot.restore", name))) 119 | ) 120 | ); 121 | 122 | deleteText.styled(style -> 123 | style.withClickEvent( 124 | new ClickEvent( 125 | ClickEvent.Action.SUGGEST_COMMAND, 126 | "/qb delete \"%s\"".formatted(name)) 127 | ) 128 | ).styled(style -> 129 | style.withHoverEvent( 130 | new HoverEvent( 131 | HoverEvent.Action.SHOW_TEXT, 132 | Text.of(tr("quickbackupmulti.list_backup.slot.delete", name))) 133 | ) 134 | ); 135 | 136 | nameText.styled(style -> 137 | style.withClickEvent( 138 | new ClickEvent( 139 | ClickEvent.Action.SUGGEST_COMMAND, 140 | "/qb show \"%s\"".formatted(name)) 141 | ) 142 | ).styled(style -> 143 | style.withHoverEvent( 144 | new HoverEvent( 145 | HoverEvent.Action.SHOW_TEXT, 146 | Text.of(tr("quickbackupmulti.list_backup.slot.show", name))) 147 | ) 148 | ); 149 | 150 | String desc = result.getDesc(); 151 | if (desc.isEmpty()) desc = tr("quickbackupmulti.empty_comment"); 152 | double backupSizeMB = (double) backupSizeB / FileUtils.ONE_MB; 153 | double backupSizeGB = (double) backupSizeB / FileUtils.ONE_GB; 154 | String sizeString = (backupSizeMB >= 1000) ? String.format("%.2fGB", backupSizeGB) : String.format("%.2fMB", backupSizeMB); 155 | resultText.append("\n" + tr("quickbackupmulti.list_backup.slot.header", num + (5 * (page - 1))) + " ") 156 | .append(nameText) 157 | .append(backText) 158 | .append(deleteText) 159 | .append("§a" + sizeString) 160 | .append( 161 | String.format( 162 | " §b%s§7: §r%s", 163 | new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(result.getTimestamp()), 164 | truncateString(desc, 10) 165 | ) 166 | ); 167 | return resultText; 168 | } 169 | 170 | public static MutableText list(int page) { 171 | long totalBackupSizeB = 0; 172 | Path backupDir = getBackupDir(); 173 | // List backupsList = getBackupsList(); 174 | List> backupsInfoList = getBackupsList().stream() 175 | .map(name -> Map.entry(name, getDataBase().getStorageInfo(name))) 176 | .sorted((c1, c2) -> -Long.compare(c1.getValue().getTimestamp(), c2.getValue().getTimestamp())) 177 | .collect(Collectors.toList()); 178 | // if (backupsList.isEmpty() || getPageCount(backupsList, page) == 0) { 179 | if (backupsInfoList.isEmpty() || getPageCount(backupsInfoList, page) == 0) { 180 | return Messenger.literal(tr("quickbackupmulti.list_empty")); 181 | } 182 | // int totalPage = getTotalPage(backupsList); 183 | int totalPage = getTotalPage(backupsInfoList); 184 | 185 | MutableText resultText = Messenger.literal(tr("quickbackupmulti.list_backup.title", page)); 186 | MutableText backPageText = getBackPageText(page, totalPage); 187 | MutableText nextPageText = getNextPageText(page, totalPage); 188 | resultText.append("\n") 189 | .append(backPageText) 190 | .append(" ") 191 | .append(tr("quickbackupmulti.list_backup.page_msg", page, totalPage)) 192 | .append(" ") 193 | .append(nextPageText); 194 | // for (int j = 1; j <= getPageCount(backupsList, page); j++) { 195 | for (int j = 1; j <= getPageCount(backupsInfoList, page); j++) { 196 | try { 197 | // String name = backupsList.get(((j - 1) + 5 * (page - 1))); 198 | Map.Entry entry = backupsInfoList.get(((j - 1) + 5 * (page - 1))); 199 | String name = entry.getKey(); 200 | long backupSizeB = getDirSize(backupDir.resolve(name).toFile()); 201 | totalBackupSizeB += backupSizeB; 202 | // resultText.append(getSlotText(name, page, j, backupSizeB)); 203 | resultText.append(getSlotText(entry, page, j, backupSizeB)); 204 | } catch (IOException e) { 205 | LOGGER.error("", e); 206 | } 207 | } 208 | double totalBackupSizeMB = (double) totalBackupSizeB / FileUtils.ONE_MB; 209 | double totalBackupSizeGB = (double) totalBackupSizeB / FileUtils.ONE_GB; 210 | String sizeString = 211 | (totalBackupSizeMB >= 1000) 212 | ? String.format("%.2fGB", totalBackupSizeGB) 213 | : String.format("%.2fMB", totalBackupSizeMB); 214 | resultText.append("\n" + tr("quickbackupmulti.list_backup.slot.total_space", sizeString)); 215 | return resultText; 216 | } 217 | 218 | public static MutableText search(List searchResultList) { 219 | MutableText resultText = Messenger.literal(tr("quickbackupmulti.search.success")); 220 | Path backupDir = getBackupDir(); 221 | for (int i = 1; i <= searchResultList.size(); i++) { 222 | try { 223 | String name = searchResultList.get(i - 1); 224 | StorageInfo result = getDataBase().getStorageInfo(name); 225 | long backupSizeB = getDirSize(backupDir.resolve(name).toFile()); 226 | resultText.append(getSlotText(Map.entry(name, result), 1, i, backupSizeB)); 227 | } catch (IOException e) { 228 | LOGGER.error("", e); 229 | } 230 | } 231 | return resultText; 232 | } 233 | 234 | public static MutableText show(String name) { 235 | MutableText resultText; 236 | if (getStorager().storageExists(name)) { 237 | StorageInfo backupInfo = getDataBase().getStorageInfo(name); 238 | resultText = Messenger.literal(tr("quickbackupmulti.show.header")); 239 | String desc = backupInfo.getDesc(); 240 | if (desc.isEmpty()) desc = tr("quickbackupmulti.empty_comment"); 241 | 242 | MutableText backText = Messenger.literal(tr("quickbackupmulti.show.back_button")); 243 | MutableText deleteText = Messenger.literal(tr("quickbackupmulti.show.delete_button")); 244 | backText.styled(style -> 245 | style.withClickEvent( 246 | new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/qb back \"%s\"".formatted(name)) 247 | ) 248 | ).styled(style -> 249 | style.withHoverEvent( 250 | new HoverEvent( 251 | HoverEvent.Action.SHOW_TEXT, 252 | Text.of(tr("quickbackupmulti.list_backup.slot.restore", name))) 253 | ) 254 | ); 255 | deleteText.styled(style -> 256 | style.withClickEvent( 257 | new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/qb delete \"%s\"".formatted(name))) 258 | ).styled(style -> 259 | style.withHoverEvent( 260 | new HoverEvent( 261 | HoverEvent.Action.SHOW_TEXT, 262 | Text.of(tr("quickbackupmulti.list_backup.slot.delete", name))) 263 | ) 264 | ); 265 | 266 | resultText.append("\n") 267 | .append(tr("quickbackupmulti.show.name") + ": §r" + backupInfo.getName() + "\n") 268 | .append(tr("quickbackupmulti.show.desc") + ": §r" + desc + "\n") 269 | .append( 270 | tr("quickbackupmulti.show.time") 271 | + ": §r" 272 | + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(backupInfo.getTimestamp()) 273 | ) 274 | .append("\n") 275 | .append(backText) 276 | .append(" ") 277 | .append(deleteText); 278 | 279 | } else { 280 | resultText = Messenger.literal(tr("quickbackupmulti.show.fail")); 281 | resultText.styled(style -> style.withColor(Formatting.RED)); 282 | } 283 | return resultText; 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /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.QuickBackupMulti; 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, folderFilter); 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 (QuickBackupMulti.config.isScheduleBackup()) 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 | public static void sendMessage(ServerCommandSource commandSource, Text text) { 12 | //#if MC>=11900 13 | commandSource.sendMessage(text); 14 | //#else 15 | //$$ commandSource.sendFeedback(text, false); 16 | //#endif 17 | } 18 | 19 | public static MutableText literal(String string) { 20 | //#if MC>=11900 21 | return Text.literal(string); 22 | //#else 23 | //$$ return new LiteralText(string); 24 | //#endif 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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.database.index.type.StorageInfo; 4 | import io.github.skydynamic.increment.storage.lib.util.IndexUtil; 5 | import io.github.skydynamic.quickbackupmulti.QbmConstant; 6 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 7 | import net.fabricmc.api.EnvType; 8 | import net.minecraft.server.command.ServerCommandSource; 9 | import net.minecraft.server.network.ServerPlayerEntity; 10 | import org.apache.commons.io.FileUtils; 11 | import org.apache.commons.io.filefilter.IOFileFilter; 12 | import org.apache.commons.io.filefilter.NameFileFilter; 13 | import org.apache.commons.io.filefilter.NotFileFilter; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.nio.file.Path; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.Objects; 21 | import java.util.stream.Stream; 22 | 23 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.LOGGER; 24 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.deleteDataStore; 25 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getDataBase; 26 | import static io.github.skydynamic.quickbackupmulti.QuickBackupMulti.getStorager; 27 | 28 | public class QbmManager { 29 | public static Path backupDir = Path.of(QbmConstant.pathGetter.getGamePath() + "/QuickBackupMulti/"); 30 | public static Path savePath; 31 | public static IOFileFilter folderFilter = new NotFileFilter(new NameFileFilter(QuickBackupMulti.config.getIgnoredFolders())); 32 | public static IOFileFilter fileFilter = new NotFileFilter(new NameFileFilter(QuickBackupMulti.config.getIgnoredFiles())); 33 | 34 | public static Path getRootBackupDir() { 35 | return backupDir; 36 | } 37 | 38 | public static Path getBackupDir() { 39 | if (QuickBackupMulti.TEMP_CONFIG.env == EnvType.SERVER) { 40 | return getRootBackupDir(); 41 | } else { 42 | return getRootBackupDir().resolve(QuickBackupMulti.TEMP_CONFIG.worldName); 43 | } 44 | } 45 | 46 | public static void restore(String slot) { 47 | File targetBackupSlot = getBackupDir().resolve(slot).toFile(); 48 | try { 49 | for (File file : FileUtils.listFiles(savePath.toFile(), fileFilter, folderFilter)) { 50 | if (file.equals(savePath.toFile())) continue; 51 | FileUtils.forceDelete(file); 52 | } 53 | 54 | FileUtils.copyDirectory(targetBackupSlot, savePath.toFile()); 55 | IndexUtil.copyIndexFile( 56 | slot, 57 | Path.of(QuickBackupMulti.config.getStoragePath()).resolve(QuickBackupMulti.TEMP_CONFIG.worldName), 58 | savePath.toFile() 59 | ); 60 | } catch (IOException e) { 61 | LOGGER.error("Restore Failed", e); 62 | } 63 | } 64 | 65 | public static List getBackupsList() { 66 | List backupsDirList = new ArrayList<>(); 67 | for (File file : Objects.requireNonNull(getBackupDir().toFile().listFiles())) { 68 | if (file.isDirectory() && getStorager().storageExists(file.getName())) { 69 | backupsDirList.add(file.getName()); 70 | } 71 | } 72 | return backupsDirList; 73 | } 74 | 75 | public static boolean delete(String name) { 76 | if (getStorager().storageExists(name)) { 77 | try { 78 | IndexUtil.reIndex(name, QuickBackupMulti.TEMP_CONFIG.worldName); 79 | getStorager().deleteStorage(name); 80 | FileUtils.deleteDirectory(getBackupDir().resolve(name).toFile()); 81 | return true; 82 | } catch (SecurityException | IOException e) { 83 | LOGGER.error("Delete Backup Failed", e); 84 | return false; 85 | } 86 | } else return false; 87 | } 88 | 89 | public static void deleteWorld(String worldName) { 90 | try { 91 | FileUtils.deleteDirectory(getRootBackupDir().resolve(worldName).toFile()); 92 | deleteDataStore(worldName); 93 | } catch (IOException e) { 94 | LOGGER.error("Delete World Backup Data Failed", e); 95 | } 96 | } 97 | 98 | public static void createBackupDir(Path path) { 99 | if (!path.toFile().exists()) path.toFile().mkdirs(); 100 | } 101 | 102 | public static List getScheduleBackupList() { 103 | Stream backupStream = getDataBase().getDatastore().find(StorageInfo.class).stream(); 104 | return backupStream.filter(it -> it.getName().startsWith("ScheduleBackup-")).toList(); 105 | } 106 | 107 | public static ServerPlayerEntity getPlayerFromCommandSource(ServerCommandSource source) { 108 | //#if MC<11900 109 | //$$ try { 110 | //$$ return source.getPlayer(); 111 | //$$ } catch (Exception e) { 112 | //$$ throw new RuntimeException("Cannot get ServerPlayerEntity from ServerCommandSource"); 113 | //$$ } 114 | //#else 115 | return source.getPlayer(); 116 | //#endif 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/io/github/skydynamic/quickbackupmulti/utils/ScheduleUtils.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils; 2 | 3 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 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 | if (QuickBackupMulti.TEMP_CONFIG.scheduler != null) { 20 | QuickBackupMulti.TEMP_CONFIG.scheduler.shutdown(); 21 | } 22 | // 照顾Java8 23 | switch (QuickBackupMulti.config.getScheduleMode()) { 24 | case "cron": { 25 | nextBackupTimeString = CronUtil.getNextExecutionTime(QuickBackupMulti.config.getScheduleCron(), false); 26 | break; 27 | } 28 | case "interval": { 29 | nextBackupTimeString = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis() + QuickBackupMulti.config.getScheduleInterval() * 1000L); 30 | break; 31 | } 32 | } 33 | CronUtil.buildScheduler(); 34 | QuickBackupMulti.TEMP_CONFIG.scheduler.start(); 35 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.enable.success", nextBackupTimeString))); 36 | } catch (SchedulerException e) { 37 | LOGGER.error("Start schedule backup fail: ", e); 38 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.enable.fail", e.toString()))); 39 | } 40 | } 41 | 42 | public static int switchScheduleMode(ServerCommandSource commandSource, String mode) { 43 | try { 44 | if (QuickBackupMulti.config.isScheduleBackup()) { 45 | if (QuickBackupMulti.TEMP_CONFIG.scheduler.isStarted()) QuickBackupMulti.TEMP_CONFIG.scheduler.shutdown(); 46 | startSchedule(commandSource); 47 | } 48 | } catch (SchedulerException e) { 49 | LOGGER.error("Switch schedule mode backup fail: ", e); 50 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.switch.fail", e.toString()))); 51 | return 0; 52 | } 53 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.switch.set", mode))); 54 | return 1; 55 | } 56 | 57 | public static int disableSchedule(ServerCommandSource commandSource) { 58 | try { 59 | QuickBackupMulti.TEMP_CONFIG.scheduler.shutdown(); 60 | QuickBackupMulti.config.setScheduleBackup(false); 61 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.disable.success"))); 62 | return 1; 63 | } catch (SchedulerException e) { 64 | LOGGER.error("Close schedule backup fail: ", e); 65 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.disable.fail", e.toString()))); 66 | return 0; 67 | } 68 | } 69 | 70 | public static int setScheduleCron(ServerCommandSource commandSource, String value) throws SchedulerException { 71 | if (CronUtil.cronIsValid(value)) { 72 | if (QuickBackupMulti.TEMP_CONFIG.scheduler != null) { 73 | if (QuickBackupMulti.TEMP_CONFIG.scheduler.isStarted()) QuickBackupMulti.TEMP_CONFIG.scheduler.shutdown(); 74 | } 75 | QuickBackupMulti.config.setScheduleCron(value); 76 | if (QuickBackupMulti.config.isScheduleBackup()) { 77 | startSchedule(commandSource); 78 | if (QuickBackupMulti.config.getScheduleMode().equals("cron")) { 79 | Messenger.sendMessage(commandSource, 80 | Messenger.literal(Translate.tr("quickbackupmulti.schedule.cron.set_custom_success", CronUtil.getNextExecutionTime(QuickBackupMulti.config.getScheduleCron(), false)))); 81 | } 82 | } else { 83 | Messenger.sendMessage(commandSource, 84 | Messenger.literal(tr("quickbackupmulti.schedule.cron.set_success_only"))); 85 | } 86 | } else { 87 | Messenger.sendMessage(commandSource, Messenger.literal(tr("quickbackupmulti.schedule.cron.expression_error"))); 88 | return 0; 89 | } 90 | return 1; 91 | } 92 | 93 | public static int setScheduleInterval(ServerCommandSource commandSource, int value, String type) throws SchedulerException { 94 | if (QuickBackupMulti.TEMP_CONFIG.scheduler != null) { 95 | if (QuickBackupMulti.TEMP_CONFIG.scheduler.isStarted()) QuickBackupMulti.TEMP_CONFIG.scheduler.shutdown(); 96 | } 97 | switch (type) { 98 | case "s" : { 99 | QuickBackupMulti.config.setScheduleInterval(value); 100 | break; 101 | } 102 | case "m" : { 103 | QuickBackupMulti.config.setScheduleInterval(CronUtil.getSeconds(value, 0, 0)); 104 | break; 105 | } 106 | case "h" : { 107 | QuickBackupMulti.config.setScheduleInterval(CronUtil.getSeconds(0, value, 0)); 108 | break; 109 | } 110 | case "d" : { 111 | QuickBackupMulti.config.setScheduleInterval(CronUtil.getSeconds(0, 0, value)); 112 | break; 113 | } 114 | } 115 | if (QuickBackupMulti.config.isScheduleBackup()) { 116 | startSchedule(commandSource); 117 | if (QuickBackupMulti.config.getScheduleMode().equals("interval")) { 118 | Messenger.sendMessage(commandSource, 119 | Messenger.literal(tr("quickbackupmulti.schedule.cron.set_success", 120 | new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 121 | .format(System.currentTimeMillis() + QuickBackupMulti.config.getScheduleInterval() * 1000L)) 122 | ) 123 | ); 124 | } 125 | } else { 126 | Messenger.sendMessage(commandSource, 127 | Messenger.literal(tr("quickbackupmulti.schedule.cron.set_success_only"))); 128 | } 129 | return 1; 130 | } 131 | 132 | public static void setScheduleInterval(ServerCommandSource commandSource, int value) throws SchedulerException { 133 | setScheduleInterval(commandSource, value, "s"); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /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/UpdateChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.skydynamic.quickbackupmulti.utils; 2 | 3 | import com.google.gson.JsonObject; 4 | import com.google.gson.JsonParser; 5 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 6 | 7 | 8 | import java.io.IOException; 9 | import java.net.URI; 10 | import java.net.URISyntaxException; 11 | import java.net.http.HttpClient; 12 | import java.net.http.HttpRequest; 13 | import java.net.http.HttpResponse; 14 | 15 | public class UpdateChecker extends Thread { 16 | 17 | private static final HttpClient CLIENT = HttpClient.newHttpClient(); 18 | private static final String RELEASE_API_URL = "https://api.github.com/repos/QuickBackupMultiMod-Dev/QuickBackupM-Fabric/releases/latest"; 19 | 20 | public String latestVersion; 21 | public String latestVersionHtmUrl; 22 | public boolean needUpdate = false; 23 | 24 | public UpdateChecker() { 25 | super("QuickBackupM-Fabric-Update-Checker"); 26 | } 27 | 28 | @Override 29 | public void run() { 30 | try { 31 | if (QuickBackupMulti.TEMP_CONFIG.modVersion == null) { 32 | QuickBackupMulti.LOGGER.warn("Current mod version is not found."); 33 | return; 34 | } 35 | HttpResponse response = CLIENT.send( 36 | HttpRequest.newBuilder().uri(new URI(RELEASE_API_URL)).build(), 37 | HttpResponse.BodyHandlers.ofString() 38 | ); 39 | 40 | // Get Meta data 41 | JsonObject jsonObject = JsonParser.parseString(response.body()).getAsJsonObject(); 42 | latestVersion = jsonObject.get("tag_name").getAsString(); 43 | latestVersionHtmUrl = jsonObject.get("html_url").getAsString(); 44 | 45 | String tag = latestVersion 46 | .replaceAll("\\+.*", "") 47 | .replaceFirst("^v", ""); 48 | String currentVersion = QuickBackupMulti.TEMP_CONFIG.modVersion 49 | .replaceAll("\\+.*", "") 50 | .replaceFirst("^v", ""); 51 | 52 | if (tag.compareTo(currentVersion) > 0) { 53 | needUpdate = true; 54 | QuickBackupMulti.LOGGER.info( 55 | "{} has new version {}. You can see: {}", QuickBackupMulti.modName, latestVersion, latestVersionHtmUrl 56 | ); 57 | } 58 | } catch (IOException | InterruptedException | URISyntaxException e) { 59 | QuickBackupMulti.LOGGER.error("Check update failed", e); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /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.QuickBackupMulti; 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 (QuickBackupMulti.config.getScheduleMode().equals("cron")) { 19 | return TriggerBuilder.newTrigger() 20 | .withSchedule(CronScheduleBuilder.cronSchedule(QuickBackupMulti.config.getScheduleCron())) 21 | .startAt(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(getNextExecutionTime(QuickBackupMulti.config.getScheduleCron(), false))) 22 | .build(); 23 | } else { 24 | return TriggerBuilder.newTrigger() 25 | .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(QuickBackupMulti.config.getScheduleInterval()).repeatForever()) 26 | .startAt(new Date(System.currentTimeMillis() + QuickBackupMulti.config.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 | QuickBackupMulti.TEMP_CONFIG.setScheduler(sf.getScheduler()); 40 | QuickBackupMulti.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(QuickBackupMulti.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.increment.storage.lib.database.index.type.StorageInfo; 4 | import io.github.skydynamic.quickbackupmulti.QuickBackupMulti; 5 | import io.github.skydynamic.quickbackupmulti.utils.Messenger; 6 | import io.github.skydynamic.quickbackupmulti.utils.QbmManager; 7 | import net.minecraft.server.MinecraftServer; 8 | import net.minecraft.server.network.ServerPlayerEntity; 9 | import org.quartz.Job; 10 | import org.quartz.JobExecutionContext; 11 | 12 | import java.text.SimpleDateFormat; 13 | import java.util.ArrayList; 14 | import java.util.Comparator; 15 | import java.util.List; 16 | import java.util.Optional; 17 | 18 | import static io.github.skydynamic.quickbackupmulti.i18n.Translate.tr; 19 | import static io.github.skydynamic.quickbackupmulti.utils.MakeUtils.scheduleMake; 20 | import static io.github.skydynamic.quickbackupmulti.utils.schedule.CronUtil.getNextExecutionTime; 21 | 22 | public class ScheduleBackup implements Job { 23 | public static String generateName() { 24 | SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); 25 | return "ScheduleBackup-" + dateFormat.format(System.currentTimeMillis()); 26 | } 27 | 28 | private static void checkAndDeleteOldScheduleBackup() { 29 | List scheduleList = QbmManager.getScheduleBackupList(); 30 | if (scheduleList.size() >= QuickBackupMulti.config.getMaxScheduleBackup()) { 31 | StorageInfo oldest; 32 | Optional oldestOpt = scheduleList.stream().min( 33 | Comparator.comparingLong(StorageInfo::getTimestamp) 34 | ); 35 | if (oldestOpt.isPresent()) { 36 | oldest = oldestOpt.get(); 37 | QbmManager.delete(oldest.getName()); 38 | } 39 | } 40 | } 41 | 42 | @Override 43 | public void execute(JobExecutionContext context) { 44 | if (QuickBackupMulti.TEMP_CONFIG.server != null) { 45 | MinecraftServer server = QuickBackupMulti.TEMP_CONFIG.server; 46 | checkAndDeleteOldScheduleBackup(); 47 | if (scheduleMake(server.getCommandSource(), generateName())) { 48 | List finalPlayerList = new ArrayList<>(server.getPlayerManager().getPlayerList()); 49 | QuickBackupMulti.TEMP_CONFIG.setLatestScheduleExecuteTime(System.currentTimeMillis()); 50 | String nextExecuteTime = ""; 51 | switch (QuickBackupMulti.config.getScheduleMode()) { 52 | case "interval" : { 53 | nextExecuteTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 54 | .format(System.currentTimeMillis() + QuickBackupMulti.config.getScheduleInterval() * 1000L); 55 | break; 56 | } 57 | case "cron" : { 58 | nextExecuteTime = getNextExecutionTime(QuickBackupMulti.config.getScheduleCron(), true); 59 | break; 60 | } 61 | } 62 | for (ServerPlayerEntity player : finalPlayerList) { 63 | player.sendMessage(Messenger.literal(tr("quickbackupmulti.schedule.execute.finish", nextExecuteTime)), false); 64 | } 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /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.QuickBackupMulti; 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 (QuickBackupMulti.config.isScheduleBackup()) { 12 | try { 13 | CronUtil.buildScheduler(); 14 | QuickBackupMulti.TEMP_CONFIG.scheduler.start(); 15 | QuickBackupMulti.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 (QuickBackupMulti.TEMP_CONFIG.scheduler != null && QuickBackupMulti.TEMP_CONFIG.scheduler.isStarted()) QuickBackupMulti.TEMP_CONFIG.scheduler.shutdown(); 26 | } catch (SchedulerException ignored) { 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/assets/quickbackupmulti/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuickBackupMultiMod-Dev/QuickBackupM-Fabric/f2465577740b443ecf3a4a69623657c026f94231/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 | check_update: 6 | on_player_join: "§bQuickBackupMulti §rhas new release: §6%s" 7 | click_tooltip: "Click to open release page" 8 | 9 | config_page: 10 | save_button: "Save Config" 11 | close_button: "Close Screen" 12 | back: "Back" 13 | title: "QuickBackupMulti Config Screen" 14 | open_schedule_config_button: "Open Schedule Config Screen" 15 | lang: "Language" 16 | schedule: 17 | switch: "Schedule Backup: %s" 18 | cron: "Cron expression" 19 | interval: "Backup Interval" 20 | mode: 21 | switch: "Schedule backup mode: %s" 22 | 23 | schedule: 24 | get: "Next backup time is: §b%s" 25 | get_fail: "Schedule Backup do not enable" 26 | mode: 27 | get: "The current §aschedule mode§r is: §4§l%s§r" 28 | switch: 29 | set: "Schedule mode switch to: §a%s" 30 | fail: "Schedule mode switch fail: §c%s" 31 | enable: 32 | success: "Schedule backup started, the next execute time: §b%s" 33 | fail: "Schedule backup start fail: §c%s" 34 | disable: 35 | success: "Schedule backup shutdown!" 36 | fail: "Schedule backup shutdown fail: §c%s" 37 | execute: 38 | finish: "Finish to backup, the next execute time: §b%s" 39 | schedule_reset: "Execute Backup, Scheduler reset and the next execute time: §b%s" 40 | cron: 41 | set_custom_success: "Custom cron set success, the next execute time: §b%s" 42 | set_success: "Set success, the next execute time: §b%s" 43 | set_success_only: "Set success, but you do not enable schedule backup" 44 | set_fail: "Set fail: §c%s" 45 | expression_error: The Cron expression you entered is non-standard! 46 | 47 | database: 48 | set_success: "Set Success" 49 | set_fail: "Set fail, because: %s" 50 | set_success_but: "Set Success, but an error has occurred: %s" 51 | value_equal_config: "Use Internal DataBase is §b%s now!" 52 | 53 | search: 54 | success: "§aSearch success§r, the result: " 55 | fail: "§4Do not have any result" 56 | 57 | toast: 58 | start_title: Restoring now...Please wait a moment 59 | start_content: Don't join world in this time! 60 | end_title: Restore success! 61 | end_content: Now you can join your save! 62 | 63 | lang: 64 | failed: "§4Language do not exists" 65 | get: "The current §alanguage§r is: §4§l%s§r" 66 | set: "§aLanguage§r has been set to: §4§l%s§r" 67 | 68 | confirm_restore: 69 | nothing_to_confirm: Nothing to confirm 70 | 71 | init: 72 | start: Initializing QuickBackupMulti... 73 | finish: Initializing QuickBackupMulti completed! 74 | 75 | make: 76 | start: §aBackup§r now...Please wait a moment 77 | no_slot: No available slots found, §abackup§r abort! 78 | success: §aBackup §rcompleted, It takes §6%s§rs 79 | fail: §a备份§r失败,错误原因%s 80 | fail_exists: "§aMake backup §rFailed,error msg: Slot already exists" 81 | 82 | delete: 83 | success: §aSuccess delete slot §6%s 84 | fail: §4Fail§r to delete slot §6%s 85 | 86 | list_backup: 87 | title: §d[Page §b%s§d Slot Information]§r 88 | back_page: Last page 89 | next_page: Next page 90 | page_msg: "[§b%s§r / §b%s§r]" 91 | slot: 92 | header: "[§6#%s§r]" 93 | restore: Click to restore to slot §6%s§r 94 | delete: Click to delete slot §6%s§r 95 | show: Click to show §6%s§r detail 96 | total_space: "These backups total space consumed: §a%s§r" 97 | 98 | restore: 99 | countdown: 100 | intro: "%s execute restore backup, §cRestore§r after 10 second" 101 | text: "%s second later the world will be §crestored§r to slot §6%s§r, " 102 | hover: Click to ABORT restore! 103 | abort: §cRestore§r aborted! 104 | fail: "§4Fail§r ro restore: slot §4NotFount§r!" 105 | confirm_hint: Use §7/qb confirm§r to confirm §crestore§r 106 | abort_hint: Confirmed restore, If you want to abort, please enter §7/qb cancel§r 107 | 108 | show: 109 | header: "§d[Slot Information]§r" 110 | name: "§bName" 111 | desc: "§bBackup Desc" 112 | time: "§bBackup Time" 113 | back_button: "§2[Click to back]" 114 | delete_button: "§c[Click to delete]" 115 | fail: Backup Not Found 116 | 117 | permission: 118 | set: "Set §g%s§r permission to §3%s" 119 | get: "§g%s's§r current permission is §3%s" 120 | reload: "Reload permission config success" 121 | 122 | restartmode: 123 | get: "The current §arestart mode§r is: §4§l%s§r" 124 | switch: "§aRestart mode§r switch to: §4§l%s§r" -------------------------------------------------------------------------------- /src/main/resources/assets/quickbackupmulti/lang/zh_cn.yml: -------------------------------------------------------------------------------- 1 | quickbackupmulti: 2 | empty_comment: §7空§r 3 | list_empty: "§7当前没有任何备份" 4 | 5 | check_update: 6 | on_player_join: "检测到 §bQuickBackupMulti §r有新版本: §6%s" 7 | click_tooltip: "点击这里打开新版本发布页面" 8 | 9 | config_page: 10 | save_button: "保存" 11 | close_button: "关闭" 12 | back: "返回" 13 | title: "QuickBackupMulti 配置页面" 14 | open_schedule_config_button: "打开定时备份配置界面" 15 | lang: "语言" 16 | schedule: 17 | switch: "定时备份: %s" 18 | cron: "Cron表达式" 19 | interval: "备份间隔" 20 | mode: 21 | switch: "定时备份模式: %s" 22 | 23 | schedule: 24 | get: "下一次备份时间为: §b%s" 25 | get_fail: "定时备份未启用" 26 | mode: 27 | get: "当前设置的§a定时备份模式§r为: §4§l%s§r" 28 | switch: 29 | set: "定时备份模式切换到: §a%s" 30 | fail: "定时备份切换失败: §c%s" 31 | enable: 32 | success: "定时备份已开启, 下一次触发备份时间: §b%s" 33 | fail: "定时备份启用失败: §c%s" 34 | disable: 35 | success: "定时备份已关闭" 36 | fail: "定时备份关闭失败: §c%s" 37 | execute: 38 | finish: "定时备份完成, 下一次触发备份时间: §b%s" 39 | schedule_reset: "备份触发, 定时备份下一次触发时间: §b%s" 40 | cron: 41 | set_custom_success: "自定义定时备份设置成功, 下一次触发备份时间: §b%s" 42 | set_success: "设置成功, 下一次触发备份时间: §b%s" 43 | set_success_only: "设置成功, 但你尚未开启定时备份" 44 | set_fail: "设置失败: §c%s" 45 | expression_error: 你输入的Cron表达式有误 46 | 47 | database: 48 | set_success: "设置成功" 49 | set_fail: "设置失败, 原因: %s" 50 | set_success_but: "设置成功, 但发生了错误: %s" 51 | value_equal_config: "当前的值已经为§b%s" 52 | 53 | search: 54 | success: "§a搜索成功§r, 结果如下: " 55 | fail: "§4没有搜索到任何结果" 56 | 57 | toast: 58 | start_title: 正在进行回档...请稍等! 59 | start_content: 在此期间请勿进入存档! 60 | end_title: 回档完成! 61 | end_content: 你现在可以进入存档了! 62 | 63 | lang: 64 | failed: "§4语言不存在" 65 | get: "当前设置的§a语言§r为: §4§l%s§r" 66 | set: "已将§a语言§r设置为: §4§l%s§r" 67 | 68 | confirm_restore: 69 | nothing_to_confirm: 没有什么需要确认的 70 | 71 | init: 72 | start: 初始化QuickBackupMulti中... 73 | finish: 初始化QuickBackupMulti完成! 74 | 75 | make: 76 | start: §a备份§r中...请稍等 77 | no_slot: 未找到可用槽位,§a备份§r中断! 78 | success: §a备份§r完成,耗时§6%s§r秒 79 | fail: §a备份§r失败,错误原因%s 80 | fail_exists: "§a备份§r失败,错误原因: 槽位已存在" 81 | 82 | delete: 83 | success: 删除槽位§6%s§r§a完成§r 84 | fail: 删除槽位§6%s§r§4失败§r 85 | 86 | list_backup: 87 | title: "§d[第§b%s§d页槽位信息]§r" 88 | back_page: 上一页 89 | next_page: 下一页 90 | page_msg: "[第§b%s§r页 / 共§b%s§r页]" 91 | slot: 92 | header: "[§6#%s§r]" 93 | restore: 点击回档至槽位§6%s§r 94 | delete: 点击删除槽位§6%s§r 95 | show: 点击查看槽位§6%s§r详情 96 | total_space: "当前页面备份总占用空间: §a%s§r" 97 | 98 | restore: 99 | countdown: 100 | intro: "%s 执行回档, 10秒后关闭服务器§c回档§r" 101 | text: "还有%s秒,将§c回档§r为槽位§6%s§r, " 102 | hover: 点击终止回档! 103 | abort: 已取消§c回档§r任务! 104 | fail: "回档§4失败§r: 槽位§4不存在§r!" 105 | confirm_hint: 使用§7/qb confirm§r 确认§c回档§r 106 | abort_hint: 已确认回档, 如需取消请输入§7/qb cancel§r 107 | 108 | show: 109 | header: "§d[槽位信息]§r" 110 | name: "§b名称" 111 | desc: "§b描述" 112 | time: "§b备份时间" 113 | back_button: "§2[点击回档]" 114 | delete_button: "§c[点击删除]" 115 | fail: 槽位不存在 116 | 117 | permission: 118 | set: "设置§g%s§r的权限为§3%s" 119 | get: "§g%s§r当前的权限为§3%s" 120 | reload: "重载权限配置文件完成" 121 | 122 | restartmode: 123 | get: "当前重启方式为: §4§l%s§r" 124 | switch: "重启方式切换为: §a%s" -------------------------------------------------------------------------------- /src/main/resources/assets/quickbackupmulti/lang/zh_tw.yml: -------------------------------------------------------------------------------- 1 | quickbackupmulti: 2 | empty_comment: §7空§r 3 | list_empty: "§7目前沒有任何備份" 4 | 5 | check_update: 6 | on_player_join: "§bQuickBackupMulti §rhas new release: §6%s" 7 | click_tooltip: "Click to open release page" 8 | 9 | config_page: 10 | save_button: "儲存" 11 | close_button: "關閉" 12 | back: "返回" 13 | title: "QuickBackupMulti 設定頁面" 14 | open_schedule_config_button: "開啟定時備份設定介面" 15 | lang: "語言" 16 | schedule: 17 | switch: "定時備份:%s" 18 | cron: "Cron 表達式" 19 | interval: "備份間隔" 20 | mode: 21 | switch: "定時備份模式:%s" 22 | 23 | schedule: 24 | get: "下一次備份時間為:§b%s" 25 | get_fail: "定時備份未啟用" 26 | mode: 27 | get: "目前設定的§a定時備份模式§r為:§4§l%s§r" 28 | switch: 29 | set: "定時備份模式切換到:§a%s" 30 | fail: "定時備份切換失敗:§c%s" 31 | enable: 32 | success: "定時備份已開啟,下一次觸發備份時間:§b%s" 33 | fail: "定時備份啟用失敗:§c%s" 34 | disable: 35 | success: "定時備份已關閉" 36 | fail: "定時備份關閉失敗:§c%s" 37 | execute: 38 | finish: "定時備份完成,下一次觸發備份時間:§b%s" 39 | schedule_reset: "備份觸發,定時備份下一次觸發時間:§b%s" 40 | cron: 41 | set_custom_success: "自訂定時備份設定成功,下一次觸發備份時間:§b%s" 42 | set_success: "設定成功,下一次觸發備份時間:§b%s" 43 | set_success_only: "設定成功,但你尚未開啟定時備份" 44 | set_fail: "設定失敗:§c%s" 45 | expression_error: 您輸入的 Cron 表達式有誤 46 | 47 | database: 48 | set_success: "設定成功" 49 | set_fail: "設定失敗,原因:%s" 50 | set_success_but: "設定成功,但發生了錯誤:%s" 51 | value_equal_config: "目前的值已經為 §b%s" 52 | 53 | search: 54 | success: "§a搜尋成功§r,結果如下:" 55 | fail: "§4沒有搜尋到任何結果" 56 | 57 | toast: 58 | start_title: 正在進行回溯...請稍等! 59 | start_content: 在此期間請勿進入存檔! 60 | end_title: 回溯完成! 61 | end_content: 您現在可以進入存檔了! 62 | 63 | lang: 64 | failed: "§4語言不存在" 65 | get: "目前設定的§a語言§r為:§4§l%s§r" 66 | set: "已將§a語言§r設定為:§4§l%s§r" 67 | 68 | confirm_restore: 69 | nothing_to_confirm: 沒有什麼需要確認的 70 | 71 | init: 72 | start: 初始化 QuickBackupMulti 中... 73 | finish: 初始化 QuickBackupMulti 完成! 74 | 75 | make: 76 | start: §a備份§r中...請稍等 77 | no_slot: 未找到可用欄位,§a備份§r中斷! 78 | success: §a備份§r完成,耗時§6%s§r秒 79 | fail: §a備份§r失敗,錯誤原因 %s 80 | fail_exists: "§a備份§r失敗,錯誤原因:欄位已存在" 81 | 82 | delete: 83 | success: 刪除欄位§6%s§r§a完成§r 84 | fail: 刪除欄位§6%s§r§4失敗§r 85 | 86 | list_backup: 87 | title: "§d[第§b%s§d頁欄位資訊]§r" 88 | back_page: 上一頁 89 | next_page: 下一頁 90 | page_msg: "[第 §b%s§r 頁 / 共 §b%s§r 頁]" 91 | slot: 92 | header: "[§6#%s§r]" 93 | restore: 點選回溯至欄位§6%s§r 94 | delete: 點選刪除欄位§6%s§r 95 | show: 點選查看欄位§6%s§r詳情 96 | total_space: "目前頁面備份總占用空間:§a%s§r" 97 | 98 | restore: 99 | countdown: 100 | intro: "%s 執行回溯,10 秒後關閉伺服器§c回溯§r" 101 | text: "還有 %s 秒,將§c回溯§r為欄位§6%s§r," 102 | hover: 點選終止回溯! 103 | abort: 已取消§c回溯§r任務! 104 | fail: "回溯§4失敗§r:欄位§4不存在§r!" 105 | confirm_hint: 使用§7/qb confirm§r 確認§c回溯§r 106 | abort_hint: 已確認回溯,如需取消請輸入 §7/qb cancel§r 107 | 108 | show: 109 | header: "§d[欄位資訊]§r" 110 | name: "§b名稱" 111 | desc: "§b描述" 112 | time: "§b備份時間" 113 | back_button: "§2[點選回溯]" 114 | delete_button: "§c[點選刪除]" 115 | fail: 欄位不存在 116 | 117 | permission: 118 | set: "設定§g%s§r的權限為§3%s" 119 | get: "§g%s§r目前的權限為§3%s" 120 | reload: "重新載入權限設定檔完成" 121 | 122 | restartmode: 123 | get: "目前設定的§a自動重新啟動模式§r為:§4§l%s§r" 124 | switch: "自動重新啟動模式切換到:§a%s" 125 | -------------------------------------------------------------------------------- /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 | ], 7 | "injectors": { 8 | "defaultRequire": 1 9 | }, 10 | "server": [ 11 | "server.MinecraftServerMixin" 12 | ], 13 | "client": [ 14 | "client.ClientPlayNetworkHandlerMixin", 15 | "client.MinecraftClientMixin", 16 | "client.MinecraftServerMixin", 17 | "client.WorldEntryMixin" 18 | ] 19 | } -------------------------------------------------------------------------------- /versions/1.18.2/build.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 'CjsahMaven' 43 | url 'https://server.cjsah.net:1002/maven/' 44 | } 45 | } 46 | 47 | loom { 48 | accessWidenerPath = file("qbm.accesswidener") 49 | } 50 | 51 | remapJar { 52 | remapperIsolation = true 53 | } 54 | 55 | JavaVersion SOURCE_JAVA_VERSION 56 | JavaVersion TARGET_JAVA_VERSION 57 | if (mcVersion >= 12005) { 58 | SOURCE_JAVA_VERSION = JavaVersion.VERSION_21 59 | TARGET_JAVA_VERSION = JavaVersion.VERSION_21 60 | } else if (mcVersion >= 11800){ 61 | SOURCE_JAVA_VERSION = JavaVersion.VERSION_17 62 | TARGET_JAVA_VERSION = JavaVersion.VERSION_17 63 | } else if (mcVersion >= 11700) { 64 | SOURCE_JAVA_VERSION = JavaVersion.VERSION_16 65 | TARGET_JAVA_VERSION = JavaVersion.VERSION_16 66 | } else { 67 | SOURCE_JAVA_VERSION = JavaVersion.VERSION_1_8 68 | TARGET_JAVA_VERSION = JavaVersion.VERSION_17 69 | } 70 | JavaVersion MIXIN_COMPATIBILITY_LEVEL = TARGET_JAVA_VERSION 71 | 72 | dependencies { 73 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 74 | mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 75 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 76 | modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 77 | 78 | implementation('commons-io:commons-io') 79 | 80 | include(implementation('org.yaml:snakeyaml:2.0')) 81 | 82 | include(implementation('io.github.skydynamic:incremental-storage-lib:1.0.8')) 83 | 84 | include(implementation('org.quartz-scheduler:quartz:2.3.2')) 85 | 86 | include(implementation('dev.morphia.morphia:morphia-core:2.3.9')) 87 | include(implementation('de.bwaldvogel:mongo-java-server:1.45.0')) 88 | include(implementation('de.bwaldvogel:mongo-java-server-core:1.45.0')) 89 | include(implementation('de.bwaldvogel:mongo-java-server-h2-backend:1.45.0')) 90 | include(implementation('org.mongodb:mongodb-driver-sync:4.11.1')) 91 | include(implementation('org.mongodb:mongodb-driver-core:4.11.1')) 92 | include(implementation('org.mongodb:bson:4.11.1')) 93 | include(implementation('com.h2database:h2:2.2.224')) 94 | include(implementation('org.lz4:lz4-java:1.8.0')) 95 | 96 | } 97 | 98 | String versionSuffix = project.getVersionSuffix() 99 | 100 | String fullModVersion = project.mod_version + versionSuffix 101 | 102 | version = 'v' + fullModVersion 103 | group = project.maven_group 104 | archivesBaseName = project.archives_base_name + '-mc' + project.minecraft_version 105 | 106 | String fabric_id = "fabric-api" 107 | if (mcVersion < 11800) { 108 | fabric_id = "fabric" 109 | } 110 | 111 | processResources { 112 | from "qbm.accesswidener" 113 | 114 | inputs.property "version", project.version 115 | 116 | filesMatching("fabric.mod.json") { 117 | def valueMap = [ 118 | "version" : fullModVersion, 119 | "id" : project.mod_id, 120 | "name" : project.archives_base_name, 121 | "minecraft_support": project.minecraft_support_version, 122 | "fabric_loader" : project.loader_version, 123 | "fabric_api_id" : fabric_id 124 | ] 125 | expand valueMap 126 | } 127 | 128 | filesMatching("quickbackupmulti.mixins.json") { 129 | filter { 130 | text -> text.replace("/*JAVA_VERSION*/", "JAVA_${MIXIN_COMPATIBILITY_LEVEL.ordinal() + 1}") 131 | } 132 | } 133 | } 134 | 135 | java { 136 | withSourcesJar() 137 | sourceCompatibility = SOURCE_JAVA_VERSION 138 | targetCompatibility = TARGET_JAVA_VERSION 139 | } 140 | 141 | jar { 142 | from "LICENSE" 143 | } 144 | 145 | tasks.withType(JavaCompile) { 146 | options.encoding = "UTF-8" 147 | } -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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/gradle.properties: -------------------------------------------------------------------------------- 1 | minecraft_version=1.20 2 | minecraft_support_version=>=1.20 <=1.20.2 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.11 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/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 --------------------------------------------------------------------------------