├── .github └── workflows │ └── build.yml ├── .gitignore ├── Jenkinsfile ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── org │ └── yatopiamc │ └── c2me │ ├── C2MEMixinConnector.java │ ├── C2MEMod.java │ ├── common │ ├── chunkscheduling │ │ └── ServerMidTickTask.java │ ├── config │ │ ├── C2MEConfig.java │ │ └── ConfigUtils.java │ ├── package-info.java │ ├── structs │ │ └── LongHashSet.java │ ├── threading │ │ ├── GlobalExecutors.java │ │ ├── chunkio │ │ │ ├── AsyncSerializationManager.java │ │ │ ├── C2MECachedRegionStorage.java │ │ │ ├── ChunkIoMainThreadTaskUtils.java │ │ │ ├── ChunkIoThreadingExecutorUtils.java │ │ │ ├── IAsyncChunkStorage.java │ │ │ └── ISerializingRegionBasedStorage.java │ │ └── worldgen │ │ │ ├── C2MEChunkGenWorker.java │ │ │ ├── ChunkStatusThreadingType.java │ │ │ ├── ChunkStatusUtils.java │ │ │ ├── IChunkStatus.java │ │ │ ├── IWorldGenLockable.java │ │ │ └── WorldGenThreadingExecutorUtils.java │ └── util │ │ ├── AsyncCombinedLock.java │ │ ├── AsyncNamedLockDelegateAsyncLock.java │ │ ├── C2MEForkJoinWorkerThreadFactory.java │ │ ├── ConfigDirUtil.java │ │ ├── DeepCloneable.java │ │ ├── ShouldKeepTickingUtils.java │ │ └── SneakyThrow.java │ ├── metrics │ ├── Metrics.java │ └── MetricsConfig.java │ └── mixin │ ├── C2MEMixinPlugin.java │ ├── chunkscheduling │ ├── fix_unload │ │ └── MixinThreadedAnvilChunkStorage.java │ ├── mid_tick_chunk_tasks │ │ ├── MixinMinecraftServer.java │ │ ├── MixinServerChunkManager.java │ │ ├── MixinServerTickScheduler.java │ │ └── MixinWorld.java │ └── package-info.java │ ├── optimizations │ ├── package-info.java │ └── thread_local_biome_sampler │ │ └── MixinOverworldBiomeProvider.java │ ├── package-info.java │ ├── threading │ ├── chunkio │ │ ├── MixinChunkSerializer.java │ │ ├── MixinChunkTickScheduler.java │ │ ├── MixinScheduledTick.java │ │ ├── MixinSerializingRegionBasedStorage.java │ │ ├── MixinServerChunkManagerMainThreadExecutor.java │ │ ├── MixinSimpleTickScheduler.java │ │ ├── MixinStorageIoWorker.java │ │ ├── MixinThreadedAnvilChunkStorage.java │ │ └── MixinVersionedChunkStorage.java │ ├── lighting │ │ └── MixinServerLightingProvider.java │ └── worldgen │ │ ├── MixinChunkStatus.java │ │ ├── MixinServerWorld.java │ │ ├── MixinStructureManager.java │ │ ├── MixinStructurePalettedBlockInfoList.java │ │ ├── MixinThreadedAnvilChunkStorage.java │ │ └── MixinWeightedBlockStateProvider.java │ └── util │ ├── accessor │ ├── IServerChunkProvider.java │ └── IThreadTaskExecutor.java │ ├── chunkgenerator │ └── MixinCommandGenerate.java │ ├── log4j2shutdownhookisnomore │ ├── MixinMain.java │ └── MixinMinecraftDedicatedServer.java │ └── progresslogger │ └── MixinWorldGenerationProgressLogger.java └── resources ├── META-INF ├── accesstransformer.cfg └── mods.toml ├── c2me.mixins.json └── c2me.png /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: C2ME Build Script 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up JDK 11 15 | uses: actions/setup-java@v1 16 | with: 17 | java-version: 11 18 | 19 | - uses: actions/cache@v2 20 | with: 21 | path: | 22 | ~/.gradle/caches 23 | ~/.gradle/wrapper 24 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 25 | restore-keys: | 26 | ${{ runner.os }}-gradle- 27 | 28 | - name: Build C2ME 29 | run: | 30 | ./gradlew build 31 | 32 | - name: Upload Artifact 33 | uses: actions/upload-artifact@v2 34 | with: 35 | name: c2me-artifact 36 | path: '**/build/libs/*-all.jar' 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run*/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { label 'slave' } 3 | options { timestamps() } 4 | stages { 5 | stage('Build') { 6 | tools { 7 | jdk "OpenJDK 16" 8 | } 9 | steps { 10 | withMaven( 11 | maven: '3', 12 | mavenLocalRepo: '.repository', 13 | publisherStrategy: 'EXPLICIT' 14 | ) { 15 | scmSkip(deleteBuild: true, skipPattern:'.*\\[CI-SKIP\\].*') 16 | sh 'chmod +x ./gradlew' 17 | sh './gradlew clean build' 18 | } 19 | } 20 | post { 21 | success { 22 | archiveArtifacts "**/build/libs/*-all.jar" 23 | } 24 | failure { 25 | cleanWs() 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 YatopiaMC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | C2ME icon 2 |
3 |

C^2M-Engine (Forge port)

4 | 5 | [![Github-CI](https://github.com/YatopiaMC/C2ME-fabric/workflows/C2ME%20Build%20Script/badge.svg)](https://github.com/YatopiaMC/C2ME-fabric/actions?query=workflow%3ACI) 6 | [![CodeMC](https://ci.codemc.io/buildStatus/icon?job=YatopiaMC%2FC2ME-fabric%2Fver%252F1.16.5)](https://ci.codemc.io/job/YatopiaMC/job/C2ME-fabric/job/ver%252F1.16.5/) 7 | [![Discord](https://img.shields.io/discord/342814924310970398?color=%237289DA&label=Discord&logo=discord&logoColor=white)](https://discord.io/YatopiaMC) 8 |

A Forge mod designed to improve the chunk performance of Minecraft.

9 |
10 | 11 | ## So what is C2ME? 12 | C^2M-Engine, or C2ME for short, is a Forge mod designed to improve the performance of chunk generation, I/O, and loading. This is done by taking advantage of multiple CPU cores in parallel. For the best performance it is recommended to use C2ME with [Starlight](https://github.com/Spottedleaf/Starlight). 13 | 14 | ## What does C2ME stand for? 15 | Concurrent chunk management engine, it's about making the game better threaded and more scalable in regard to world gen and chunk io performance. 16 | 17 | ## So what is C2ME not? 18 | C2ME is not production ready and still pretty experimental. 19 | So backup your worlds and practice good game modding skills. 20 | 21 | ## Building and setting up 22 | 23 | Run the following commands in the root directory: 24 | 25 | ```shell 26 | ./gradlew clean build 27 | ``` 28 | 29 | ## License 30 | License information can be found [here](/LICENSE). 31 | 32 | ## Statistics 33 | [![](https://bstats.org/signatures/bukkit/C2ME-forge.svg)](https://bstats.org/plugin/bukkit/C2ME-forge/10823) 34 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | maven { url = 'https://files.minecraftforge.net/maven' } 4 | maven { url = 'https://repo.spongepowered.org/maven' } 5 | mavenCentral() 6 | } 7 | dependencies { 8 | classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '4.1.+', changing: true 9 | classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT' 10 | classpath 'com.github.jengelman.gradle.plugins:shadow:6.1.0' 11 | } 12 | } 13 | 14 | apply plugin: 'net.minecraftforge.gradle' 15 | // Only edit below this line, the above code adds and enables the necessary things for Forge to be setup. 16 | apply plugin: 'eclipse' 17 | apply plugin: 'maven-publish' 18 | apply plugin: 'com.github.johnrengelman.shadow' 19 | 20 | version = project.mod_version 21 | group = project.maven_group // http://maven.apache.org/guides/mini/guide-naming-conventions.html 22 | archivesBaseName = "${project.archives_base_name}-mc${project.minecraft_version}" 23 | 24 | sourceCompatibility = JavaVersion.VERSION_11 25 | targetCompatibility = JavaVersion.VERSION_11 26 | 27 | println('Java: ' + System.getProperty('java.version') + ' JVM: ' + System.getProperty('java.vm.version') + '(' + System.getProperty('java.vendor') + ') Arch: ' + System.getProperty('os.arch')) 28 | minecraft { 29 | // The mappings can be changed at any time, and must be in the following format. 30 | // Channel: Version: 31 | // snapshot YYYYMMDD Snapshot are built nightly. 32 | // stable # Stables are built at the discretion of the MCP team. 33 | // official MCVersion Official field/method names from Mojang mapping files 34 | // 35 | // You must be aware of the Mojang license when using the 'official' mappings. 36 | // See more information here: https://github.com/MinecraftForge/MCPConfig/blob/master/Mojang.md 37 | // 38 | // Use non-default mappings at your own risk. they may not always work. 39 | // Simply re-run your setup task after changing the mappings to update your workspace. 40 | mappings channel: 'official', version: project.minecraft_version 41 | // makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable. 42 | 43 | accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg') 44 | 45 | // Default run configurations. 46 | // These can be tweaked, removed, or duplicated as needed. 47 | runs { 48 | client { 49 | workingDirectory project.file('run') 50 | 51 | // add mixins 52 | arg "-mixin.config=c2me.mixins.json" 53 | 54 | // Recommended logging data for a userdev environment 55 | // The markers can be changed as needed. 56 | // "SCAN": For mods scan. 57 | // "REGISTRIES": For firing of registry events. 58 | // "REGISTRYDUMP": For getting the contents of all registries. 59 | property 'forge.logging.markers', 'REGISTRIES' 60 | 61 | // Recommended logging level for the console 62 | // You can set various levels here. 63 | // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels 64 | property 'forge.logging.console.level', 'debug' 65 | 66 | mods { 67 | c2me { 68 | source sourceSets.main 69 | } 70 | } 71 | } 72 | 73 | server { 74 | workingDirectory project.file('run') 75 | 76 | // add mixins 77 | arg "-mixin.config=c2me.mixins.json" 78 | 79 | // Recommended logging data for a userdev environment 80 | // The markers can be changed as needed. 81 | // "SCAN": For mods scan. 82 | // "REGISTRIES": For firing of registry events. 83 | // "REGISTRYDUMP": For getting the contents of all registries. 84 | property 'forge.logging.markers', 'REGISTRIES' 85 | 86 | // Recommended logging level for the console 87 | // You can set various levels here. 88 | // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels 89 | property 'forge.logging.console.level', 'debug' 90 | 91 | mods { 92 | c2me { 93 | source sourceSets.main 94 | } 95 | } 96 | } 97 | 98 | data { 99 | workingDirectory project.file('run') 100 | 101 | // add mixins 102 | arg "-mixin.config=c2me.mixins.json" 103 | 104 | // Recommended logging data for a userdev environment 105 | // The markers can be changed as needed. 106 | // "SCAN": For mods scan. 107 | // "REGISTRIES": For firing of registry events. 108 | // "REGISTRYDUMP": For getting the contents of all registries. 109 | property 'forge.logging.markers', 'REGISTRIES' 110 | 111 | // Recommended logging level for the console 112 | // You can set various levels here. 113 | // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels 114 | property 'forge.logging.console.level', 'debug' 115 | 116 | // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. 117 | args '--mod', 'c2me', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') 118 | 119 | mods { 120 | c2me { 121 | source sourceSets.main 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | // Include resources generated by data generators. 129 | sourceSets.main.resources { srcDir 'src/generated/resources' } 130 | 131 | dependencies { 132 | // Specify the version of Minecraft to use, If this is any group other then 'net.minecraft' it is assumed 133 | // that the dep is a ForgeGradle 'patcher' dependency. And it's patches will be applied. 134 | // The userdev artifact is a special name and will get all sorts of transformations applied to it. 135 | minecraft "net.minecraftforge:forge:${project.minecraft_version}-${project.forge_version}" 136 | 137 | implementation "com.ibm.async:asyncutil:${async_util_version}" 138 | implementation "com.electronwill.night-config:toml:${night_config_version}" 139 | implementation "org.threadly:threadly:${threadly_version}" 140 | 141 | // Real examples 142 | // compile 'com.mod-buildcraft:buildcraft:6.0.8:dev' // adds buildcraft to the dev env 143 | // compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env 144 | 145 | // The 'provided' configuration is for optional dependencies that exist at compile-time but might not at runtime. 146 | // provided 'com.mod-buildcraft:buildcraft:6.0.8:dev' 147 | 148 | // These dependencies get remapped to your current MCP mappings 149 | // deobf 'com.mod-buildcraft:buildcraft:6.0.8:dev' 150 | 151 | // For more info... 152 | // http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html 153 | // http://www.gradle.org/docs/current/userguide/dependency_management.html 154 | 155 | // Mixin stuff 156 | implementation 'org.spongepowered:mixin:0.8.2' 157 | annotationProcessor 'org.spongepowered:mixin:0.8.2' 158 | annotationProcessor 'com.google.code.gson:gson:2.8.0' 159 | annotationProcessor 'com.google.guava:guava:21.0' 160 | annotationProcessor 'org.apache.logging.log4j:log4j-core:2.11.2' 161 | annotationProcessor 'org.apache.logging.log4j:log4j-api:2.11.2' 162 | annotationProcessor 'org.ow2.asm:asm:9.0' 163 | annotationProcessor 'org.ow2.asm:asm-analysis:9.0' 164 | annotationProcessor 'org.ow2.asm:asm-commons:9.0' 165 | annotationProcessor 'org.ow2.asm:asm-tree:9.0' 166 | annotationProcessor 'org.ow2.asm:asm-util:9.0' 167 | } 168 | 169 | // Example for how to get properties into the manifest for reading by the runtime.. 170 | jar { 171 | manifest { 172 | attributes([ 173 | "Specification-Title": "c2me-forge", 174 | "Specification-Vendor": "c2me", 175 | "Specification-Version": "1", // We are version 1 of ourselves 176 | "Implementation-Title": project.name, 177 | "Implementation-Version": "${project.version}", 178 | "Implementation-Vendor" :"c2me", 179 | "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"), 180 | "MixinConfigs": "c2me.mixins.json", 181 | "MixinConnector": "org.yatopiamc.c2me.C2MEMixinConnector" 182 | ]) 183 | } 184 | finalizedBy(shadowJar) 185 | } 186 | 187 | shadowJar { 188 | dependencies { 189 | include(dependency("com.ibm.async:asyncutil:${async_util_version}")) 190 | include(dependency("com.electronwill.night-config:toml:${night_config_version}")) 191 | include(dependency("com.electronwill.night-config:core:${night_config_version}")) 192 | include(dependency("org.threadly:threadly:${threadly_version}")) 193 | } 194 | finalizedBy('reobfJar') 195 | } 196 | 197 | afterEvaluate { 198 | reobf.maybeCreate("shadowJar").mappings = tasks.getByName("createMcpToSrg").output 199 | } 200 | 201 | // More mixin stuff 202 | apply plugin: 'org.spongepowered.mixin' 203 | 204 | mixin { 205 | add sourceSets.main, "c2me.refmap.json" 206 | } 207 | 208 | publishing { 209 | publications { 210 | mavenJava(MavenPublication) { 211 | artifact jar 212 | } 213 | } 214 | repositories { 215 | maven { 216 | url "file:///${project.projectDir}/mcmodsrepo" 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle and improve build times. 2 | org.gradle.jvmargs=-Xmx4G 3 | org.gradle.parallel=true 4 | # Minecraft properties 5 | minecraft_version=1.16.5 6 | forge_version=36.1.2 7 | # Mod Properties 8 | mod_version=0.1-SNAPSHOT 9 | maven_group=org.yatopiamc.c2me 10 | archives_base_name=c2me-forge 11 | # Java Dependencies 12 | async_util_version=0.1.0 13 | night_config_version=3.6.3 14 | threadly_version=6.6 15 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RelativityMC/C2ME-forge/3e9c6b49497f6c6bd492c57f9bfde5e16b8b803f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | jcenter() 4 | maven { 5 | name = 'Fabric' 6 | url = 'https://maven.fabricmc.net/' 7 | } 8 | gradlePluginPortal() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/C2MEMixinConnector.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me; 2 | 3 | import org.spongepowered.asm.mixin.Mixins; 4 | import org.spongepowered.asm.mixin.connect.IMixinConnector; 5 | 6 | @SuppressWarnings("unused") 7 | public class C2MEMixinConnector implements IMixinConnector { 8 | @Override 9 | public void connect() { 10 | System.out.println("Initializing C2ME Mixins"); 11 | Mixins.addConfiguration("c2me.mixins.json"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/C2MEMod.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import net.minecraft.util.SharedConstants; 5 | import net.minecraftforge.fml.common.Mod; 6 | import net.minecraftforge.fml.loading.FMLLoader; 7 | import org.apache.logging.log4j.LogManager; 8 | import org.apache.logging.log4j.Logger; 9 | import org.yatopiamc.c2me.common.config.C2MEConfig; 10 | import org.yatopiamc.c2me.metrics.Metrics; 11 | 12 | import java.util.Locale; 13 | 14 | @Mod("c2me") 15 | public class C2MEMod { 16 | public static final Logger LOGGER = LogManager.getLogger("C2ME"); 17 | 18 | { 19 | onInitialize(); 20 | } 21 | 22 | public void onInitialize() { 23 | final Metrics metrics = new Metrics(10823); 24 | metrics.addCustomChart(new Metrics.SimplePie("environmentType", () -> FMLLoader.getDist().name().toLowerCase(Locale.ENGLISH))); 25 | metrics.addCustomChart(new Metrics.SimplePie("useThreadedWorldGeneration", () -> String.valueOf(C2MEConfig.threadedWorldGenConfig.enabled))); 26 | metrics.addCustomChart(new Metrics.SimplePie("useThreadedWorldFeatureGeneration", () -> String.valueOf(C2MEConfig.threadedWorldGenConfig.allowThreadedFeatures))); 27 | metrics.addCustomChart(new Metrics.DrilldownPie("detailedMinecraftVersion", () -> ImmutableMap.of(SharedConstants.getCurrentVersion().getReleaseTarget(), ImmutableMap.of(SharedConstants.getCurrentVersion().getName(), 1)))); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/chunkscheduling/ServerMidTickTask.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.chunkscheduling; 2 | 3 | public interface ServerMidTickTask { 4 | 5 | void executeTasksMidTick(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/config/C2MEConfig.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.config; 2 | 3 | import com.electronwill.nightconfig.core.CommentedConfig; 4 | import com.electronwill.nightconfig.core.file.CommentedFileConfig; 5 | import com.google.common.base.Preconditions; 6 | import net.minecraftforge.fml.loading.FMLLoader; 7 | import org.apache.logging.log4j.LogManager; 8 | import org.apache.logging.log4j.Logger; 9 | import org.yatopiamc.c2me.C2MEMod; 10 | import org.yatopiamc.c2me.common.util.ConfigDirUtil; 11 | 12 | import java.util.List; 13 | 14 | public class C2MEConfig { 15 | 16 | static final Logger LOGGER = LogManager.getLogger("C2ME Config"); 17 | 18 | public static final AsyncIoConfig asyncIoConfig; 19 | public static final ThreadedWorldGenConfig threadedWorldGenConfig; 20 | 21 | static { 22 | long startTime = System.nanoTime(); 23 | CommentedFileConfig config = CommentedFileConfig.builder(ConfigDirUtil.getConfigDir().resolve("c2me.toml")) 24 | .autosave() 25 | .preserveInsertionOrder() 26 | .sync() 27 | .build(); 28 | config.load(); 29 | 30 | final ConfigUtils.ConfigScope configScope = new ConfigUtils.ConfigScope(config); 31 | asyncIoConfig = new AsyncIoConfig(ConfigUtils.getValue(configScope, "asyncIO", CommentedConfig::inMemory, "Configuration for async io system", List.of(), null)); 32 | threadedWorldGenConfig = new ThreadedWorldGenConfig(ConfigUtils.getValue(configScope, "threadedWorldGen", CommentedConfig::inMemory, "Configuration for threaded world generation", List.of(), null)); 33 | configScope.removeUnusedKeys(); 34 | config.save(); 35 | config.close(); 36 | C2MEMod.LOGGER.info("Configuration loaded successfully after {}ms", (System.nanoTime() - startTime) / 1_000_000.0); 37 | } 38 | 39 | public static class AsyncIoConfig { 40 | public final boolean enabled; 41 | public final int serializerParallelism; 42 | public final int ioWorkerParallelism; 43 | 44 | public AsyncIoConfig(CommentedConfig config) { 45 | Preconditions.checkNotNull(config, "asyncIo config is not present"); 46 | final ConfigUtils.ConfigScope configScope = new ConfigUtils.ConfigScope(config); 47 | this.enabled = ConfigUtils.getValue(configScope, "enabled", () -> true, "Whether to enable this feature", List.of("radon", "immersive_portals"), false); 48 | this.serializerParallelism = ConfigUtils.getValue(configScope, "serializerParallelism", () -> Math.min(2, Runtime.getRuntime().availableProcessors()), "IO worker executor parallelism", List.of(), null, ConfigUtils.CheckType.THREAD_COUNT); 49 | this.ioWorkerParallelism = ConfigUtils.getValue(configScope, "ioWorkerParallelism", () -> Math.min(6, Runtime.getRuntime().availableProcessors()), "unused", List.of(), null, ConfigUtils.CheckType.THREAD_COUNT); 50 | configScope.removeUnusedKeys(); 51 | } 52 | } 53 | 54 | public static class ThreadedWorldGenConfig { 55 | public final boolean enabled; 56 | public final int parallelism; 57 | public final boolean allowThreadedFeatures; 58 | public final boolean reduceLockRadius; 59 | 60 | public ThreadedWorldGenConfig(CommentedConfig config) { 61 | Preconditions.checkNotNull(config, "threadedWorldGen config is not present"); 62 | final ConfigUtils.ConfigScope configScope = new ConfigUtils.ConfigScope(config); 63 | this.enabled = ConfigUtils.getValue(configScope, "enabled", () -> true, "Whether to enable this feature", List.of(), null); 64 | this.parallelism = ConfigUtils.getValue(configScope, "parallelism", () -> Math.min(6, Runtime.getRuntime().availableProcessors()), "World generation worker executor parallelism", List.of(), null, ConfigUtils.CheckType.THREAD_COUNT); 65 | this.allowThreadedFeatures = ConfigUtils.getValue(configScope, "allowThreadedFeatures", () -> false, "Whether to allow feature generation (world decorations like trees, ores and etc.) run in parallel \n (may cause incompatibility with other mods)", List.of(), null); 66 | this.reduceLockRadius = ConfigUtils.getValue(configScope, "reduceLockRadius", () -> false, "Whether to allow reducing lock radius (faster but UNSAFE) (YOU HAVE BEEN WARNED) \n (may cause incompatibility with other mods)", List.of(), null); 67 | configScope.removeUnusedKeys(); 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/config/ConfigUtils.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.config; 2 | 3 | import com.electronwill.nightconfig.core.CommentedConfig; 4 | import com.electronwill.nightconfig.core.Config; 5 | import com.google.common.base.Preconditions; 6 | import com.google.common.base.Supplier; 7 | import com.google.common.base.Suppliers; 8 | import net.minecraftforge.fml.loading.FMLLoader; 9 | import net.minecraftforge.fml.loading.moddiscovery.ModInfo; 10 | 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.HashSet; 14 | import java.util.List; 15 | import java.util.Objects; 16 | import java.util.Set; 17 | import java.util.stream.Collectors; 18 | 19 | public class ConfigUtils { 20 | 21 | private static final boolean IGNORE_INCOMPATIBILITY = Boolean.parseBoolean(System.getProperty("org.yatopia.c2me.common.config.ignoreIncompatibility", "false")); 22 | 23 | public static T getValue(ConfigScope config, String key, Supplier deff, String comment, List incompatibleMods, T incompatibleDefault, CheckType... checks) { 24 | if (IGNORE_INCOMPATIBILITY) C2MEConfig.LOGGER.fatal("Ignoring incompatibility check. You will get NO support if you do this unless explicitly stated. "); 25 | Preconditions.checkNotNull(config); 26 | Preconditions.checkNotNull(key); 27 | Preconditions.checkArgument(!key.isEmpty()); 28 | Preconditions.checkNotNull(deff); 29 | Preconditions.checkNotNull(incompatibleMods); 30 | final Set foundIncompatibleMods = IGNORE_INCOMPATIBILITY ? Collections.emptySet() : FMLLoader.getLoadingModList().getMods().stream().filter(modInfo -> incompatibleMods.contains(modInfo.getModId())).collect(Collectors.toSet()); 31 | Supplier def = Suppliers.memoize(deff); 32 | if (!foundIncompatibleMods.isEmpty()) { 33 | comment = comment + " \n INCOMPATIBILITY: Set to " + incompatibleDefault + " forcefully by: " + String.join(", ", foundIncompatibleMods.stream().map(ModInfo::getModId).collect(Collectors.toSet())); 34 | 35 | } 36 | config.processedKeys.add(key); 37 | if (!config.config.contains(key) || (checks.length != 0 && Arrays.stream(checks).anyMatch(checkType -> !checkType.check(config.config.get(key))))) 38 | config.config.set(key, def.get()); 39 | if (def.get() instanceof Config) config.config.setComment(key, String.format(" %s", comment)); 40 | else config.config.setComment(key, String.format(" (Default: %s) %s", def.get(), comment)); 41 | return foundIncompatibleMods.isEmpty() ? Objects.requireNonNull(config.config.get(key)) : incompatibleDefault; 42 | } 43 | 44 | static class ConfigScope { 45 | final CommentedConfig config; 46 | final Set processedKeys; 47 | 48 | ConfigScope(CommentedConfig config) { 49 | this.config = config; 50 | this.processedKeys = new HashSet<>(); 51 | } 52 | 53 | void removeUnusedKeys() { 54 | config.entrySet().removeIf(entry -> !processedKeys.contains(entry.getKey())); 55 | } 56 | } 57 | 58 | public enum CheckType { 59 | THREAD_COUNT() { 60 | @Override 61 | public boolean check(T value) { 62 | return value instanceof Number && ((Number) value).intValue() >= 1 && ((Number) value).intValue() <= 0x7fff; 63 | } 64 | }; 65 | 66 | public abstract boolean check(T value); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/package-info.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common; -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/structs/LongHashSet.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.structs; 2 | 3 | import it.unimi.dsi.fastutil.longs.LongCollection; 4 | import it.unimi.dsi.fastutil.longs.LongIterator; 5 | import it.unimi.dsi.fastutil.longs.LongSet; 6 | 7 | import javax.annotation.Nonnull; 8 | import java.util.Collection; 9 | import java.util.HashSet; 10 | import java.util.Iterator; 11 | 12 | public class LongHashSet implements LongSet { 13 | 14 | private final HashSet delegate = new HashSet<>(); 15 | 16 | @Override 17 | public int size() { 18 | return delegate.size(); 19 | } 20 | 21 | @Override 22 | public boolean isEmpty() { 23 | return delegate.isEmpty(); 24 | } 25 | 26 | @Override 27 | public @Nonnull 28 | LongIterator iterator() { 29 | final Iterator iterator = delegate.iterator(); 30 | return new LongIterator() { 31 | @Override 32 | public long nextLong() { 33 | return iterator.next(); 34 | } 35 | 36 | @Override 37 | public boolean hasNext() { 38 | return iterator.hasNext(); 39 | } 40 | 41 | @Override 42 | public void remove() { 43 | iterator.remove(); 44 | } 45 | }; 46 | } 47 | 48 | @Nonnull 49 | @Override 50 | public Object[] toArray() { 51 | return delegate.toArray(); 52 | } 53 | 54 | @Nonnull 55 | @SuppressWarnings("SuspiciousToArrayCall") 56 | @Override 57 | public T[] toArray(@Nonnull T[] a) { 58 | return delegate.toArray(a); 59 | } 60 | 61 | @Override 62 | public boolean containsAll(@Nonnull Collection c) { 63 | return delegate.containsAll(c); 64 | } 65 | 66 | @Override 67 | public boolean addAll(@Nonnull Collection c) { 68 | return delegate.addAll(c); 69 | } 70 | 71 | @Override 72 | public boolean removeAll(@Nonnull Collection c) { 73 | return delegate.removeAll(c); 74 | } 75 | 76 | @Override 77 | public boolean retainAll(@Nonnull Collection c) { 78 | return delegate.retainAll(c); 79 | } 80 | 81 | @Override 82 | public void clear() { 83 | delegate.clear(); 84 | } 85 | 86 | @Override 87 | public boolean add(long key) { 88 | return delegate.add(key); 89 | } 90 | 91 | @Override 92 | public boolean contains(long key) { 93 | return delegate.contains(key); 94 | } 95 | 96 | @Override 97 | public long[] toLongArray() { 98 | return delegate.stream().mapToLong(value -> value).toArray(); 99 | } 100 | 101 | @Override 102 | public long[] toLongArray(long[] a) { 103 | final long[] longs = toLongArray(); 104 | for (int i = 0; i < longs.length && i < a.length; i++) { 105 | a[i] = longs[i]; 106 | } 107 | return a; 108 | } 109 | 110 | @Override 111 | public long[] toArray(long[] a) { 112 | return toLongArray(a); 113 | } 114 | 115 | @Override 116 | public boolean addAll(LongCollection c) { 117 | return delegate.addAll(c); 118 | } 119 | 120 | @Override 121 | public boolean containsAll(LongCollection c) { 122 | return delegate.containsAll(c); 123 | } 124 | 125 | @Override 126 | public boolean removeAll(LongCollection c) { 127 | return delegate.removeAll(c); 128 | } 129 | 130 | @Override 131 | public boolean retainAll(LongCollection c) { 132 | return delegate.retainAll(c); 133 | } 134 | 135 | @Override 136 | public boolean remove(long k) { 137 | return delegate.remove(k); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/GlobalExecutors.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading; 2 | 3 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 4 | 5 | import java.util.concurrent.ScheduledThreadPoolExecutor; 6 | import java.util.concurrent.atomic.AtomicReference; 7 | 8 | public class GlobalExecutors { 9 | 10 | public static final ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor( 11 | 1, 12 | new ThreadFactoryBuilder().setNameFormat("C2ME scheduler").setDaemon(true).setPriority(Thread.NORM_PRIORITY - 1).setThreadFactory(r -> { 13 | final Thread thread = new Thread(r); 14 | GlobalExecutors.schedulerThread.set(thread); 15 | return thread; 16 | }).build() 17 | ); 18 | private static final AtomicReference schedulerThread = new AtomicReference<>(); 19 | 20 | public static void ensureSchedulerThread() { 21 | if (Thread.currentThread() != schedulerThread.get()) 22 | throw new IllegalStateException("Not on scheduler thread"); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/chunkio/AsyncSerializationManager.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.chunkio; 2 | 3 | import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; 4 | import net.minecraft.block.Block; 5 | import net.minecraft.fluid.Fluid; 6 | import net.minecraft.tileentity.TileEntity; 7 | import net.minecraft.util.math.BlockPos; 8 | import net.minecraft.util.math.ChunkPos; 9 | import net.minecraft.util.math.SectionPos; 10 | import net.minecraft.util.registry.Registry; 11 | import net.minecraft.world.ITickList; 12 | import net.minecraft.world.LightType; 13 | import net.minecraft.world.SerializableTickList; 14 | import net.minecraft.world.chunk.Chunk; 15 | import net.minecraft.world.chunk.IChunk; 16 | import net.minecraft.world.chunk.NibbleArray; 17 | import net.minecraft.world.lighting.IWorldLightListener; 18 | import net.minecraft.world.lighting.WorldLightManager; 19 | import net.minecraft.world.server.ServerTickList; 20 | import net.minecraft.world.server.ServerWorld; 21 | import org.apache.logging.log4j.LogManager; 22 | import org.apache.logging.log4j.Logger; 23 | import org.yatopiamc.c2me.common.util.DeepCloneable; 24 | 25 | import javax.annotation.Nonnull; 26 | import java.util.ArrayDeque; 27 | import java.util.Arrays; 28 | import java.util.Map; 29 | import java.util.Objects; 30 | import java.util.concurrent.atomic.AtomicBoolean; 31 | import java.util.function.Function; 32 | import java.util.stream.Collectors; 33 | 34 | public class AsyncSerializationManager { 35 | 36 | private static final Logger LOGGER = LogManager.getLogger("C2ME Async Serialization Manager"); 37 | 38 | private static final ThreadLocal> scopeHolder = ThreadLocal.withInitial(ArrayDeque::new); 39 | 40 | public static void push(Scope scope) { 41 | scopeHolder.get().push(scope); 42 | } 43 | 44 | public static Scope getScope(ChunkPos pos) { 45 | final Scope scope = scopeHolder.get().peek(); 46 | if (pos == null) return scope; 47 | if (scope != null) { 48 | if (scope.pos.equals(pos)) 49 | return scope; 50 | LOGGER.error("Scope position mismatch! Expected: {} but got {}. This will impact stability. Incompatible mods?", scope.pos, pos, new Throwable()); 51 | } 52 | return null; 53 | } 54 | 55 | public static void pop(Scope scope) { 56 | if (scope != scopeHolder.get().peek()) throw new IllegalArgumentException("Scope mismatch"); 57 | scopeHolder.get().pop(); 58 | } 59 | 60 | public static class Scope { 61 | public final ChunkPos pos; 62 | public final Map lighting; 63 | public final ITickList blockTickScheduler; 64 | public final ITickList fluidTickScheduler; 65 | public final Map blockEntities; 66 | private final AtomicBoolean isOpen = new AtomicBoolean(false); 67 | 68 | public Scope(IChunk chunk, ServerWorld world) { 69 | this.pos = chunk.getPos(); 70 | this.lighting = Arrays.stream(LightType.values()).map(type -> new CachedLightingView(world.getLightEngine(), chunk.getPos(), type)).collect(Collectors.toMap(CachedLightingView::getLightType, Function.identity())); 71 | final ITickList blockTickScheduler = chunk.getBlockTicks(); 72 | if (blockTickScheduler instanceof DeepCloneable) { 73 | this.blockTickScheduler = (ITickList) ((DeepCloneable) blockTickScheduler).deepClone(); 74 | } else { 75 | final ServerTickList worldBlockTickScheduler = world.getBlockTicks(); 76 | this.blockTickScheduler = new SerializableTickList<>(Registry.BLOCK::getKey, worldBlockTickScheduler.fetchTicksInChunk(chunk.getPos(), false, true), world.getGameTime()); 77 | } 78 | final ITickList fluidTickScheduler = chunk.getLiquidTicks(); 79 | if (fluidTickScheduler instanceof DeepCloneable) { 80 | this.fluidTickScheduler = (ITickList) ((DeepCloneable) fluidTickScheduler).deepClone(); 81 | } else { 82 | final ServerTickList worldFluidTickScheduler = world.getLiquidTicks(); 83 | this.fluidTickScheduler = new SerializableTickList<>(Registry.FLUID::getKey, worldFluidTickScheduler.fetchTicksInChunk(chunk.getPos(), false, true), world.getGameTime()); 84 | } 85 | this.blockEntities = chunk.getBlockEntitiesPos().stream().map(chunk::getBlockEntity).filter(Objects::nonNull).filter(blockEntity -> !blockEntity.isRemoved()).collect(Collectors.toMap(TileEntity::getBlockPos, Function.identity())); 86 | } 87 | 88 | public void open() { 89 | if (!isOpen.compareAndSet(false, true)) throw new IllegalStateException("Cannot use scope twice"); 90 | } 91 | 92 | private static final class CachedLightingView implements IWorldLightListener { 93 | 94 | private static final NibbleArray EMPTY = new NibbleArray(); 95 | 96 | private final LightType lightType; 97 | private final Map cachedData = new Object2ObjectOpenHashMap<>(); 98 | 99 | CachedLightingView(WorldLightManager provider, ChunkPos pos, LightType type) { 100 | this.lightType = type; 101 | for (int i = -1; i < 17; i++) { 102 | final SectionPos sectionPos = SectionPos.of(pos, i); 103 | NibbleArray lighting = provider.getLayerListener(type).getDataLayerData(sectionPos); 104 | cachedData.put(sectionPos, lighting != null ? lighting.copy() : null); 105 | } 106 | } 107 | 108 | public LightType getLightType() { 109 | return this.lightType; 110 | } 111 | 112 | @Override 113 | public void updateSectionStatus(@Nonnull SectionPos pos, boolean notReady) { 114 | throw new UnsupportedOperationException(); 115 | } 116 | 117 | @Nonnull 118 | @Override 119 | public NibbleArray getDataLayerData(SectionPos pos) { 120 | return cachedData.getOrDefault(pos, EMPTY); 121 | } 122 | 123 | @Override 124 | public int getLightValue(BlockPos pos) { 125 | throw new UnsupportedOperationException(); 126 | } 127 | } 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/chunkio/C2MECachedRegionStorage.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.chunkio; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.google.common.cache.Cache; 5 | import com.google.common.cache.CacheBuilder; 6 | import com.google.common.cache.RemovalNotification; 7 | import com.ibm.asyncutil.locks.AsyncLock; 8 | import com.ibm.asyncutil.locks.AsyncNamedLock; 9 | import net.minecraft.nbt.CompoundNBT; 10 | import net.minecraft.nbt.CompressedStreamTools; 11 | import net.minecraft.util.math.ChunkPos; 12 | import net.minecraft.world.chunk.storage.IOWorker; 13 | import net.minecraft.world.chunk.storage.RegionFile; 14 | import net.minecraft.world.chunk.storage.RegionFileCache; 15 | import org.apache.logging.log4j.LogManager; 16 | import org.apache.logging.log4j.Logger; 17 | import org.yatopiamc.c2me.common.config.C2MEConfig; 18 | import org.yatopiamc.c2me.common.threading.GlobalExecutors; 19 | import org.yatopiamc.c2me.common.util.C2MEForkJoinWorkerThreadFactory; 20 | import org.yatopiamc.c2me.common.util.SneakyThrow; 21 | 22 | import javax.annotation.Nullable; 23 | import java.io.DataInputStream; 24 | import java.io.DataOutputStream; 25 | import java.io.File; 26 | import java.io.IOException; 27 | import java.util.Objects; 28 | import java.util.concurrent.CompletableFuture; 29 | import java.util.concurrent.ConcurrentHashMap; 30 | import java.util.concurrent.ForkJoinPool; 31 | import java.util.concurrent.TimeUnit; 32 | import java.util.concurrent.atomic.AtomicBoolean; 33 | 34 | public class C2MECachedRegionStorage extends IOWorker { 35 | 36 | private static final CompoundNBT EMPTY_VALUE = new CompoundNBT(); 37 | private static final Logger LOGGER = LogManager.getLogger(); 38 | private static final ForkJoinPool IOExecutor = new ForkJoinPool( 39 | C2MEConfig.asyncIoConfig.ioWorkerParallelism, 40 | new C2MEForkJoinWorkerThreadFactory("C2ME chunkio io worker #%d", Thread.NORM_PRIORITY - 3), 41 | null, true 42 | ); 43 | 44 | private final RegionFileCache storage; 45 | private final Cache chunkCache; 46 | private final ConcurrentHashMap> writeFutures = new ConcurrentHashMap<>(); 47 | private final AsyncNamedLock chunkLocks = AsyncNamedLock.createFair(); 48 | private final AsyncNamedLock regionLocks = AsyncNamedLock.createFair(); 49 | private final AsyncLock storageLock = AsyncLock.createFair(); 50 | 51 | private final AtomicBoolean isClosed = new AtomicBoolean(false); 52 | 53 | public C2MECachedRegionStorage(File file, boolean bl, String string) { 54 | super(file, bl, string); 55 | this.storage = new RegionFileCache(file, bl); 56 | this.chunkCache = CacheBuilder.newBuilder() 57 | .concurrencyLevel(Runtime.getRuntime().availableProcessors() * 2) 58 | .expireAfterAccess(3, TimeUnit.SECONDS) 59 | .maximumSize(8192) 60 | .removalListener((RemovalNotification notification) -> { 61 | if (notification.getValue() != EMPTY_VALUE) 62 | scheduleWrite(notification.getKey(), notification.getValue()); 63 | }) 64 | .build(); 65 | this.tick(); 66 | } 67 | 68 | private void tick() { 69 | long startTime = System.currentTimeMillis(); 70 | chunkCache.cleanUp(); 71 | if (!isClosed.get()) 72 | GlobalExecutors.scheduler.schedule(this::tick, 1000 - (System.currentTimeMillis() - startTime), TimeUnit.MILLISECONDS); 73 | } 74 | 75 | private CompletableFuture getRegionFile(ChunkPos pos) { 76 | return storageLock.acquireLock().toCompletableFuture().thenApplyAsync(lockToken -> { 77 | try { 78 | return storage.getRegionFile(pos); 79 | } catch (IOException e) { 80 | SneakyThrow.sneaky(e); 81 | throw new RuntimeException(e); 82 | } finally { 83 | lockToken.releaseLock(); 84 | } 85 | }, GlobalExecutors.scheduler); 86 | } 87 | 88 | private void scheduleWrite(ChunkPos pos, CompoundNBT chunkData) { 89 | writeFutures.put(pos, regionLocks.acquireLock(new RegionPos(pos)).toCompletableFuture().thenCombineAsync(getRegionFile(pos), (lockToken, regionFile) -> { 90 | try (final DataOutputStream dataOutputStream = regionFile.getChunkDataOutputStream(pos)) { 91 | CompressedStreamTools.write(chunkData, dataOutputStream); 92 | } catch (Throwable t) { 93 | LOGGER.error("Failed to store chunk {}", pos, t); 94 | } finally { 95 | lockToken.releaseLock(); 96 | } 97 | return null; 98 | }, IOExecutor).exceptionally(t -> null).thenAccept(unused -> { 99 | })); 100 | } 101 | 102 | @Override 103 | public CompletableFuture store(ChunkPos pos, CompoundNBT nbt) { 104 | ensureOpen(); 105 | Preconditions.checkNotNull(pos); 106 | Preconditions.checkNotNull(nbt); 107 | return chunkLocks.acquireLock(pos).toCompletableFuture().thenAcceptAsync(lockToken -> { 108 | try { 109 | this.chunkCache.put(pos, nbt); 110 | } finally { 111 | lockToken.releaseLock(); 112 | } 113 | }, GlobalExecutors.scheduler); 114 | } 115 | 116 | public CompletableFuture getNbtAtAsync(ChunkPos pos) { 117 | ensureOpen(); 118 | // Check cache 119 | { 120 | final CompoundNBT cachedValue = this.chunkCache.getIfPresent(pos); 121 | if (cachedValue != null) { 122 | if (cachedValue == EMPTY_VALUE) 123 | return CompletableFuture.completedFuture(null); 124 | else 125 | return CompletableFuture.completedFuture(cachedValue); 126 | } 127 | } 128 | return chunkLocks.acquireLock(pos).toCompletableFuture().thenComposeAsync(lockToken -> { 129 | try { 130 | // Check again in single-threaded environment 131 | final CompoundNBT cachedValue = this.chunkCache.getIfPresent(pos); 132 | if (cachedValue != null) { 133 | if (cachedValue == EMPTY_VALUE) 134 | return CompletableFuture.completedFuture(null); 135 | else 136 | return CompletableFuture.completedFuture(cachedValue); 137 | } 138 | return regionLocks.acquireLock(new RegionPos(pos)).thenCombineAsync(getRegionFile(pos), (lockToken1, regionFile) -> { 139 | try { 140 | final CompoundNBT queriedTag; 141 | try (final DataInputStream dataInputStream = regionFile.getChunkDataInputStream(pos)) { 142 | if (dataInputStream != null) 143 | queriedTag = CompressedStreamTools.read(dataInputStream); 144 | else 145 | queriedTag = null; 146 | } 147 | chunkLocks.acquireLock(pos).thenAccept(lockToken2 -> { 148 | try { 149 | chunkCache.put(pos, queriedTag != null ? queriedTag : EMPTY_VALUE); 150 | } finally { 151 | lockToken2.releaseLock(); 152 | } 153 | }); 154 | return queriedTag; 155 | } catch (Throwable t) { 156 | LOGGER.warn("Failed to read chunk {}", pos, t); 157 | return null; 158 | } finally { 159 | lockToken1.releaseLock(); 160 | } 161 | }, IOExecutor); 162 | } catch (Throwable t) { 163 | LOGGER.warn("Failed to read chunk {}", pos, t); 164 | return CompletableFuture.completedFuture(null); 165 | } finally { 166 | lockToken.releaseLock(); 167 | } 168 | }, GlobalExecutors.scheduler); 169 | } 170 | 171 | @Nullable 172 | @Override 173 | public CompoundNBT load(ChunkPos pos) { 174 | return getNbtAtAsync(pos).join(); 175 | } 176 | 177 | @Override 178 | public CompletableFuture synchronize() { 179 | chunkCache.invalidateAll(); 180 | return CompletableFuture.allOf(writeFutures.values().toArray(CompletableFuture[]::new)).thenRunAsync(() -> { 181 | try { 182 | storage.flush(); 183 | } catch (IOException e) { 184 | LOGGER.warn("Failed to synchronized chunks", e); 185 | } 186 | }); 187 | } 188 | 189 | @Override 190 | public void close() throws IOException { 191 | if (this.isClosed.compareAndSet(false, true)) { 192 | synchronize().join(); 193 | this.storage.close(); 194 | } 195 | } 196 | 197 | private void ensureOpen() { 198 | Preconditions.checkState(!isClosed.get(), "Tried to modify a closed instance"); 199 | } 200 | 201 | private static class RegionPos { 202 | private final int x; 203 | private final int z; 204 | 205 | @SuppressWarnings("unused") 206 | private RegionPos(int x, int z) { 207 | this.x = x; 208 | this.z = z; 209 | } 210 | 211 | private RegionPos(ChunkPos chunkPos) { 212 | this.x = chunkPos.getRegionX(); 213 | this.z = chunkPos.getRegionZ(); 214 | } 215 | 216 | @SuppressWarnings("unused") 217 | public int getX() { 218 | return x; 219 | } 220 | 221 | @SuppressWarnings("unused") 222 | public int getZ() { 223 | return z; 224 | } 225 | 226 | @Override 227 | public boolean equals(Object o) { 228 | if (this == o) return true; 229 | if (o == null || getClass() != o.getClass()) return false; 230 | RegionPos regionPos = (RegionPos) o; 231 | return x == regionPos.x && z == regionPos.z; 232 | } 233 | 234 | @Override 235 | public int hashCode() { 236 | return Objects.hash(x, z); 237 | } 238 | 239 | @Override 240 | public String toString() { 241 | return "RegionPos{" + 242 | "x=" + x + 243 | ", z=" + z + 244 | '}'; 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/chunkio/ChunkIoMainThreadTaskUtils.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.chunkio; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | 6 | import java.util.concurrent.LinkedBlockingQueue; 7 | 8 | public class ChunkIoMainThreadTaskUtils { 9 | 10 | private static final Logger LOGGER = LogManager.getLogger(); 11 | private static final LinkedBlockingQueue mainThreadQueue = new LinkedBlockingQueue<>(); 12 | 13 | public static void executeMain(Runnable command) { 14 | mainThreadQueue.add(command); 15 | } 16 | 17 | public static void drainQueue() { 18 | Runnable command; 19 | while ((command = mainThreadQueue.poll()) != null) { 20 | try { 21 | command.run(); 22 | } catch (Throwable t) { 23 | LOGGER.error("Error while executing main thread task", t); 24 | } 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/chunkio/ChunkIoThreadingExecutorUtils.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.chunkio; 2 | 3 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 4 | import org.threadly.concurrent.UnfairExecutor; 5 | import org.yatopiamc.c2me.common.config.C2MEConfig; 6 | import org.yatopiamc.c2me.common.util.C2MEForkJoinWorkerThreadFactory; 7 | 8 | import java.util.concurrent.ForkJoinPool; 9 | import java.util.concurrent.ThreadFactory; 10 | 11 | public class ChunkIoThreadingExecutorUtils { 12 | 13 | public static final UnfairExecutor serializerExecutor = new UnfairExecutor( 14 | C2MEConfig.asyncIoConfig.serializerParallelism, 15 | new ThreadFactoryBuilder().setDaemon(true).setPriority(Thread.NORM_PRIORITY - 1).setNameFormat("C2ME serializer worker #%d").build() 16 | ); 17 | 18 | public static final ThreadFactory ioWorkerFactory = new ThreadFactoryBuilder().setDaemon(true).setNameFormat("IOWorker-%d").setPriority(Thread.NORM_PRIORITY - 1).build(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/chunkio/IAsyncChunkStorage.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.chunkio; 2 | 3 | import net.minecraft.nbt.CompoundNBT; 4 | import net.minecraft.util.math.ChunkPos; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | public interface IAsyncChunkStorage { 9 | 10 | CompletableFuture getNbtAtAsync(ChunkPos pos); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/chunkio/ISerializingRegionBasedStorage.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.chunkio; 2 | 3 | import net.minecraft.nbt.CompoundNBT; 4 | import net.minecraft.util.math.ChunkPos; 5 | 6 | public interface ISerializingRegionBasedStorage { 7 | 8 | void update(ChunkPos pos, CompoundNBT tag); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/worldgen/C2MEChunkGenWorker.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.worldgen; 2 | 3 | import net.minecraft.command.CommandSource; 4 | import net.minecraft.util.Unit; 5 | import net.minecraft.util.math.BlockPos; 6 | import net.minecraft.util.math.ChunkPos; 7 | import net.minecraft.util.text.StringTextComponent; 8 | import net.minecraft.util.text.TranslationTextComponent; 9 | import net.minecraft.world.chunk.ChunkStatus; 10 | import net.minecraft.world.server.ChunkHolder; 11 | import net.minecraft.world.server.ServerWorld; 12 | import net.minecraft.world.server.TicketType; 13 | import net.minecraftforge.server.command.ChunkGenWorker; 14 | import org.yatopiamc.c2me.mixin.util.accessor.IServerChunkProvider; 15 | 16 | import java.util.Arrays; 17 | import java.util.Queue; 18 | import java.util.concurrent.Semaphore; 19 | 20 | public class C2MEChunkGenWorker extends ChunkGenWorker { 21 | 22 | private static final TicketType C2ME_CHUNK_GEN = TicketType.create("c2me_chunk_gen", (o1, o2) -> 0); 23 | 24 | private static final int MAX_WORKING = 2048; 25 | 26 | private final CommandSource listener; 27 | private final ServerWorld world; 28 | private final Queue queue; 29 | private final Semaphore working = new Semaphore(MAX_WORKING); 30 | 31 | private int genned = 0; 32 | private int gennedLastSecond = 0; 33 | private long lastNotification = 0; 34 | private final int[] gennedSecond = new int[8]; 35 | private int gennedSecondLocation = 0; 36 | private long lastPollTask = System.nanoTime(); 37 | 38 | public C2MEChunkGenWorker(CommandSource listener, BlockPos start, int total, ServerWorld dim, int interval) { 39 | super(listener, start, total, dim, interval); 40 | this.listener = listener; 41 | this.queue = buildQueue(); 42 | this.world = dim; 43 | Arrays.fill(gennedSecond, -1); 44 | } 45 | 46 | @Override 47 | public boolean hasWork() { 48 | return !queue.isEmpty() || genned < total; 49 | } 50 | 51 | private double average(int[] arr) { 52 | int total = 0; 53 | int emptyCount = 0; 54 | for (int i : arr) { 55 | if (i == -1) { 56 | emptyCount ++; 57 | } else { 58 | total += i; 59 | } 60 | } 61 | return total / ((double) (arr.length - emptyCount)); 62 | } 63 | 64 | @Override 65 | public boolean doWork() { 66 | final long timeMillis = System.currentTimeMillis(); 67 | final long timeNanos = System.nanoTime(); 68 | if (timeNanos - lastPollTask > 10_000) { 69 | world.getChunkSource().pollTask(); 70 | lastPollTask = timeNanos; 71 | } 72 | if (timeMillis - lastNotification > 1000) { 73 | gennedSecond[(gennedSecondLocation ++ % gennedSecond.length)] = gennedLastSecond; 74 | listener.sendSuccess(new StringTextComponent(String.format("Running generation task for %s: %d / %d (%.1f%%) at %.1f (%d) cps", 75 | world, genned, total, genned / ((double) total) * 100.0, average(gennedSecond), gennedLastSecond)), true); 76 | lastNotification = timeMillis; 77 | gennedLastSecond = 0; 78 | } 79 | 80 | if (working.tryAcquire()) { 81 | final BlockPos pos = queue.poll(); 82 | if (pos == null) { 83 | working.release(); 84 | } else { 85 | final ChunkPos chunkPos = new ChunkPos(pos.getX(), pos.getZ()); 86 | world.getChunkSource().registerTickingTicket(C2ME_CHUNK_GEN, chunkPos, 0, Unit.INSTANCE); 87 | ((IServerChunkProvider) world.getChunkSource()).IRunDistanceManagerUpdates(); 88 | final ChunkHolder chunkHolder = ((IServerChunkProvider) world.getChunkSource()).IGetVisibleChunkIfPresent(chunkPos.toLong()); 89 | if (chunkHolder == null) { 90 | listener.sendSuccess(new StringTextComponent("Null chunkholder for " + chunkPos), true); 91 | world.getChunkSource().releaseTickingTicket(C2ME_CHUNK_GEN, chunkPos, 0, Unit.INSTANCE); 92 | working.release(); 93 | gennedLastSecond ++; 94 | genned ++; 95 | } else { 96 | world.getChunkSource().chunkMap.schedule(chunkHolder, ChunkStatus.FULL).thenRunAsync(() -> { 97 | world.getChunkSource().releaseTickingTicket(C2ME_CHUNK_GEN, chunkPos, 0, Unit.INSTANCE); 98 | working.release(); 99 | gennedLastSecond ++; 100 | genned ++; 101 | }, world.chunkSource.mainThreadProcessor); 102 | } 103 | } 104 | } 105 | if (!hasWork()) { 106 | listener.sendSuccess(new TranslationTextComponent("commands.forge.gen.complete", total, total, world.dimension().location()), true); 107 | return false; 108 | } 109 | return true; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/worldgen/ChunkStatusThreadingType.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.worldgen; 2 | 3 | import com.google.common.base.Preconditions; 4 | import com.ibm.asyncutil.locks.AsyncLock; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.function.Function; 8 | import java.util.function.Supplier; 9 | 10 | public enum ChunkStatusThreadingType { 11 | 12 | PARALLELIZED() { 13 | @Override 14 | public CompletableFuture runTask(AsyncLock lock, Supplier> completableFuture) { 15 | return CompletableFuture.supplyAsync(completableFuture, WorldGenThreadingExecutorUtils.mainExecutor).thenCompose(Function.identity()); 16 | } 17 | }, 18 | SINGLE_THREADED() { 19 | @Override 20 | public CompletableFuture runTask(AsyncLock lock, Supplier> completableFuture) { 21 | Preconditions.checkNotNull(lock); 22 | return lock.acquireLock().toCompletableFuture().thenComposeAsync(lockToken -> { 23 | try { 24 | return completableFuture.get(); 25 | } finally { 26 | lockToken.releaseLock(); 27 | } 28 | }, WorldGenThreadingExecutorUtils.mainExecutor); 29 | } 30 | }, 31 | AS_IS() { 32 | @Override 33 | public CompletableFuture runTask(AsyncLock lock, Supplier> completableFuture) { 34 | return completableFuture.get(); 35 | } 36 | }; 37 | 38 | public abstract CompletableFuture runTask(AsyncLock lock, Supplier> completableFuture); 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/worldgen/ChunkStatusUtils.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.worldgen; 2 | 3 | import com.ibm.asyncutil.locks.AsyncLock; 4 | import com.ibm.asyncutil.locks.AsyncNamedLock; 5 | import net.minecraft.util.math.ChunkPos; 6 | import net.minecraft.world.chunk.ChunkStatus; 7 | import org.yatopiamc.c2me.common.config.C2MEConfig; 8 | import org.yatopiamc.c2me.common.threading.GlobalExecutors; 9 | import org.yatopiamc.c2me.common.util.AsyncCombinedLock; 10 | import org.yatopiamc.c2me.common.util.AsyncNamedLockDelegateAsyncLock; 11 | 12 | import java.util.ArrayList; 13 | import java.util.HashSet; 14 | import java.util.List; 15 | import java.util.concurrent.CompletableFuture; 16 | import java.util.function.Function; 17 | import java.util.function.Supplier; 18 | 19 | import static org.yatopiamc.c2me.common.threading.worldgen.ChunkStatusThreadingType.AS_IS; 20 | import static org.yatopiamc.c2me.common.threading.worldgen.ChunkStatusThreadingType.PARALLELIZED; 21 | import static org.yatopiamc.c2me.common.threading.worldgen.ChunkStatusThreadingType.SINGLE_THREADED; 22 | 23 | public class ChunkStatusUtils { 24 | 25 | public static ChunkStatusThreadingType getThreadingType(final ChunkStatus status) { 26 | if (status.equals(ChunkStatus.STRUCTURE_STARTS) 27 | || status.equals(ChunkStatus.STRUCTURE_REFERENCES) 28 | || status.equals(ChunkStatus.BIOMES) 29 | || status.equals(ChunkStatus.NOISE) 30 | || status.equals(ChunkStatus.SURFACE) 31 | || status.equals(ChunkStatus.CARVERS) 32 | || status.equals(ChunkStatus.LIQUID_CARVERS) 33 | || status.equals(ChunkStatus.HEIGHTMAPS)) { 34 | return PARALLELIZED; 35 | } else if (status.equals(ChunkStatus.SPAWN)) { 36 | return SINGLE_THREADED; 37 | } else if (status.equals(ChunkStatus.FEATURES)) { 38 | return C2MEConfig.threadedWorldGenConfig.allowThreadedFeatures ? PARALLELIZED : SINGLE_THREADED; 39 | } 40 | return AS_IS; 41 | } 42 | 43 | public static CompletableFuture runChunkGenWithLock(ChunkPos target, int radius, AsyncNamedLock chunkLock, Supplier> action) { 44 | return CompletableFuture.supplyAsync(() -> { 45 | ArrayList fetchedLocks = new ArrayList<>((2 * radius + 1) * (2 * radius + 1)); 46 | for (int x = target.x - radius; x <= target.x + radius; x++) 47 | for (int z = target.z - radius; z <= target.z + radius; z++) 48 | fetchedLocks.add(new ChunkPos(x, z)); 49 | 50 | return new AsyncCombinedLock(chunkLock, new HashSet<>(fetchedLocks)).getFuture().thenComposeAsync(lockToken -> { 51 | final CompletableFuture future = action.get(); 52 | future.thenRun(lockToken::releaseLock); 53 | return future; 54 | }, GlobalExecutors.scheduler); 55 | }, AsyncCombinedLock.lockWorker).thenCompose(Function.identity()); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/worldgen/IChunkStatus.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.worldgen; 2 | 3 | public interface IChunkStatus { 4 | 5 | void calculateReducedTaskRadius(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/worldgen/IWorldGenLockable.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.worldgen; 2 | 3 | import com.ibm.asyncutil.locks.AsyncLock; 4 | import com.ibm.asyncutil.locks.AsyncNamedLock; 5 | import net.minecraft.util.math.ChunkPos; 6 | 7 | public interface IWorldGenLockable { 8 | 9 | AsyncLock getWorldGenSingleThreadedLock(); 10 | 11 | AsyncNamedLock getWorldGenChunkLock(); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/threading/worldgen/WorldGenThreadingExecutorUtils.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.threading.worldgen; 2 | 3 | import org.yatopiamc.c2me.common.config.C2MEConfig; 4 | import org.yatopiamc.c2me.common.util.C2MEForkJoinWorkerThreadFactory; 5 | 6 | import java.util.concurrent.ForkJoinPool; 7 | 8 | public class WorldGenThreadingExecutorUtils { 9 | 10 | public static final ForkJoinPool mainExecutor = new ForkJoinPool( 11 | C2MEConfig.threadedWorldGenConfig.parallelism, 12 | new C2MEForkJoinWorkerThreadFactory("C2ME worldgen worker #%d", Thread.NORM_PRIORITY - 1), 13 | null, 14 | true 15 | ); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/util/AsyncCombinedLock.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.util; 2 | 3 | import com.google.common.collect.Sets; 4 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 5 | import com.ibm.asyncutil.locks.AsyncLock; 6 | import com.ibm.asyncutil.locks.AsyncNamedLock; 7 | import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; 8 | import net.minecraft.util.math.ChunkPos; 9 | import org.threadly.concurrent.UnfairExecutor; 10 | import org.yatopiamc.c2me.common.config.C2MEConfig; 11 | 12 | import java.util.Optional; 13 | import java.util.Set; 14 | import java.util.concurrent.CompletableFuture; 15 | import java.util.concurrent.ForkJoinPool; 16 | import java.util.function.Function; 17 | import java.util.stream.Collectors; 18 | 19 | public class AsyncCombinedLock { 20 | 21 | public static final UnfairExecutor lockWorker = new UnfairExecutor( 22 | C2MEConfig.asyncIoConfig.serializerParallelism, 23 | new ThreadFactoryBuilder().setDaemon(true).setPriority(Thread.NORM_PRIORITY - 1).setNameFormat("C2ME lock worker #%d").build() 24 | ); 25 | 26 | private final AsyncNamedLock lock; 27 | private final ChunkPos[] names; 28 | private final CompletableFuture future = new CompletableFuture<>(); 29 | 30 | public AsyncCombinedLock(AsyncNamedLock lock, Set names) { 31 | this.lock = lock; 32 | this.names = names.toArray(ChunkPos[]::new); 33 | lockWorker.execute(this::tryAcquire); 34 | } 35 | 36 | private synchronized void tryAcquire() { // TODO optimize logic further 37 | final LockEntry[] tryLocks = new LockEntry[names.length]; 38 | boolean allAcquired = true; 39 | for (int i = 0, namesLength = names.length; i < namesLength; i++) { 40 | ChunkPos name = names[i]; 41 | final LockEntry entry = new LockEntry(name, this.lock.tryLock(name)); 42 | tryLocks[i] = entry; 43 | if (entry.lockToken.isEmpty()) { 44 | allAcquired = false; 45 | break; 46 | } 47 | } 48 | if (allAcquired) { 49 | future.complete(() -> { 50 | for (LockEntry entry : tryLocks) { 51 | //noinspection OptionalGetWithoutIsPresent 52 | entry.lockToken.get().releaseLock(); // if it isn't present then something is really wrong 53 | } 54 | }); 55 | } else { 56 | boolean triedRelock = false; 57 | for (LockEntry entry : tryLocks) { 58 | if (entry == null) continue; 59 | entry.lockToken.ifPresent(AsyncLock.LockToken::releaseLock); 60 | if (!triedRelock && entry.lockToken.isEmpty()) { 61 | this.lock.acquireLock(entry.name).thenCompose(lockToken -> { 62 | lockToken.releaseLock(); 63 | return CompletableFuture.runAsync(this::tryAcquire, lockWorker); 64 | }); 65 | triedRelock = true; 66 | } 67 | } 68 | if (!triedRelock) { 69 | // shouldn't happen at all... 70 | lockWorker.execute(this::tryAcquire); 71 | } 72 | } 73 | } 74 | 75 | public CompletableFuture getFuture() { 76 | return future.thenApply(Function.identity()); 77 | } 78 | 79 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") 80 | private static class LockEntry { 81 | public final ChunkPos name; 82 | public final Optional lockToken; 83 | 84 | private LockEntry(ChunkPos name, Optional lockToken) { 85 | this.name = name; 86 | this.lockToken = lockToken; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/util/AsyncNamedLockDelegateAsyncLock.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.util; 2 | 3 | import com.ibm.asyncutil.locks.AsyncLock; 4 | import com.ibm.asyncutil.locks.AsyncNamedLock; 5 | 6 | import java.util.Objects; 7 | import java.util.Optional; 8 | import java.util.concurrent.CompletionStage; 9 | 10 | public class AsyncNamedLockDelegateAsyncLock implements AsyncLock { 11 | 12 | private final AsyncNamedLock delegate; 13 | private final T name; 14 | 15 | public AsyncNamedLockDelegateAsyncLock(AsyncNamedLock delegate, T name) { 16 | this.delegate = Objects.requireNonNull(delegate); 17 | this.name = name; 18 | } 19 | 20 | @Override 21 | public CompletionStage acquireLock() { 22 | return delegate.acquireLock(name); 23 | } 24 | 25 | @Override 26 | public Optional tryLock() { 27 | return delegate.tryLock(name); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/util/C2MEForkJoinWorkerThreadFactory.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.util; 2 | 3 | import java.util.concurrent.ForkJoinPool; 4 | import java.util.concurrent.ForkJoinWorkerThread; 5 | import java.util.concurrent.atomic.AtomicLong; 6 | 7 | public class C2MEForkJoinWorkerThreadFactory implements ForkJoinPool.ForkJoinWorkerThreadFactory { 8 | private final AtomicLong serial = new AtomicLong(0); 9 | private final String namePattern; 10 | private final int priority; 11 | 12 | public C2MEForkJoinWorkerThreadFactory(String namePattern, int priority) { 13 | this.namePattern = namePattern; 14 | this.priority = priority; 15 | } 16 | 17 | @Override 18 | public ForkJoinWorkerThread newThread(ForkJoinPool pool) { 19 | final C2MEForkJoinWorkerThread C2MEForkJoinWorkerThread = new C2MEForkJoinWorkerThread(pool); 20 | C2MEForkJoinWorkerThread.setName(String.format(namePattern, serial.incrementAndGet())); 21 | C2MEForkJoinWorkerThread.setPriority(priority); 22 | C2MEForkJoinWorkerThread.setDaemon(true); 23 | return C2MEForkJoinWorkerThread; 24 | } 25 | 26 | private static class C2MEForkJoinWorkerThread extends ForkJoinWorkerThread { 27 | 28 | /** 29 | * Creates a ForkJoinWorkerThread operating in the given pool. 30 | * 31 | * @param pool the pool this thread works in 32 | * @throws NullPointerException if pool is null 33 | */ 34 | protected C2MEForkJoinWorkerThread(ForkJoinPool pool) { 35 | super(pool); 36 | } 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/util/ConfigDirUtil.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.util; 2 | 3 | import net.minecraftforge.fml.loading.FMLLoader; 4 | 5 | import java.nio.file.Path; 6 | 7 | public class ConfigDirUtil { 8 | 9 | public static Path getConfigDir() { 10 | final Path config = FMLLoader.getGamePath().resolve("config"); 11 | config.toFile().mkdirs(); 12 | return config; 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/util/DeepCloneable.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.util; 2 | 3 | public interface DeepCloneable { 4 | 5 | Object deepClone(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/util/ShouldKeepTickingUtils.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.util; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | import java.util.function.BooleanSupplier; 5 | 6 | public class ShouldKeepTickingUtils { 7 | 8 | public static BooleanSupplier minimumTicks(BooleanSupplier delegate, int minimum) { 9 | AtomicInteger ticks = new AtomicInteger(0); 10 | return () -> ticks.getAndIncrement() < minimum || delegate.getAsBoolean(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/common/util/SneakyThrow.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.common.util; 2 | 3 | public class SneakyThrow { 4 | 5 | public static void sneaky(Throwable throwable) { 6 | throw0(throwable); 7 | } 8 | 9 | @SuppressWarnings("unchecked") 10 | private static void throw0(Throwable throwable) throws T { 11 | throw (T) throwable; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/metrics/Metrics.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.metrics; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import net.minecraft.client.Minecraft; 6 | import net.minecraft.server.MinecraftServer; 7 | import net.minecraft.util.MinecraftVersion; 8 | import net.minecraftforge.api.distmarker.Dist; 9 | import net.minecraftforge.common.MinecraftForge; 10 | import net.minecraftforge.fml.event.server.FMLServerAboutToStartEvent; 11 | import net.minecraftforge.fml.event.server.FMLServerStartingEvent; 12 | import net.minecraftforge.fml.event.server.FMLServerStoppedEvent; 13 | import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; 14 | import net.minecraftforge.fml.loading.FMLLoader; 15 | import org.apache.logging.log4j.LogManager; 16 | import org.apache.logging.log4j.Logger; 17 | import org.yatopiamc.c2me.common.util.ConfigDirUtil; 18 | 19 | import javax.net.ssl.HttpsURLConnection; 20 | import java.io.BufferedReader; 21 | import java.io.ByteArrayOutputStream; 22 | import java.io.DataOutputStream; 23 | import java.io.File; 24 | import java.io.IOException; 25 | import java.io.InputStreamReader; 26 | import java.lang.reflect.Method; 27 | import java.net.URL; 28 | import java.nio.charset.StandardCharsets; 29 | import java.nio.file.Files; 30 | import java.nio.file.Path; 31 | import java.nio.file.StandardOpenOption; 32 | import java.util.Arrays; 33 | import java.util.Collection; 34 | import java.util.HashSet; 35 | import java.util.Map; 36 | import java.util.Objects; 37 | import java.util.Set; 38 | import java.util.UUID; 39 | import java.util.concurrent.Callable; 40 | import java.util.concurrent.Executors; 41 | import java.util.concurrent.ScheduledExecutorService; 42 | import java.util.concurrent.TimeUnit; 43 | import java.util.concurrent.atomic.AtomicReference; 44 | import java.util.function.BiConsumer; 45 | import java.util.function.Consumer; 46 | import java.util.function.Supplier; 47 | import java.util.logging.Level; 48 | import java.util.stream.Collectors; 49 | import java.util.zip.GZIPOutputStream; 50 | 51 | public class Metrics { 52 | 53 | private static final Logger LOGGER = LogManager.getLogger("Bstats-Metrics"); 54 | private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); 55 | public static final AtomicReference capturedServer = new AtomicReference<>(null); 56 | 57 | private final MetricsBase metricsBase; 58 | 59 | /** 60 | * Creates a new Metrics instance. 61 | * 62 | * @param serviceId The id of the service. It can be found at What is my plugin id? 64 | */ 65 | public Metrics(int serviceId) { 66 | // Get the config file 67 | final Path configFile = ConfigDirUtil.getConfigDir().resolve("c2me-bstats.json"); 68 | final MetricsConfig config = readConfig(configFile); 69 | // Load the data 70 | metricsBase = 71 | new MetricsBase( 72 | "bukkit", 73 | config.serverUuid, 74 | serviceId, 75 | config.enabled, 76 | this::appendPlatformData, 77 | this::appendServiceData, 78 | Runnable::run, 79 | () -> true, 80 | LOGGER::warn, 81 | LOGGER::info, 82 | config.logFailedRequests, 83 | config.logSentData, 84 | config.logResponseStatusText); 85 | // register the listener 86 | MinecraftForge.EVENT_BUS.addListener((FMLServerAboutToStartEvent event) -> capturedServer.set(event.getServer())); 87 | MinecraftForge.EVENT_BUS.addListener((FMLServerStoppedEvent event) -> capturedServer.compareAndSet(event.getServer(), null)); 88 | } 89 | 90 | private MetricsConfig readConfig(Path configFile) { 91 | if (Files.notExists(configFile)) { 92 | writeConfig(configFile, new MetricsConfig()); 93 | } 94 | final MetricsConfig config; 95 | try { 96 | config = gson.fromJson(Files.readString(configFile), MetricsConfig.class); 97 | } catch (IOException e) { 98 | LOGGER.warn("Failed to read bstats config, creating one...", e); 99 | writeConfig(configFile, new MetricsConfig()); 100 | return readConfig(configFile); 101 | } 102 | writeConfig(configFile, config); 103 | return config; 104 | } 105 | 106 | private void writeConfig(Path configFile, MetricsConfig config) { 107 | try { 108 | Files.writeString(configFile, gson.toJson(config), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC); 109 | } catch (IOException e) { 110 | LOGGER.error("Unable to create a new config", e); 111 | throw new RuntimeException(e); 112 | } 113 | } 114 | 115 | /** 116 | * Adds a custom chart. 117 | * 118 | * @param chart The chart to add. 119 | */ 120 | public void addCustomChart(CustomChart chart) { 121 | metricsBase.addCustomChart(chart); 122 | } 123 | 124 | private void appendPlatformData(JsonObjectBuilder builder) { 125 | builder.appendField("playerAmount", getPlayerAmount()); 126 | if (FMLLoader.getDist() == Dist.CLIENT) { 127 | builder.appendField("onlineMode", Minecraft.getInstance().getUser().getAccessToken() != null ? 1 : 0); 128 | } else if (FMLLoader.getDist() == Dist.DEDICATED_SERVER) { 129 | final MinecraftServer minecraftServer = capturedServer.get(); 130 | if (minecraftServer != null) { 131 | builder.appendField("onlineMode", minecraftServer.usesAuthentication() ? 1 : 0); 132 | } else { 133 | LOGGER.warn("No captured server found for dedicated server environment, assuming offline mode"); 134 | builder.appendField("onlineMode", 0); 135 | } 136 | } else { 137 | LOGGER.warn("Unknown environment, assuming offline mode"); 138 | builder.appendField("onlineMode", 0); 139 | } 140 | builder.appendField("bukkitVersion", FMLLoader.getLauncherInfo() + " (MC: " + MinecraftVersion.BUILT_IN.getReleaseTarget() + ")"); 141 | builder.appendField("bukkitName", "forge"); 142 | builder.appendField("javaVersion", System.getProperty("java.version")); 143 | builder.appendField("osName", System.getProperty("os.name")); 144 | builder.appendField("osArch", System.getProperty("os.arch")); 145 | builder.appendField("osVersion", System.getProperty("os.version")); 146 | builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); 147 | } 148 | 149 | private void appendServiceData(JsonObjectBuilder builder) { 150 | builder.appendField("pluginVersion", Metrics.class.getPackage().getImplementationVersion()); 151 | } 152 | 153 | private int getPlayerAmount() { 154 | final MinecraftServer minecraftServer = capturedServer.get(); 155 | if (minecraftServer != null && minecraftServer.isRunning()) { 156 | return minecraftServer.getPlayerCount(); 157 | } else { 158 | return 0; 159 | } 160 | } 161 | 162 | public static class MetricsBase { 163 | 164 | /** The version of the Metrics class. */ 165 | public static final String METRICS_VERSION = "2.2.1"; 166 | 167 | private static final ScheduledExecutorService scheduler = 168 | Executors.newScheduledThreadPool(1, task -> new Thread(task, "bStats-Metrics")); 169 | 170 | private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; 171 | 172 | private final String platform; 173 | 174 | private final String serverUuid; 175 | 176 | private final int serviceId; 177 | 178 | private final Consumer appendPlatformDataConsumer; 179 | 180 | private final Consumer appendServiceDataConsumer; 181 | 182 | private final Consumer submitTaskConsumer; 183 | 184 | private final Supplier checkServiceEnabledSupplier; 185 | 186 | private final BiConsumer errorLogger; 187 | 188 | private final Consumer infoLogger; 189 | 190 | private final boolean logErrors; 191 | 192 | private final boolean logSentData; 193 | 194 | private final boolean logResponseStatusText; 195 | 196 | private final Set customCharts = new HashSet<>(); 197 | 198 | private final boolean enabled; 199 | 200 | /** 201 | * Creates a new MetricsBase class instance. 202 | * 203 | * @param platform The platform of the service. 204 | * @param serviceId The id of the service. 205 | * @param serverUuid The server uuid. 206 | * @param enabled Whether or not data sending is enabled. 207 | * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 208 | * appends all platform-specific data. 209 | * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 210 | * appends all service-specific data. 211 | * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be 212 | * used to delegate the data collection to a another thread to prevent errors caused by 213 | * concurrency. Can be {@code null}. 214 | * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. 215 | * @param errorLogger A consumer that accepts log message and an error. 216 | * @param infoLogger A consumer that accepts info log messages. 217 | * @param logErrors Whether or not errors should be logged. 218 | * @param logSentData Whether or not the sent data should be logged. 219 | * @param logResponseStatusText Whether or not the response status text should be logged. 220 | */ 221 | public MetricsBase( 222 | String platform, 223 | String serverUuid, 224 | int serviceId, 225 | boolean enabled, 226 | Consumer appendPlatformDataConsumer, 227 | Consumer appendServiceDataConsumer, 228 | Consumer submitTaskConsumer, 229 | Supplier checkServiceEnabledSupplier, 230 | BiConsumer errorLogger, 231 | Consumer infoLogger, 232 | boolean logErrors, 233 | boolean logSentData, 234 | boolean logResponseStatusText) { 235 | this.platform = platform; 236 | this.serverUuid = serverUuid; 237 | this.serviceId = serviceId; 238 | this.enabled = enabled; 239 | this.appendPlatformDataConsumer = appendPlatformDataConsumer; 240 | this.appendServiceDataConsumer = appendServiceDataConsumer; 241 | this.submitTaskConsumer = submitTaskConsumer; 242 | this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; 243 | this.errorLogger = errorLogger; 244 | this.infoLogger = infoLogger; 245 | this.logErrors = logErrors; 246 | this.logSentData = logSentData; 247 | this.logResponseStatusText = logResponseStatusText; 248 | checkRelocation(); 249 | if (enabled) { 250 | startSubmitting(); 251 | } 252 | } 253 | 254 | public void addCustomChart(CustomChart chart) { 255 | this.customCharts.add(chart); 256 | } 257 | 258 | private void startSubmitting() { 259 | final Runnable submitTask = 260 | () -> { 261 | if (!enabled || !checkServiceEnabledSupplier.get()) { 262 | // Submitting data or service is disabled 263 | scheduler.shutdown(); 264 | return; 265 | } 266 | if (submitTaskConsumer != null) { 267 | submitTaskConsumer.accept(this::submitData); 268 | } else { 269 | this.submitData(); 270 | } 271 | }; 272 | // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution 273 | // of requests on the 274 | // bStats backend. To circumvent this problem, we introduce some randomness into the initial 275 | // and second delay. 276 | // WARNING: You must not modify and part of this Metrics class, including the submit delay or 277 | // frequency! 278 | // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it! 279 | long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); 280 | long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); 281 | scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); 282 | scheduler.scheduleAtFixedRate( 283 | submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); 284 | } 285 | 286 | private void submitData() { 287 | final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); 288 | appendPlatformDataConsumer.accept(baseJsonBuilder); 289 | final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); 290 | appendServiceDataConsumer.accept(serviceJsonBuilder); 291 | JsonObjectBuilder.JsonObject[] chartData = 292 | customCharts.stream() 293 | .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) 294 | .filter(Objects::nonNull) 295 | .toArray(JsonObjectBuilder.JsonObject[]::new); 296 | serviceJsonBuilder.appendField("id", serviceId); 297 | serviceJsonBuilder.appendField("customCharts", chartData); 298 | baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); 299 | baseJsonBuilder.appendField("serverUUID", serverUuid); 300 | baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); 301 | JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); 302 | scheduler.execute( 303 | () -> { 304 | try { 305 | // Send the data 306 | sendData(data); 307 | } catch (Exception e) { 308 | // Something went wrong! :( 309 | if (logErrors) { 310 | errorLogger.accept("Could not submit bStats metrics data", e); 311 | } 312 | } 313 | }); 314 | } 315 | 316 | private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { 317 | if (logSentData) { 318 | infoLogger.accept("Sent bStats metrics data: " + data.toString()); 319 | } 320 | String url = String.format(REPORT_URL, platform); 321 | HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); 322 | // Compress the data to save bandwidth 323 | byte[] compressedData = compress(data.toString()); 324 | connection.setRequestMethod("POST"); 325 | connection.addRequestProperty("Accept", "application/json"); 326 | connection.addRequestProperty("Connection", "close"); 327 | connection.addRequestProperty("Content-Encoding", "gzip"); 328 | connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); 329 | connection.setRequestProperty("Content-Type", "application/json"); 330 | connection.setRequestProperty("User-Agent", "Metrics-Service/1"); 331 | connection.setDoOutput(true); 332 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { 333 | outputStream.write(compressedData); 334 | } 335 | StringBuilder builder = new StringBuilder(); 336 | try (BufferedReader bufferedReader = 337 | new BufferedReader(new InputStreamReader(connection.getInputStream()))) { 338 | String line; 339 | while ((line = bufferedReader.readLine()) != null) { 340 | builder.append(line); 341 | } 342 | } 343 | if (logResponseStatusText) { 344 | infoLogger.accept("Sent data to bStats and received response: " + builder); 345 | } 346 | } 347 | 348 | /** Checks that the class was properly relocated. */ 349 | private void checkRelocation() { 350 | // You can use the property to disable the check in your test environment 351 | if (System.getProperty("bstats.relocatecheck") == null 352 | || !System.getProperty("bstats.relocatecheck").equals("false")) { 353 | // Maven's Relocate is clever and changes strings, too. So we have to use this little 354 | // "trick" ... :D 355 | final String defaultPackage = 356 | new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); 357 | final String examplePackage = 358 | new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); 359 | // We want to make sure no one just copy & pastes the example and uses the wrong package 360 | // names 361 | if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) 362 | || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { 363 | throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); 364 | } 365 | } 366 | } 367 | 368 | /** 369 | * Gzips the given string. 370 | * 371 | * @param str The string to gzip. 372 | * @return The gzipped string. 373 | */ 374 | private static byte[] compress(final String str) throws IOException { 375 | if (str == null) { 376 | return null; 377 | } 378 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 379 | try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { 380 | gzip.write(str.getBytes(StandardCharsets.UTF_8)); 381 | } 382 | return outputStream.toByteArray(); 383 | } 384 | } 385 | 386 | public static class AdvancedBarChart extends CustomChart { 387 | 388 | private final Callable> callable; 389 | 390 | /** 391 | * Class constructor. 392 | * 393 | * @param chartId The id of the chart. 394 | * @param callable The callable which is used to request the chart data. 395 | */ 396 | public AdvancedBarChart(String chartId, Callable> callable) { 397 | super(chartId); 398 | this.callable = callable; 399 | } 400 | 401 | @Override 402 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 403 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 404 | Map map = callable.call(); 405 | if (map == null || map.isEmpty()) { 406 | // Null = skip the chart 407 | return null; 408 | } 409 | boolean allSkipped = true; 410 | for (Map.Entry entry : map.entrySet()) { 411 | if (entry.getValue().length == 0) { 412 | // Skip this invalid 413 | continue; 414 | } 415 | allSkipped = false; 416 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 417 | } 418 | if (allSkipped) { 419 | // Null = skip the chart 420 | return null; 421 | } 422 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 423 | } 424 | } 425 | 426 | public static class SimpleBarChart extends CustomChart { 427 | 428 | private final Callable> callable; 429 | 430 | /** 431 | * Class constructor. 432 | * 433 | * @param chartId The id of the chart. 434 | * @param callable The callable which is used to request the chart data. 435 | */ 436 | public SimpleBarChart(String chartId, Callable> callable) { 437 | super(chartId); 438 | this.callable = callable; 439 | } 440 | 441 | @Override 442 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 443 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 444 | Map map = callable.call(); 445 | if (map == null || map.isEmpty()) { 446 | // Null = skip the chart 447 | return null; 448 | } 449 | for (Map.Entry entry : map.entrySet()) { 450 | valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); 451 | } 452 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 453 | } 454 | } 455 | 456 | public static class MultiLineChart extends CustomChart { 457 | 458 | private final Callable> callable; 459 | 460 | /** 461 | * Class constructor. 462 | * 463 | * @param chartId The id of the chart. 464 | * @param callable The callable which is used to request the chart data. 465 | */ 466 | public MultiLineChart(String chartId, Callable> callable) { 467 | super(chartId); 468 | this.callable = callable; 469 | } 470 | 471 | @Override 472 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 473 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 474 | Map map = callable.call(); 475 | if (map == null || map.isEmpty()) { 476 | // Null = skip the chart 477 | return null; 478 | } 479 | boolean allSkipped = true; 480 | for (Map.Entry entry : map.entrySet()) { 481 | if (entry.getValue() == 0) { 482 | // Skip this invalid 483 | continue; 484 | } 485 | allSkipped = false; 486 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 487 | } 488 | if (allSkipped) { 489 | // Null = skip the chart 490 | return null; 491 | } 492 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 493 | } 494 | } 495 | 496 | public static class AdvancedPie extends CustomChart { 497 | 498 | private final Callable> callable; 499 | 500 | /** 501 | * Class constructor. 502 | * 503 | * @param chartId The id of the chart. 504 | * @param callable The callable which is used to request the chart data. 505 | */ 506 | public AdvancedPie(String chartId, Callable> callable) { 507 | super(chartId); 508 | this.callable = callable; 509 | } 510 | 511 | @Override 512 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 513 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 514 | Map map = callable.call(); 515 | if (map == null || map.isEmpty()) { 516 | // Null = skip the chart 517 | return null; 518 | } 519 | boolean allSkipped = true; 520 | for (Map.Entry entry : map.entrySet()) { 521 | if (entry.getValue() == 0) { 522 | // Skip this invalid 523 | continue; 524 | } 525 | allSkipped = false; 526 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 527 | } 528 | if (allSkipped) { 529 | // Null = skip the chart 530 | return null; 531 | } 532 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 533 | } 534 | } 535 | 536 | public abstract static class CustomChart { 537 | 538 | private final String chartId; 539 | 540 | protected CustomChart(String chartId) { 541 | if (chartId == null) { 542 | throw new IllegalArgumentException("chartId must not be null"); 543 | } 544 | this.chartId = chartId; 545 | } 546 | 547 | public JsonObjectBuilder.JsonObject getRequestJsonObject( 548 | BiConsumer errorLogger, boolean logErrors) { 549 | JsonObjectBuilder builder = new JsonObjectBuilder(); 550 | builder.appendField("chartId", chartId); 551 | try { 552 | JsonObjectBuilder.JsonObject data = getChartData(); 553 | if (data == null) { 554 | // If the data is null we don't send the chart. 555 | return null; 556 | } 557 | builder.appendField("data", data); 558 | } catch (Throwable t) { 559 | if (logErrors) { 560 | errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); 561 | } 562 | return null; 563 | } 564 | return builder.build(); 565 | } 566 | 567 | protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; 568 | } 569 | 570 | public static class SingleLineChart extends CustomChart { 571 | 572 | private final Callable callable; 573 | 574 | /** 575 | * Class constructor. 576 | * 577 | * @param chartId The id of the chart. 578 | * @param callable The callable which is used to request the chart data. 579 | */ 580 | public SingleLineChart(String chartId, Callable callable) { 581 | super(chartId); 582 | this.callable = callable; 583 | } 584 | 585 | @Override 586 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 587 | int value = callable.call(); 588 | if (value == 0) { 589 | // Null = skip the chart 590 | return null; 591 | } 592 | return new JsonObjectBuilder().appendField("value", value).build(); 593 | } 594 | } 595 | 596 | public static class SimplePie extends CustomChart { 597 | 598 | private final Callable callable; 599 | 600 | /** 601 | * Class constructor. 602 | * 603 | * @param chartId The id of the chart. 604 | * @param callable The callable which is used to request the chart data. 605 | */ 606 | public SimplePie(String chartId, Callable callable) { 607 | super(chartId); 608 | this.callable = callable; 609 | } 610 | 611 | @Override 612 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 613 | String value = callable.call(); 614 | if (value == null || value.isEmpty()) { 615 | // Null = skip the chart 616 | return null; 617 | } 618 | return new JsonObjectBuilder().appendField("value", value).build(); 619 | } 620 | } 621 | 622 | public static class DrilldownPie extends CustomChart { 623 | 624 | private final Callable>> callable; 625 | 626 | /** 627 | * Class constructor. 628 | * 629 | * @param chartId The id of the chart. 630 | * @param callable The callable which is used to request the chart data. 631 | */ 632 | public DrilldownPie(String chartId, Callable>> callable) { 633 | super(chartId); 634 | this.callable = callable; 635 | } 636 | 637 | @Override 638 | public JsonObjectBuilder.JsonObject getChartData() throws Exception { 639 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 640 | Map> map = callable.call(); 641 | if (map == null || map.isEmpty()) { 642 | // Null = skip the chart 643 | return null; 644 | } 645 | boolean reallyAllSkipped = true; 646 | for (Map.Entry> entryValues : map.entrySet()) { 647 | JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); 648 | boolean allSkipped = true; 649 | for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { 650 | valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); 651 | allSkipped = false; 652 | } 653 | if (!allSkipped) { 654 | reallyAllSkipped = false; 655 | valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); 656 | } 657 | } 658 | if (reallyAllSkipped) { 659 | // Null = skip the chart 660 | return null; 661 | } 662 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 663 | } 664 | } 665 | 666 | /** 667 | * An extremely simple JSON builder. 668 | * 669 | *

While this class is neither feature-rich nor the most performant one, it's sufficient enough 670 | * for its use-case. 671 | */ 672 | public static class JsonObjectBuilder { 673 | 674 | private StringBuilder builder = new StringBuilder(); 675 | 676 | private boolean hasAtLeastOneField = false; 677 | 678 | public JsonObjectBuilder() { 679 | builder.append("{"); 680 | } 681 | 682 | /** 683 | * Appends a null field to the JSON. 684 | * 685 | * @param key The key of the field. 686 | * @return A reference to this object. 687 | */ 688 | public JsonObjectBuilder appendNull(String key) { 689 | appendFieldUnescaped(key, "null"); 690 | return this; 691 | } 692 | 693 | /** 694 | * Appends a string field to the JSON. 695 | * 696 | * @param key The key of the field. 697 | * @param value The value of the field. 698 | * @return A reference to this object. 699 | */ 700 | public JsonObjectBuilder appendField(String key, String value) { 701 | if (value == null) { 702 | throw new IllegalArgumentException("JSON value must not be null"); 703 | } 704 | appendFieldUnescaped(key, "\"" + escape(value) + "\""); 705 | return this; 706 | } 707 | 708 | /** 709 | * Appends an integer field to the JSON. 710 | * 711 | * @param key The key of the field. 712 | * @param value The value of the field. 713 | * @return A reference to this object. 714 | */ 715 | public JsonObjectBuilder appendField(String key, int value) { 716 | appendFieldUnescaped(key, String.valueOf(value)); 717 | return this; 718 | } 719 | 720 | /** 721 | * Appends an object to the JSON. 722 | * 723 | * @param key The key of the field. 724 | * @param object The object. 725 | * @return A reference to this object. 726 | */ 727 | public JsonObjectBuilder appendField(String key, JsonObject object) { 728 | if (object == null) { 729 | throw new IllegalArgumentException("JSON object must not be null"); 730 | } 731 | appendFieldUnescaped(key, object.toString()); 732 | return this; 733 | } 734 | 735 | /** 736 | * Appends a string array to the JSON. 737 | * 738 | * @param key The key of the field. 739 | * @param values The string array. 740 | * @return A reference to this object. 741 | */ 742 | public JsonObjectBuilder appendField(String key, String[] values) { 743 | if (values == null) { 744 | throw new IllegalArgumentException("JSON values must not be null"); 745 | } 746 | String escapedValues = 747 | Arrays.stream(values) 748 | .map(value -> "\"" + escape(value) + "\"") 749 | .collect(Collectors.joining(",")); 750 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 751 | return this; 752 | } 753 | 754 | /** 755 | * Appends an integer array to the JSON. 756 | * 757 | * @param key The key of the field. 758 | * @param values The integer array. 759 | * @return A reference to this object. 760 | */ 761 | public JsonObjectBuilder appendField(String key, int[] values) { 762 | if (values == null) { 763 | throw new IllegalArgumentException("JSON values must not be null"); 764 | } 765 | String escapedValues = 766 | Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); 767 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 768 | return this; 769 | } 770 | 771 | /** 772 | * Appends an object array to the JSON. 773 | * 774 | * @param key The key of the field. 775 | * @param values The integer array. 776 | * @return A reference to this object. 777 | */ 778 | public JsonObjectBuilder appendField(String key, JsonObject[] values) { 779 | if (values == null) { 780 | throw new IllegalArgumentException("JSON values must not be null"); 781 | } 782 | String escapedValues = 783 | Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); 784 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 785 | return this; 786 | } 787 | 788 | /** 789 | * Appends a field to the object. 790 | * 791 | * @param key The key of the field. 792 | * @param escapedValue The escaped value of the field. 793 | */ 794 | private void appendFieldUnescaped(String key, String escapedValue) { 795 | if (builder == null) { 796 | throw new IllegalStateException("JSON has already been built"); 797 | } 798 | if (key == null) { 799 | throw new IllegalArgumentException("JSON key must not be null"); 800 | } 801 | if (hasAtLeastOneField) { 802 | builder.append(","); 803 | } 804 | builder.append("\"").append(escape(key)).append("\":").append(escapedValue); 805 | hasAtLeastOneField = true; 806 | } 807 | 808 | /** 809 | * Builds the JSON string and invalidates this builder. 810 | * 811 | * @return The built JSON string. 812 | */ 813 | public JsonObject build() { 814 | if (builder == null) { 815 | throw new IllegalStateException("JSON has already been built"); 816 | } 817 | JsonObject object = new JsonObject(builder.append("}").toString()); 818 | builder = null; 819 | return object; 820 | } 821 | 822 | /** 823 | * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. 824 | * 825 | *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. 826 | * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). 827 | * 828 | * @param value The value to escape. 829 | * @return The escaped value. 830 | */ 831 | private static String escape(String value) { 832 | final StringBuilder builder = new StringBuilder(); 833 | for (int i = 0; i < value.length(); i++) { 834 | char c = value.charAt(i); 835 | if (c == '"') { 836 | builder.append("\\\""); 837 | } else if (c == '\\') { 838 | builder.append("\\\\"); 839 | } else if (c <= '\u000F') { 840 | builder.append("\\u000").append(Integer.toHexString(c)); 841 | } else if (c <= '\u001F') { 842 | builder.append("\\u00").append(Integer.toHexString(c)); 843 | } else { 844 | builder.append(c); 845 | } 846 | } 847 | return builder.toString(); 848 | } 849 | 850 | /** 851 | * A super simple representation of a JSON object. 852 | * 853 | *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not 854 | * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, 855 | * JsonObject)}. 856 | */ 857 | public static class JsonObject { 858 | 859 | private final String value; 860 | 861 | private JsonObject(String value) { 862 | this.value = value; 863 | } 864 | 865 | @Override 866 | public String toString() { 867 | return value; 868 | } 869 | } 870 | } 871 | } -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/metrics/MetricsConfig.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.metrics; 2 | 3 | import java.util.UUID; 4 | 5 | public class MetricsConfig { 6 | 7 | final String _notice1 = "bStats (https://bStats.org) collects some basic information for plugin authors, like how"; 8 | final String _notice2 = "many people use their plugin and their total player count. It's recommended to keep bStats"; 9 | final String _notice3 = "enabled, but if you're not comfortable with this, you can turn this setting off. There is no"; 10 | final String _notice4 = "performance penalty associated with having metrics enabled, and data sent to bStats is fully"; 11 | final String _notice5 = "anonymous."; 12 | final String _notice6 = "C2ME-forge bStats page: https://bstats.org/plugin/bukkit/C2ME-forge/10823"; 13 | String serverUuid; 14 | boolean enabled; 15 | boolean logFailedRequests; 16 | boolean logSentData; 17 | boolean logResponseStatusText; 18 | 19 | public MetricsConfig() { 20 | serverUuid = UUID.randomUUID().toString(); 21 | enabled = true; 22 | logFailedRequests = false; 23 | logSentData = false; 24 | logResponseStatusText = false; 25 | } 26 | 27 | public MetricsConfig(String serverUuid, boolean enabled, boolean logFailedRequests, boolean logSentData, boolean logResponseStatusText) { 28 | this.serverUuid = serverUuid; 29 | this.enabled = enabled; 30 | this.logFailedRequests = logFailedRequests; 31 | this.logSentData = logSentData; 32 | this.logResponseStatusText = logResponseStatusText; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/C2MEMixinPlugin.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import org.objectweb.asm.tree.ClassNode; 6 | import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; 7 | import org.spongepowered.asm.mixin.extensibility.IMixinInfo; 8 | import org.yatopiamc.c2me.common.config.C2MEConfig; 9 | 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | public class C2MEMixinPlugin implements IMixinConfigPlugin { 14 | private static final Logger LOGGER = LogManager.getLogger("C2ME Mixin"); 15 | 16 | @Override 17 | public void onLoad(String mixinPackage) { 18 | C2MEConfig.threadedWorldGenConfig.getClass().getName(); // Load configuration 19 | LOGGER.info("Successfully loaded configuration for C2ME"); 20 | } 21 | 22 | @Override 23 | public String getRefMapperConfig() { 24 | return null; 25 | } 26 | 27 | @Override 28 | public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { 29 | if (mixinClassName.startsWith("org.yatopiamc.c2me.mixin.threading.worldgen.")) 30 | return C2MEConfig.threadedWorldGenConfig.enabled; 31 | if (mixinClassName.startsWith("org.yatopiamc.c2me.mixin.threading.chunkio.")) 32 | return C2MEConfig.asyncIoConfig.enabled; 33 | return true; 34 | } 35 | 36 | @Override 37 | public void acceptTargets(Set myTargets, Set otherTargets) { 38 | 39 | } 40 | 41 | @Override 42 | public List getMixins() { 43 | return null; 44 | } 45 | 46 | @Override 47 | public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { 48 | 49 | } 50 | 51 | @Override 52 | public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/chunkscheduling/fix_unload/MixinThreadedAnvilChunkStorage.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.chunkscheduling.fix_unload; 2 | 3 | import it.unimi.dsi.fastutil.longs.LongSet; 4 | import net.minecraft.util.concurrent.ThreadTaskExecutor; 5 | import net.minecraft.village.PointOfInterestManager; 6 | import net.minecraft.world.server.ChunkHolder; 7 | import net.minecraft.world.server.ChunkManager; 8 | import org.spongepowered.asm.mixin.Dynamic; 9 | import org.spongepowered.asm.mixin.Final; 10 | import org.spongepowered.asm.mixin.Mixin; 11 | import org.spongepowered.asm.mixin.Mutable; 12 | import org.spongepowered.asm.mixin.Overwrite; 13 | import org.spongepowered.asm.mixin.Shadow; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Inject; 16 | import org.spongepowered.asm.mixin.injection.Redirect; 17 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 18 | import org.yatopiamc.c2me.common.structs.LongHashSet; 19 | import org.yatopiamc.c2me.common.util.ShouldKeepTickingUtils; 20 | 21 | import java.util.function.BooleanSupplier; 22 | 23 | @Mixin(ChunkManager.class) 24 | public abstract class MixinThreadedAnvilChunkStorage { 25 | 26 | @Shadow @Final private ThreadTaskExecutor mainThreadExecutor; 27 | 28 | @Shadow protected abstract void processUnloads(BooleanSupplier shouldKeepTicking); 29 | 30 | @Mutable 31 | @Shadow @Final private LongSet toDrop; 32 | 33 | /** 34 | * @author ishland 35 | * @reason Queue unload immediately 36 | */ 37 | @SuppressWarnings("OverwriteTarget") 38 | @Dynamic 39 | @Overwrite(aliases = "func_222962_a_") 40 | private void lambda$unpackTicks$38(ChunkHolder holder, Runnable runnable) { // TODO synthetic method in thenApplyAsync call of makeChunkAccessible 41 | this.mainThreadExecutor.execute(runnable); 42 | } 43 | 44 | @Redirect(method = "tick(Ljava/util/function/BooleanSupplier;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/village/PointOfInterestManager;tick(Ljava/util/function/BooleanSupplier;)V")) 45 | private void redirectTickPointOfInterestStorageTick(PointOfInterestManager pointOfInterestStorage, BooleanSupplier shouldKeepTicking) { 46 | pointOfInterestStorage.tick(ShouldKeepTickingUtils.minimumTicks(shouldKeepTicking, 32)); 47 | } 48 | 49 | @Redirect(method = "tick(Ljava/util/function/BooleanSupplier;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/server/ChunkManager;processUnloads(Ljava/util/function/BooleanSupplier;)V")) 50 | private void redirectTickUnloadChunks(ChunkManager threadedAnvilChunkStorage, BooleanSupplier shouldKeepTicking) { 51 | this.processUnloads(ShouldKeepTickingUtils.minimumTicks(shouldKeepTicking, 32)); 52 | } 53 | 54 | @Inject(method = "", at = @At("RETURN")) 55 | private void onInit(CallbackInfo info) { 56 | this.toDrop = new LongHashSet(); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/chunkscheduling/mid_tick_chunk_tasks/MixinMinecraftServer.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.chunkscheduling.mid_tick_chunk_tasks; 2 | 3 | import net.minecraft.server.MinecraftServer; 4 | import net.minecraft.world.server.ServerWorld; 5 | import org.spongepowered.asm.mixin.Final; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.Shadow; 8 | import org.yatopiamc.c2me.common.chunkscheduling.ServerMidTickTask; 9 | import org.yatopiamc.c2me.mixin.util.accessor.IThreadTaskExecutor; 10 | 11 | import java.util.concurrent.atomic.AtomicLong; 12 | 13 | @Mixin(MinecraftServer.class) 14 | public abstract class MixinMinecraftServer implements ServerMidTickTask { 15 | 16 | @Shadow public abstract Iterable getAllLevels(); 17 | 18 | @Shadow @Final private Thread serverThread; 19 | private static final long minMidTickTaskInterval = 25_000L; // 25us 20 | private final AtomicLong lastRun = new AtomicLong(System.nanoTime()); 21 | 22 | public void executeTasksMidTick() { 23 | if (this.serverThread != Thread.currentThread()) return; 24 | if (System.nanoTime() - lastRun.get() < minMidTickTaskInterval) return; 25 | for (ServerWorld world : this.getAllLevels()) { 26 | ((IThreadTaskExecutor) world.getChunkSource().mainThreadProcessor).IPollTask(); 27 | } 28 | lastRun.set(System.nanoTime()); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/chunkscheduling/mid_tick_chunk_tasks/MixinServerChunkManager.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.chunkscheduling.mid_tick_chunk_tasks; 2 | 3 | import net.minecraft.world.server.ServerChunkProvider; 4 | import net.minecraft.world.server.ServerWorld; 5 | import org.spongepowered.asm.mixin.Dynamic; 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 | import org.yatopiamc.c2me.common.chunkscheduling.ServerMidTickTask; 13 | 14 | @Mixin(ServerChunkProvider.class) 15 | public class MixinServerChunkManager { 16 | 17 | @Shadow @Final 18 | public ServerWorld level; 19 | 20 | @Dynamic 21 | @Inject(method = {"lambda$tickChunks$5", "func_241099_a_"}, at = @At(value = "INVOKE", target = "Lnet/minecraft/world/server/ServerWorld;tickChunk(Lnet/minecraft/world/chunk/Chunk;I)V")) 22 | private void onPostTickChunk(CallbackInfo ci) { // TODO synthetic method - in tickChunks() 23 | ((ServerMidTickTask) this.level.getServer()).executeTasksMidTick(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/chunkscheduling/mid_tick_chunk_tasks/MixinServerTickScheduler.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.chunkscheduling.mid_tick_chunk_tasks; 2 | 3 | import net.minecraft.world.server.ServerTickList; 4 | import net.minecraft.world.server.ServerWorld; 5 | import org.spongepowered.asm.mixin.Final; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.Shadow; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | import org.yatopiamc.c2me.common.chunkscheduling.ServerMidTickTask; 12 | 13 | @Mixin(ServerTickList.class) 14 | public class MixinServerTickScheduler { 15 | 16 | @Shadow @Final public ServerWorld level; 17 | 18 | @Inject(method = "tick", at = @At(value = "INVOKE", target = "Ljava/util/function/Consumer;accept(Ljava/lang/Object;)V", shift = At.Shift.AFTER)) 19 | private void onPostActionTick(CallbackInfo ci) { 20 | ((ServerMidTickTask) this.level.getServer()).executeTasksMidTick(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/chunkscheduling/mid_tick_chunk_tasks/MixinWorld.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.chunkscheduling.mid_tick_chunk_tasks; 2 | 3 | import net.minecraft.server.MinecraftServer; 4 | import net.minecraft.world.World; 5 | import org.spongepowered.asm.mixin.Final; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.Shadow; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | import org.yatopiamc.c2me.common.chunkscheduling.ServerMidTickTask; 12 | 13 | import javax.annotation.Nullable; 14 | 15 | @Mixin(World.class) 16 | public abstract class MixinWorld { 17 | 18 | @Shadow @Nullable 19 | public abstract MinecraftServer getServer(); 20 | 21 | @Shadow @Final public boolean isClientSide; 22 | 23 | @Inject(method = "guardEntityTick", at = @At("TAIL")) 24 | private void onPostTickEntity(CallbackInfo ci) { 25 | final MinecraftServer server = this.getServer(); 26 | if (!this.isClientSide && server != null) { 27 | ((ServerMidTickTask) server).executeTasksMidTick(); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/chunkscheduling/package-info.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.chunkscheduling; -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/optimizations/package-info.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.optimizations; -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/optimizations/thread_local_biome_sampler/MixinOverworldBiomeProvider.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.optimizations.thread_local_biome_sampler; 2 | 3 | import net.minecraft.util.registry.Registry; 4 | import net.minecraft.world.biome.Biome; 5 | import net.minecraft.world.biome.provider.OverworldBiomeProvider; 6 | import net.minecraft.world.gen.layer.Layer; 7 | import net.minecraft.world.gen.layer.LayerUtil; 8 | import org.spongepowered.asm.mixin.Final; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Overwrite; 11 | import org.spongepowered.asm.mixin.Shadow; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 15 | 16 | @Mixin(OverworldBiomeProvider.class) 17 | public class MixinOverworldBiomeProvider { 18 | 19 | @Shadow @Final private Registry biomes; 20 | private ThreadLocal layerThreadLocal = new ThreadLocal<>(); 21 | 22 | @Inject(method = "", at = @At("RETURN")) 23 | private void onInit(long p_i241958_1_, boolean p_i241958_3_, boolean p_i241958_4_, Registry p_i241958_5_, CallbackInfo ci) { 24 | this.layerThreadLocal = ThreadLocal.withInitial(() -> LayerUtil.getDefaultLayer(p_i241958_1_, p_i241958_3_, p_i241958_4_ ? 6 : 4, 4)); // [VanillaCopy] 25 | } 26 | 27 | /** 28 | * @author ishland 29 | * @reason use thread_local sampler 30 | */ 31 | @Overwrite 32 | public Biome getNoiseBiome(int p_225526_1_, int p_225526_2_, int p_225526_3_) { 33 | return this.layerThreadLocal.get().get(this.biomes, p_225526_1_, p_225526_3_); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/package-info.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin; -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/chunkio/MixinChunkSerializer.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.chunkio; 2 | 3 | import net.minecraft.block.Block; 4 | import net.minecraft.fluid.Fluid; 5 | import net.minecraft.nbt.CompoundNBT; 6 | import net.minecraft.nbt.ListNBT; 7 | import net.minecraft.tileentity.TileEntity; 8 | import net.minecraft.util.math.BlockPos; 9 | import net.minecraft.util.math.ChunkPos; 10 | import net.minecraft.village.PointOfInterestManager; 11 | import net.minecraft.world.ITickList; 12 | import net.minecraft.world.LightType; 13 | import net.minecraft.world.chunk.Chunk; 14 | import net.minecraft.world.chunk.ChunkSection; 15 | import net.minecraft.world.chunk.IChunk; 16 | import net.minecraft.world.chunk.storage.ChunkSerializer; 17 | import net.minecraft.world.lighting.IWorldLightListener; 18 | import net.minecraft.world.lighting.WorldLightManager; 19 | import net.minecraft.world.server.ServerTickList; 20 | import org.spongepowered.asm.mixin.Mixin; 21 | import org.spongepowered.asm.mixin.injection.At; 22 | import org.spongepowered.asm.mixin.injection.Redirect; 23 | import org.yatopiamc.c2me.common.threading.chunkio.AsyncSerializationManager; 24 | import org.yatopiamc.c2me.common.threading.chunkio.ChunkIoMainThreadTaskUtils; 25 | 26 | import java.util.Set; 27 | import java.util.concurrent.CompletableFuture; 28 | 29 | @Mixin(ChunkSerializer.class) 30 | public class MixinChunkSerializer { 31 | 32 | @Redirect(method = "read", at = @At(value = "INVOKE", target = "Lnet/minecraft/village/PointOfInterestManager;checkConsistencyWithBlocks(Lnet/minecraft/util/math/ChunkPos;Lnet/minecraft/world/chunk/ChunkSection;)V")) 33 | private static void onPoiStorageInitForPalette(PointOfInterestManager pointOfInterestStorage, ChunkPos chunkPos, ChunkSection chunkSection) { 34 | ChunkIoMainThreadTaskUtils.executeMain(() -> pointOfInterestStorage.checkConsistencyWithBlocks(chunkPos, chunkSection)); 35 | } 36 | 37 | @Redirect(method = "write", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/chunk/IChunk;getBlockEntitiesPos()Ljava/util/Set;")) 38 | private static Set onChunkGetBlockEntityPositions(IChunk chunk) { 39 | final AsyncSerializationManager.Scope scope = AsyncSerializationManager.getScope(chunk.getPos()); 40 | return scope != null ? scope.blockEntities.keySet() : chunk.getBlockEntitiesPos(); 41 | } 42 | 43 | @Redirect(method = "write", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/chunk/IChunk;getBlockEntityNbtForSaving(Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/nbt/CompoundNBT;")) 44 | private static CompoundNBT onChunkGetPackedBlockEntityTag(IChunk chunk, BlockPos pos) { 45 | final AsyncSerializationManager.Scope scope = AsyncSerializationManager.getScope(chunk.getPos()); 46 | if (scope == null) return chunk.getBlockEntityNbtForSaving(pos); 47 | final TileEntity blockEntity = scope.blockEntities.get(pos); 48 | if (blockEntity == null || blockEntity.isRemoved()) return null; 49 | final CompoundNBT compoundTag = new CompoundNBT(); 50 | if (chunk instanceof Chunk) compoundTag.putBoolean("keepPacked", false); 51 | blockEntity.save(compoundTag); 52 | return compoundTag; 53 | } 54 | 55 | @Redirect(method = "write", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/chunk/IChunk;getBlockTicks()Lnet/minecraft/world/ITickList;")) 56 | private static ITickList onChunkGetBlockTickScheduler(IChunk chunk) { 57 | final AsyncSerializationManager.Scope scope = AsyncSerializationManager.getScope(chunk.getPos()); 58 | return scope != null ? scope.blockTickScheduler : chunk.getBlockTicks(); 59 | } 60 | 61 | @Redirect(method = "write", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/chunk/IChunk;getLiquidTicks()Lnet/minecraft/world/ITickList;")) 62 | private static ITickList onChunkGetFluidTickScheduler(IChunk chunk) { 63 | final AsyncSerializationManager.Scope scope = AsyncSerializationManager.getScope(chunk.getPos()); 64 | return scope != null ? scope.fluidTickScheduler : chunk.getLiquidTicks(); 65 | } 66 | 67 | @Redirect(method = "write", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/server/ServerTickList;save(Lnet/minecraft/util/math/ChunkPos;)Lnet/minecraft/nbt/ListNBT;")) 68 | private static ListNBT onServerTickSchedulerToTag(@SuppressWarnings("rawtypes") ServerTickList serverTickScheduler, ChunkPos chunkPos) { 69 | final AsyncSerializationManager.Scope scope = AsyncSerializationManager.getScope(chunkPos); 70 | return scope != null ? CompletableFuture.supplyAsync(() -> serverTickScheduler.save(chunkPos), serverTickScheduler.level.chunkSource.mainThreadProcessor).join() : serverTickScheduler.save(chunkPos); 71 | } 72 | 73 | @Redirect(method = "write", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/lighting/WorldLightManager;getLayerListener(Lnet/minecraft/world/LightType;)Lnet/minecraft/world/lighting/IWorldLightListener;")) 74 | private static IWorldLightListener onLightingProviderGet(WorldLightManager lightingProvider, LightType lightType) { 75 | final AsyncSerializationManager.Scope scope = AsyncSerializationManager.getScope(null); 76 | return scope != null ? scope.lighting.get(lightType) : lightingProvider.getLayerListener(lightType); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/chunkio/MixinChunkTickScheduler.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.chunkio; 2 | 3 | import net.minecraft.nbt.ListNBT; 4 | import net.minecraft.util.math.ChunkPos; 5 | import net.minecraft.world.chunk.ChunkPrimerTickList; 6 | import org.spongepowered.asm.mixin.Final; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.Shadow; 9 | import org.yatopiamc.c2me.common.util.DeepCloneable; 10 | 11 | import java.util.function.Predicate; 12 | 13 | @Mixin(ChunkPrimerTickList.class) 14 | public abstract class MixinChunkTickScheduler implements DeepCloneable { 15 | 16 | @Shadow 17 | public abstract ListNBT save(); 18 | 19 | @Shadow 20 | @Final 21 | private ChunkPos chunkPos; 22 | @Shadow @Final protected Predicate ignore; 23 | 24 | public ChunkPrimerTickList deepClone() { 25 | return new ChunkPrimerTickList<>(ignore, chunkPos, save()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/chunkio/MixinScheduledTick.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.chunkio; 2 | 3 | import net.minecraft.world.NextTickListEntry; 4 | import org.spongepowered.asm.mixin.Final; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.Mutable; 7 | import org.spongepowered.asm.mixin.Shadow; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | 12 | import java.util.concurrent.atomic.AtomicLong; 13 | 14 | @Mixin(NextTickListEntry.class) 15 | public class MixinScheduledTick { 16 | 17 | @Mutable 18 | @Shadow @Final private long c; 19 | private static final AtomicLong COUNTER = new AtomicLong(0); 20 | 21 | @Inject(method = "", at = @At("TAIL")) 22 | private void onInit(CallbackInfo info) { 23 | this.c = COUNTER.getAndIncrement(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/chunkio/MixinSerializingRegionBasedStorage.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.chunkio; 2 | 3 | import com.mojang.serialization.DynamicOps; 4 | import net.minecraft.nbt.CompoundNBT; 5 | import net.minecraft.nbt.NBTDynamicOps; 6 | import net.minecraft.util.math.ChunkPos; 7 | import net.minecraft.world.chunk.storage.RegionSectionCache; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.Shadow; 10 | import org.yatopiamc.c2me.common.threading.chunkio.ISerializingRegionBasedStorage; 11 | 12 | import javax.annotation.Nullable; 13 | 14 | @Mixin(RegionSectionCache.class) 15 | public abstract class MixinSerializingRegionBasedStorage implements ISerializingRegionBasedStorage { 16 | 17 | @Shadow protected abstract void readColumn(ChunkPos p_235992_1_, DynamicOps p_235992_2_, @Nullable T p_235992_3_); 18 | 19 | @Override 20 | public void update(ChunkPos pos, CompoundNBT tag) { 21 | this.readColumn(pos, NBTDynamicOps.INSTANCE, tag); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/chunkio/MixinServerChunkManagerMainThreadExecutor.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.chunkio; 2 | 3 | import net.minecraft.util.concurrent.ThreadTaskExecutor; 4 | import net.minecraft.world.server.ServerChunkProvider; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 9 | 10 | @Mixin(ServerChunkProvider.ChunkExecutor.class) 11 | public abstract class MixinServerChunkManagerMainThreadExecutor extends ThreadTaskExecutor { 12 | 13 | protected MixinServerChunkManagerMainThreadExecutor(String name) { 14 | super(name); 15 | } 16 | 17 | @Inject(method = "pollTask", at = @At("RETURN")) 18 | private void onPostRunTask(CallbackInfoReturnable cir) { 19 | super.pollTask(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/chunkio/MixinSimpleTickScheduler.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.chunkio; 2 | 3 | import it.unimi.dsi.fastutil.objects.ObjectArrayList; 4 | import net.minecraft.util.ResourceLocation; 5 | import net.minecraft.world.ITickList; 6 | import net.minecraft.world.SerializableTickList; 7 | import org.spongepowered.asm.mixin.Final; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.Shadow; 10 | import org.yatopiamc.c2me.common.util.DeepCloneable; 11 | 12 | import java.util.function.Function; 13 | 14 | @Mixin(SerializableTickList.class) 15 | public abstract class MixinSimpleTickScheduler implements DeepCloneable { 16 | 17 | @Shadow @Final private Function toId; 18 | 19 | @Shadow public abstract void copyOut(ITickList scheduler); 20 | 21 | @Override 22 | public Object deepClone() { 23 | final SerializableTickList scheduler = new SerializableTickList<>(toId, new ObjectArrayList<>()); 24 | copyOut(scheduler); 25 | return scheduler; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/chunkio/MixinStorageIoWorker.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.chunkio; 2 | 3 | import com.mojang.datafixers.util.Either; 4 | import net.minecraft.nbt.CompoundNBT; 5 | import net.minecraft.util.math.ChunkPos; 6 | import net.minecraft.world.chunk.storage.IOWorker; 7 | import net.minecraft.world.chunk.storage.RegionFileCache; 8 | import org.apache.logging.log4j.Logger; 9 | import org.spongepowered.asm.mixin.Final; 10 | import org.spongepowered.asm.mixin.Mixin; 11 | import org.spongepowered.asm.mixin.Shadow; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.Redirect; 15 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 16 | import org.yatopiamc.c2me.common.threading.chunkio.C2MECachedRegionStorage; 17 | import org.yatopiamc.c2me.common.threading.chunkio.ChunkIoThreadingExecutorUtils; 18 | import org.yatopiamc.c2me.common.threading.chunkio.IAsyncChunkStorage; 19 | 20 | import java.util.Map; 21 | import java.util.concurrent.CompletableFuture; 22 | import java.util.concurrent.Executor; 23 | import java.util.concurrent.ExecutorService; 24 | import java.util.concurrent.Executors; 25 | import java.util.concurrent.atomic.AtomicBoolean; 26 | import java.util.concurrent.atomic.AtomicReference; 27 | import java.util.function.Supplier; 28 | 29 | @Mixin(IOWorker.class) 30 | public abstract class MixinStorageIoWorker implements IAsyncChunkStorage { 31 | 32 | @Shadow @Final private AtomicBoolean shutdownRequested; 33 | 34 | @Shadow @Final private static Logger LOGGER; 35 | 36 | @Shadow protected abstract CompletableFuture submitTask(Supplier> p_235975_1_); 37 | 38 | @Shadow @Final private RegionFileCache storage; 39 | 40 | @Shadow @Final private Map pendingWrites; 41 | 42 | @Inject(method = "", at = @At("RETURN")) 43 | private void onPostInit(CallbackInfo info) { 44 | //noinspection ConstantConditions 45 | if (((Object) this) instanceof C2MECachedRegionStorage) { 46 | this.shutdownRequested.set(true); 47 | } 48 | } 49 | 50 | private AtomicReference executorService = new AtomicReference<>(); 51 | 52 | @Redirect(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;ioPool()Ljava/util/concurrent/Executor;")) 53 | private Executor onGetStorageIoWorker() { 54 | executorService.set(Executors.newSingleThreadExecutor(ChunkIoThreadingExecutorUtils.ioWorkerFactory)); 55 | return executorService.get(); 56 | } 57 | 58 | @Inject(method = "close", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/concurrent/DelegatedTaskExecutor;close()V", shift = At.Shift.AFTER)) 59 | private void onClose(CallbackInfo ci) { 60 | final ExecutorService executorService = this.executorService.get(); 61 | if (executorService != null) executorService.shutdown(); 62 | } 63 | 64 | @Override 65 | public CompletableFuture getNbtAtAsync(ChunkPos pos) { 66 | // TODO [VanillaCopy] 67 | return this.submitTask(() -> { 68 | IOWorker.Entry result = (IOWorker.Entry)this.pendingWrites.get(pos); 69 | if (result != null) { 70 | return Either.left(result.data); 71 | } else { 72 | try { 73 | CompoundNBT compoundTag = this.storage.read(pos); 74 | return Either.left(compoundTag); 75 | } catch (Exception var4) { 76 | LOGGER.warn((String)"Failed to read chunk {}", (Object)pos, (Object)var4); 77 | return Either.right(var4); 78 | } 79 | } 80 | }); 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/chunkio/MixinThreadedAnvilChunkStorage.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.chunkio; 2 | 3 | import com.ibm.asyncutil.locks.AsyncNamedLock; 4 | import com.mojang.datafixers.DataFixer; 5 | import com.mojang.datafixers.util.Either; 6 | import net.minecraft.nbt.CompoundNBT; 7 | import net.minecraft.util.concurrent.ThreadTaskExecutor; 8 | import net.minecraft.util.math.ChunkPos; 9 | import net.minecraft.util.palette.UpgradeData; 10 | import net.minecraft.village.PointOfInterestManager; 11 | import net.minecraft.world.chunk.ChunkPrimer; 12 | import net.minecraft.world.chunk.ChunkStatus; 13 | import net.minecraft.world.chunk.IChunk; 14 | import net.minecraft.world.chunk.storage.ChunkLoader; 15 | import net.minecraft.world.chunk.storage.ChunkSerializer; 16 | import net.minecraft.world.gen.feature.structure.StructureStart; 17 | import net.minecraft.world.gen.feature.template.TemplateManager; 18 | import net.minecraft.world.server.ChunkHolder; 19 | import net.minecraft.world.server.ChunkManager; 20 | import net.minecraft.world.server.ServerWorld; 21 | import net.minecraft.world.storage.DimensionSavedDataManager; 22 | import org.apache.logging.log4j.Logger; 23 | import org.spongepowered.asm.mixin.Dynamic; 24 | import org.spongepowered.asm.mixin.Final; 25 | import org.spongepowered.asm.mixin.Mixin; 26 | import org.spongepowered.asm.mixin.Overwrite; 27 | import org.spongepowered.asm.mixin.Shadow; 28 | import org.spongepowered.asm.mixin.injection.At; 29 | import org.spongepowered.asm.mixin.injection.Inject; 30 | import org.spongepowered.asm.mixin.injection.Redirect; 31 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 32 | import org.yatopiamc.c2me.common.threading.chunkio.AsyncSerializationManager; 33 | import org.yatopiamc.c2me.common.threading.chunkio.C2MECachedRegionStorage; 34 | import org.yatopiamc.c2me.common.threading.chunkio.ChunkIoMainThreadTaskUtils; 35 | import org.yatopiamc.c2me.common.threading.chunkio.ChunkIoThreadingExecutorUtils; 36 | import org.yatopiamc.c2me.common.threading.chunkio.IAsyncChunkStorage; 37 | import org.yatopiamc.c2me.common.threading.chunkio.ISerializingRegionBasedStorage; 38 | import org.yatopiamc.c2me.common.util.SneakyThrow; 39 | 40 | import java.io.File; 41 | import java.util.HashSet; 42 | import java.util.Set; 43 | import java.util.concurrent.CompletableFuture; 44 | import java.util.concurrent.ConcurrentLinkedQueue; 45 | import java.util.function.Supplier; 46 | 47 | @Mixin(ChunkManager.class) 48 | public abstract class MixinThreadedAnvilChunkStorage extends ChunkLoader implements ChunkHolder.IPlayerProvider { 49 | 50 | public MixinThreadedAnvilChunkStorage(File file, DataFixer dataFixer, boolean bl) { 51 | super(file, dataFixer, bl); 52 | } 53 | 54 | @Shadow 55 | @Final 56 | private ServerWorld level; 57 | 58 | @Shadow 59 | @Final 60 | private TemplateManager structureManager; 61 | 62 | @Shadow 63 | @Final 64 | private PointOfInterestManager poiManager; 65 | 66 | @Shadow 67 | protected abstract byte markPosition(ChunkPos chunkPos, ChunkStatus.Type chunkType); 68 | 69 | @Shadow 70 | @Final 71 | private static Logger LOGGER; 72 | 73 | @Shadow 74 | protected abstract void markPositionReplaceable(ChunkPos chunkPos); 75 | 76 | @Shadow 77 | @Final 78 | private Supplier overworldDataStorage; 79 | 80 | @Shadow 81 | @Final 82 | private ThreadTaskExecutor mainThreadExecutor; 83 | 84 | @Shadow 85 | protected abstract boolean isExistingChunkFull(ChunkPos chunkPos); 86 | 87 | private AsyncNamedLock chunkLock = AsyncNamedLock.createFair(); 88 | 89 | @Inject(method = "", at = @At("RETURN")) 90 | private void onInit(CallbackInfo info) { 91 | chunkLock = AsyncNamedLock.createFair(); 92 | } 93 | 94 | private Set scheduledChunks = new HashSet<>(); 95 | 96 | /** 97 | * @author ishland 98 | * @reason async io and deserialization 99 | */ 100 | @Overwrite 101 | private CompletableFuture> scheduleChunkLoad(ChunkPos pos) { 102 | if (scheduledChunks == null) scheduledChunks = new HashSet<>(); 103 | synchronized (scheduledChunks) { 104 | if (scheduledChunks.contains(pos)) throw new IllegalArgumentException("Already scheduled"); 105 | scheduledChunks.add(pos); 106 | } 107 | 108 | final CompletableFuture poiData = ((IAsyncChunkStorage) this.poiManager.worker).getNbtAtAsync(pos); 109 | 110 | final CompletableFuture> future = getUpdatedChunkTagAtAsync(pos).thenApplyAsync(compoundTag -> { 111 | if (compoundTag != null) { 112 | try { 113 | if (compoundTag.contains("Level", 10) && compoundTag.getCompound("Level").contains("Status", 8)) { 114 | return ChunkSerializer.read(this.level, this.structureManager, this.poiManager, pos, compoundTag); 115 | } 116 | 117 | LOGGER.warn("Chunk file at {} is missing level data, skipping", pos); 118 | } catch (Throwable t) { 119 | LOGGER.error("Couldn't load chunk {}, chunk data will be lost!", pos, t); 120 | } 121 | } 122 | return null; 123 | }, ChunkIoThreadingExecutorUtils.serializerExecutor).thenCombine(poiData, (protoChunk, tag) -> protoChunk).thenApplyAsync(protoChunk -> { 124 | ((ISerializingRegionBasedStorage) this.poiManager).update(pos, poiData.join()); 125 | ChunkIoMainThreadTaskUtils.drainQueue(); 126 | if (protoChunk != null) { 127 | protoChunk.setLastSaveTime(this.level.getGameTime()); 128 | this.markPosition(pos, protoChunk.getStatus().getChunkType()); 129 | return Either.left(protoChunk); 130 | } else { 131 | this.markPositionReplaceable(pos); 132 | return Either.left(new ChunkPrimer(pos, UpgradeData.EMPTY)); 133 | } 134 | }, this.mainThreadExecutor); 135 | future.exceptionally(throwable -> null).thenRun(() -> { 136 | synchronized (scheduledChunks) { 137 | scheduledChunks.remove(pos); 138 | } 139 | }); 140 | return future; 141 | 142 | // [VanillaCopy] - for reference 143 | /* 144 | return CompletableFuture.supplyAsync(() -> { 145 | try { 146 | CompoundTag compoundTag = this.getUpdatedChunkTag(pos); 147 | if (compoundTag != null) { 148 | boolean bl = compoundTag.contains("Level", 10) && compoundTag.getCompound("Level").contains("Status", 8); 149 | if (bl) { 150 | Chunk chunk = ChunkSerializer.deserialize(this.world, this.structureManager, this.pointOfInterestStorage, pos, compoundTag); 151 | chunk.setLastSaveTime(this.world.getTime()); 152 | this.method_27053(pos, chunk.getStatus().getChunkType()); 153 | return Either.left(chunk); 154 | } 155 | 156 | LOGGER.error((String)"Chunk file at {} is missing level data, skipping", (Object)pos); 157 | } 158 | } catch (CrashException var5) { 159 | Throwable throwable = var5.getCause(); 160 | if (!(throwable instanceof IOException)) { 161 | this.method_27054(pos); 162 | throw var5; 163 | } 164 | 165 | LOGGER.error("Couldn't load chunk {}", pos, throwable); 166 | } catch (Exception var6) { 167 | LOGGER.error("Couldn't load chunk {}", pos, var6); 168 | } 169 | 170 | this.method_27054(pos); 171 | return Either.left(new ProtoChunk(pos, UpgradeData.NO_UPGRADE_DATA)); 172 | }, this.mainThreadExecutor); 173 | */ 174 | } 175 | 176 | private CompletableFuture getUpdatedChunkTagAtAsync(ChunkPos pos) { 177 | return chunkLock.acquireLock(pos).toCompletableFuture().thenCompose(lockToken -> ((IAsyncChunkStorage) this.worker).getNbtAtAsync(pos).thenApply(compoundTag -> { 178 | if (compoundTag != null) 179 | return this.upgradeChunkTag(this.level.dimension(), this.overworldDataStorage, compoundTag); 180 | else return null; 181 | }).handle((tag, throwable) -> { 182 | lockToken.releaseLock(); 183 | if (throwable != null) 184 | SneakyThrow.sneaky(throwable); 185 | return tag; 186 | })); 187 | } 188 | 189 | private ConcurrentLinkedQueue> saveFutures = new ConcurrentLinkedQueue<>(); 190 | 191 | @Dynamic 192 | @Redirect(method = {"lambda$scheduleUnload$10", "func_219185_a"}, at = @At(value = "INVOKE", target = "Lnet/minecraft/world/server/ChunkManager;save(Lnet/minecraft/world/chunk/IChunk;)Z")) // method: consumer in tryUnloadChunk 193 | private boolean asyncSave(ChunkManager tacs, IChunk p_219229_1_) { 194 | // TODO [VanillaCopy] - check when updating minecraft version 195 | this.poiManager.flush(p_219229_1_.getPos()); 196 | if (!p_219229_1_.isUnsaved()) { 197 | return false; 198 | } else { 199 | p_219229_1_.setLastSaveTime(this.level.getGameTime()); 200 | p_219229_1_.setUnsaved(false); 201 | ChunkPos chunkpos = p_219229_1_.getPos(); 202 | 203 | try { 204 | ChunkStatus chunkstatus = p_219229_1_.getStatus(); 205 | if (chunkstatus.getChunkType() != ChunkStatus.Type.LEVELCHUNK) { 206 | if (this.isExistingChunkFull(chunkpos)) { 207 | return false; 208 | } 209 | 210 | if (chunkstatus == ChunkStatus.EMPTY && p_219229_1_.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) { 211 | return false; 212 | } 213 | } 214 | 215 | this.level.getProfiler().incrementCounter("chunkSave"); 216 | // C2ME start - async serialization 217 | if (saveFutures == null) saveFutures = new ConcurrentLinkedQueue<>(); 218 | AsyncSerializationManager.Scope scope = new AsyncSerializationManager.Scope(p_219229_1_, level); 219 | 220 | saveFutures.add(chunkLock.acquireLock(p_219229_1_.getPos()).toCompletableFuture().thenCompose(lockToken -> 221 | CompletableFuture.supplyAsync(() -> { 222 | scope.open(); 223 | AsyncSerializationManager.push(scope); 224 | try { 225 | return ChunkSerializer.write(this.level, p_219229_1_); 226 | } finally { 227 | AsyncSerializationManager.pop(scope); 228 | } 229 | }, ChunkIoThreadingExecutorUtils.serializerExecutor) 230 | .thenAcceptAsync(compoundTag -> { 231 | net.minecraftforge.common.MinecraftForge.EVENT_BUS.post(new net.minecraftforge.event.world.ChunkDataEvent.Save(p_219229_1_, p_219229_1_.getWorldForge() != null ? p_219229_1_.getWorldForge() : this.level, compoundTag)); // [VanillaCopy] 232 | this.write(chunkpos, compoundTag); 233 | }, this.mainThreadExecutor) 234 | .handle((unused, throwable) -> { 235 | lockToken.releaseLock(); 236 | if (throwable != null) 237 | LOGGER.error("Failed to save chunk {},{}", chunkpos.x, chunkpos.z, throwable); 238 | return unused; 239 | }))); 240 | this.markPosition(chunkpos, chunkstatus.getChunkType()); 241 | // C2ME end 242 | return true; 243 | } catch (Exception exception) { 244 | LOGGER.error("Failed to save chunk {},{}", chunkpos.x, chunkpos.z, exception); 245 | return false; 246 | } 247 | } 248 | } 249 | 250 | @Inject(method = "tick(Ljava/util/function/BooleanSupplier;)V", at = @At("HEAD")) 251 | private void onTick(CallbackInfo info) { 252 | ChunkIoThreadingExecutorUtils.serializerExecutor.execute(() -> saveFutures.removeIf(CompletableFuture::isDone)); 253 | } 254 | 255 | @Override 256 | public void flushWorker() { 257 | final CompletableFuture future = CompletableFuture.allOf(saveFutures.toArray(new CompletableFuture[0])); 258 | this.mainThreadExecutor.managedBlock(future::isDone); // wait for serialization to complete 259 | super.flushWorker(); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/chunkio/MixinVersionedChunkStorage.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.chunkio; 2 | 3 | import com.ibm.asyncutil.locks.AsyncLock; 4 | import com.mojang.datafixers.DataFixer; 5 | import net.minecraft.nbt.CompoundNBT; 6 | import net.minecraft.nbt.NBTUtil; 7 | import net.minecraft.util.RegistryKey; 8 | import net.minecraft.util.SharedConstants; 9 | import net.minecraft.util.datafix.DefaultTypeReferences; 10 | import net.minecraft.world.World; 11 | import net.minecraft.world.chunk.storage.ChunkLoader; 12 | import net.minecraft.world.chunk.storage.IOWorker; 13 | import net.minecraft.world.gen.feature.structure.LegacyStructureDataUtil; 14 | import net.minecraft.world.storage.DimensionSavedDataManager; 15 | import org.spongepowered.asm.mixin.Final; 16 | import org.spongepowered.asm.mixin.Mixin; 17 | import org.spongepowered.asm.mixin.Overwrite; 18 | import org.spongepowered.asm.mixin.Shadow; 19 | import org.spongepowered.asm.mixin.injection.At; 20 | import org.spongepowered.asm.mixin.injection.Inject; 21 | import org.spongepowered.asm.mixin.injection.Redirect; 22 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 23 | import org.yatopiamc.c2me.common.threading.chunkio.C2MECachedRegionStorage; 24 | 25 | import javax.annotation.Nullable; 26 | import java.io.File; 27 | import java.util.function.Supplier; 28 | 29 | @Mixin(ChunkLoader.class) 30 | public abstract class MixinVersionedChunkStorage { 31 | 32 | @Shadow @Final protected DataFixer fixerUpper; 33 | 34 | @Shadow @Nullable 35 | private LegacyStructureDataUtil legacyStructureHandler; 36 | 37 | private AsyncLock featureUpdaterLock = AsyncLock.createFair(); 38 | 39 | @Inject(method = "", at = @At("RETURN")) 40 | private void onInit(CallbackInfo info) { 41 | this.featureUpdaterLock = AsyncLock.createFair(); 42 | } 43 | 44 | /** 45 | * @author ishland 46 | * @reason async loading 47 | */ 48 | @Overwrite 49 | public CompoundNBT upgradeChunkTag(RegistryKey registryKey, Supplier persistentStateManagerFactory, CompoundNBT tag) { 50 | // TODO [VanillaCopy] - check when updating minecraft version 51 | int i = ChunkLoader.getVersion(tag); 52 | if (i < 1493) { 53 | try (final AsyncLock.LockToken ignored = featureUpdaterLock.acquireLock().toCompletableFuture().join()) { // C2ME - async chunk loading 54 | tag = NBTUtil.update(this.fixerUpper, DefaultTypeReferences.CHUNK, tag, i, 1493); 55 | if (tag.getCompound("Level").getBoolean("hasLegacyStructureData")) { 56 | if (this.legacyStructureHandler == null) { 57 | this.legacyStructureHandler = LegacyStructureDataUtil.getLegacyStructureHandler(registryKey, persistentStateManagerFactory.get()); 58 | } 59 | 60 | tag = this.legacyStructureHandler.updateFromLegacy(tag); 61 | } 62 | } // C2ME - async chunk loading 63 | } 64 | 65 | tag = NBTUtil.update(this.fixerUpper, DefaultTypeReferences.CHUNK, tag, Math.max(1493, i)); 66 | if (i < SharedConstants.getCurrentVersion().getWorldVersion()) { 67 | tag.putInt("DataVersion", SharedConstants.getCurrentVersion().getWorldVersion()); 68 | } 69 | 70 | return tag; 71 | } 72 | 73 | @Redirect(method = "write", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/gen/feature/structure/LegacyStructureDataUtil;removeIndex(J)V")) 74 | private void onSetTagAtFeatureUpdaterMarkResolved(LegacyStructureDataUtil featureUpdater, long l) { 75 | try (final AsyncLock.LockToken ignored = featureUpdaterLock.acquireLock().toCompletableFuture().join()) { 76 | featureUpdater.removeIndex(l); 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/lighting/MixinServerLightingProvider.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.lighting; 2 | 3 | import net.minecraft.world.server.ServerWorldLightManager; 4 | import org.spongepowered.asm.mixin.Dynamic; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.Shadow; 7 | import org.spongepowered.asm.mixin.injection.At; 8 | import org.spongepowered.asm.mixin.injection.Inject; 9 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 10 | 11 | @Mixin(ServerWorldLightManager.class) 12 | public abstract class MixinServerLightingProvider { 13 | 14 | @Shadow public abstract void tryScheduleUpdate(); 15 | 16 | @Dynamic 17 | @Inject(method = {"lambda$tryScheduleUpdate$22", "func_223124_c"}, at = @At("RETURN")) 18 | private void onPostRunTask(CallbackInfo info) { 19 | this.tryScheduleUpdate(); // Run more tasks 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/worldgen/MixinChunkStatus.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.worldgen; 2 | 3 | import com.mojang.datafixers.util.Either; 4 | import net.minecraft.util.registry.Registry; 5 | import net.minecraft.world.chunk.ChunkStatus; 6 | import net.minecraft.world.chunk.IChunk; 7 | import net.minecraft.world.gen.ChunkGenerator; 8 | import net.minecraft.world.gen.feature.template.TemplateManager; 9 | import net.minecraft.world.server.ChunkHolder; 10 | import net.minecraft.world.server.ServerWorld; 11 | import net.minecraft.world.server.ServerWorldLightManager; 12 | import org.spongepowered.asm.mixin.Dynamic; 13 | import org.spongepowered.asm.mixin.Final; 14 | import org.spongepowered.asm.mixin.Mixin; 15 | import org.spongepowered.asm.mixin.Overwrite; 16 | import org.spongepowered.asm.mixin.Shadow; 17 | import org.spongepowered.asm.mixin.injection.At; 18 | import org.spongepowered.asm.mixin.injection.Inject; 19 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 20 | import org.yatopiamc.c2me.common.config.C2MEConfig; 21 | import org.yatopiamc.c2me.common.threading.worldgen.ChunkStatusUtils; 22 | import org.yatopiamc.c2me.common.threading.worldgen.IChunkStatus; 23 | import org.yatopiamc.c2me.common.threading.worldgen.IWorldGenLockable; 24 | 25 | import java.util.List; 26 | import java.util.concurrent.CompletableFuture; 27 | import java.util.function.Function; 28 | import java.util.function.Supplier; 29 | 30 | @Mixin(ChunkStatus.class) 31 | public abstract class MixinChunkStatus implements IChunkStatus { 32 | 33 | @Shadow 34 | @Final 35 | private ChunkStatus.IGenerationWorker generationTask; 36 | 37 | private int reducedTaskRadius = -1; 38 | 39 | @Shadow @Final private int range; 40 | 41 | public void calculateReducedTaskRadius() { 42 | if (this.range == 0) { 43 | this.reducedTaskRadius = 0; 44 | } else { 45 | for (int i = 0; i <= this.range; i++) { 46 | final ChunkStatus status = ChunkStatus.getStatus(ChunkStatus.getDistance((ChunkStatus) (Object) this) + i); // TODO [VanillaCopy] from TACS getRequiredStatusForGeneration 47 | if (status == ChunkStatus.STRUCTURE_STARTS) { 48 | this.reducedTaskRadius = Math.min(this.range, i); 49 | break; 50 | } 51 | } 52 | } 53 | //noinspection ConstantConditions 54 | if ((Object) this == ChunkStatus.LIGHT) { 55 | this.reducedTaskRadius = 1; 56 | } 57 | System.out.println(String.format("%s task radius: %d -> %d", this, this.range, this.reducedTaskRadius)); 58 | } 59 | 60 | @Dynamic 61 | @Inject(method = "", at = @At("RETURN")) 62 | private static void onCLInit(CallbackInfo info) { 63 | for (ChunkStatus chunkStatus : Registry.CHUNK_STATUS) { 64 | ((IChunkStatus) chunkStatus).calculateReducedTaskRadius(); 65 | } 66 | } 67 | 68 | /** 69 | * @author ishland 70 | * @reason take over generation & improve chunk status transition speed 71 | */ 72 | @Overwrite 73 | public CompletableFuture> generate(ServerWorld world, ChunkGenerator chunkGenerator, TemplateManager structureManager, ServerWorldLightManager lightingProvider, Function>> function, List chunks) { 74 | final IChunk targetChunk = chunks.get(chunks.size() / 2); 75 | Supplier>> generationTask = () -> 76 | this.generationTask.doWork((ChunkStatus) (Object) this, world, chunkGenerator, structureManager, lightingProvider, function, chunks, targetChunk); 77 | if (targetChunk.getStatus().isOrAfter((ChunkStatus) (Object) this)) { 78 | return generationTask.get(); 79 | } else { 80 | int lockRadius = C2MEConfig.threadedWorldGenConfig.reduceLockRadius && this.reducedTaskRadius != -1 ? this.reducedTaskRadius : this.range; 81 | //noinspection ConstantConditions 82 | return ChunkStatusUtils.runChunkGenWithLock(targetChunk.getPos(), lockRadius, ((IWorldGenLockable) world).getWorldGenChunkLock(), () -> 83 | ChunkStatusUtils.getThreadingType((ChunkStatus) (Object) this).runTask(((IWorldGenLockable) world).getWorldGenSingleThreadedLock(), generationTask)); 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/worldgen/MixinServerWorld.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.worldgen; 2 | 3 | import com.ibm.asyncutil.locks.AsyncLock; 4 | import com.ibm.asyncutil.locks.AsyncNamedLock; 5 | import net.minecraft.util.math.ChunkPos; 6 | import net.minecraft.world.server.ServerWorld; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | import org.yatopiamc.c2me.common.threading.worldgen.IWorldGenLockable; 12 | 13 | @Mixin(ServerWorld.class) 14 | public class MixinServerWorld implements IWorldGenLockable { 15 | 16 | private volatile AsyncLock worldGenSingleThreadedLock = null; 17 | private volatile AsyncNamedLock worldGenChunkLock = null; 18 | 19 | @Inject(method = "", at = @At("RETURN")) 20 | private void initWorldGenSingleThreadedLock(CallbackInfo ci) { 21 | worldGenSingleThreadedLock = AsyncLock.createFair(); 22 | worldGenChunkLock = AsyncNamedLock.createFair(); 23 | } 24 | 25 | @Override 26 | public AsyncLock getWorldGenSingleThreadedLock() { 27 | return worldGenSingleThreadedLock; 28 | } 29 | 30 | @Override 31 | public AsyncNamedLock getWorldGenChunkLock() { 32 | return worldGenChunkLock; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/worldgen/MixinStructureManager.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.worldgen; 2 | 3 | import net.minecraft.util.ResourceLocation; 4 | import net.minecraft.world.gen.feature.template.Template; 5 | import net.minecraft.world.gen.feature.template.TemplateManager; 6 | import org.spongepowered.asm.mixin.Final; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.Mutable; 9 | import org.spongepowered.asm.mixin.Shadow; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Inject; 12 | import org.spongepowered.asm.mixin.injection.Redirect; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | 15 | import java.util.Map; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | import java.util.function.Function; 18 | 19 | @Mixin(TemplateManager.class) 20 | public class MixinStructureManager { 21 | 22 | @Mutable 23 | @Shadow 24 | @Final 25 | private Map structureRepository; 26 | 27 | @Inject(method = "", at = @At("TAIL")) 28 | private void onPostInit(CallbackInfo info) { 29 | this.structureRepository = new ConcurrentHashMap<>(); 30 | } 31 | 32 | @Redirect(method = "get", at = @At(value = "INVOKE", target = "Ljava/util/Map;computeIfAbsent(Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object;")) 33 | private V onGetStructureComputeIfAbsent(Map map, K key, Function mappingFunction) { 34 | if (map.containsKey(key)) return map.get(key); 35 | final V value = mappingFunction.apply(key); 36 | if (value == null) return null; 37 | map.put(key, value); 38 | return value; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/worldgen/MixinStructurePalettedBlockInfoList.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.worldgen; 2 | 3 | import net.minecraft.block.Block; 4 | import net.minecraft.world.gen.feature.template.Template; 5 | import org.spongepowered.asm.mixin.Final; 6 | import org.spongepowered.asm.mixin.Mixin; 7 | import org.spongepowered.asm.mixin.Mutable; 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 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | 17 | @Mixin(Template.Palette.class) 18 | public class MixinStructurePalettedBlockInfoList { 19 | 20 | @Mutable 21 | @Shadow @Final private Map> cache; 22 | 23 | @Inject(method = "", at = @At("RETURN")) 24 | private void onInit(CallbackInfo info) { 25 | this.cache = new ConcurrentHashMap<>(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/worldgen/MixinThreadedAnvilChunkStorage.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.worldgen; 2 | 3 | import com.mojang.datafixers.util.Either; 4 | import net.minecraft.util.concurrent.ThreadTaskExecutor; 5 | import net.minecraft.world.chunk.Chunk; 6 | import net.minecraft.world.chunk.ChunkStatus; 7 | import net.minecraft.world.chunk.IChunk; 8 | import net.minecraft.world.server.ChunkHolder; 9 | import net.minecraft.world.server.ChunkManager; 10 | import net.minecraft.world.server.ServerWorld; 11 | import org.spongepowered.asm.mixin.Dynamic; 12 | import org.spongepowered.asm.mixin.Final; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.Overwrite; 15 | import org.spongepowered.asm.mixin.Shadow; 16 | import org.spongepowered.asm.mixin.injection.At; 17 | import org.spongepowered.asm.mixin.injection.Inject; 18 | import org.spongepowered.asm.mixin.injection.Redirect; 19 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 20 | import org.yatopiamc.c2me.common.threading.GlobalExecutors; 21 | 22 | import java.util.concurrent.CompletableFuture; 23 | import java.util.concurrent.CompletionStage; 24 | import java.util.concurrent.Executor; 25 | import java.util.function.Function; 26 | 27 | @Mixin(ChunkManager.class) 28 | public class MixinThreadedAnvilChunkStorage { 29 | 30 | @Shadow @Final private ServerWorld level; 31 | @Shadow @Final private ThreadTaskExecutor mainThreadExecutor; 32 | 33 | private final Executor mainInvokingExecutor = runnable -> { 34 | if (this.level.getServer().isSameThread()) { 35 | runnable.run(); 36 | } else { 37 | this.mainThreadExecutor.execute(runnable); 38 | } 39 | }; 40 | 41 | private final ThreadLocal capturedRequiredStatus = new ThreadLocal<>(); 42 | 43 | @Inject(method = "scheduleChunkGeneration", at = @At("HEAD")) 44 | private void onUpgradeChunk(ChunkHolder holder, ChunkStatus requiredStatus, CallbackInfoReturnable>> cir) { 45 | capturedRequiredStatus.set(requiredStatus); 46 | } 47 | 48 | @Redirect(method = "getEntityTickingRangeFuture", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;")) 49 | private CompletableFuture redirectMainThreadExecutor1(CompletableFuture completableFuture, Function fn, Executor executor) { 50 | return completableFuture.thenApplyAsync(fn, this.mainInvokingExecutor); 51 | } 52 | 53 | @Redirect(method = "schedule", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenComposeAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;")) 54 | private CompletableFuture redirectMainThreadExecutor2(CompletableFuture completableFuture, Function> fn, Executor executor) { 55 | return completableFuture.thenComposeAsync(fn, this.mainInvokingExecutor); 56 | } 57 | 58 | /** 59 | * @author ishland 60 | * @reason move to scheduler & improve chunk status transition speed 61 | */ 62 | @SuppressWarnings("OverwriteTarget") 63 | @Dynamic 64 | @Overwrite(aliases = "func_219216_e_") 65 | private void lambda$scheduleChunkGeneration$21(ChunkHolder chunkHolder, Runnable runnable) { // synthetic method for worldGenExecutor scheduling in upgradeChunk 66 | final ChunkStatus capturedStatus = capturedRequiredStatus.get(); 67 | capturedRequiredStatus.remove(); 68 | if (capturedStatus != null) { 69 | final IChunk currentChunk = chunkHolder.getLastAvailable(); 70 | if (currentChunk != null && currentChunk.getStatus().isOrAfter(capturedStatus)) { 71 | this.mainInvokingExecutor.execute(runnable); 72 | return; 73 | } 74 | } 75 | GlobalExecutors.scheduler.execute(runnable); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/threading/worldgen/MixinWeightedBlockStateProvider.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.threading.worldgen; 2 | 3 | import net.minecraft.block.BlockState; 4 | import net.minecraft.util.WeightedList; 5 | import net.minecraft.util.math.BlockPos; 6 | import net.minecraft.world.gen.blockstateprovider.WeightedBlockStateProvider; 7 | import org.spongepowered.asm.mixin.Final; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.Overwrite; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | 12 | import java.util.Random; 13 | 14 | @Mixin(WeightedBlockStateProvider.class) 15 | public class MixinWeightedBlockStateProvider { 16 | 17 | @Shadow @Final private WeightedList weightedList; 18 | 19 | /** 20 | * @author ishland 21 | * @reason thread-safe getBlockState 22 | */ 23 | @Overwrite 24 | public BlockState getState(Random random, BlockPos pos) { 25 | return new WeightedList<>(weightedList.entries).getOne(random); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/util/accessor/IServerChunkProvider.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.util.accessor; 2 | 3 | import net.minecraft.world.server.ChunkHolder; 4 | import net.minecraft.world.server.ServerChunkProvider; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Invoker; 7 | 8 | @Mixin(ServerChunkProvider.class) 9 | public interface IServerChunkProvider { 10 | 11 | @Invoker("getVisibleChunkIfPresent") 12 | ChunkHolder IGetVisibleChunkIfPresent(long p_219219_1_); 13 | 14 | @Invoker("runDistanceManagerUpdates") 15 | boolean IRunDistanceManagerUpdates(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/util/accessor/IThreadTaskExecutor.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.util.accessor; 2 | 3 | import net.minecraft.util.concurrent.ThreadTaskExecutor; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.gen.Invoker; 6 | 7 | @Mixin(ThreadTaskExecutor.class) 8 | public interface IThreadTaskExecutor { 9 | 10 | @Invoker("pollTask") 11 | boolean IPollTask(); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/util/chunkgenerator/MixinCommandGenerate.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.util.chunkgenerator; 2 | 3 | import net.minecraft.command.CommandSource; 4 | import net.minecraft.util.math.BlockPos; 5 | import net.minecraft.world.server.ServerWorld; 6 | import net.minecraftforge.server.command.ChunkGenWorker; 7 | import org.spongepowered.asm.mixin.Dynamic; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.Redirect; 11 | import org.yatopiamc.c2me.common.threading.worldgen.C2MEChunkGenWorker; 12 | 13 | @Mixin(targets = "net.minecraftforge.server.command.CommandGenerate") 14 | public class MixinCommandGenerate { 15 | 16 | @Redirect(method = "execute", at = @At(value = "NEW", target = "net/minecraftforge/server/command/ChunkGenWorker"), remap = false) 17 | private static ChunkGenWorker redirectChunkGenWorkerInit(CommandSource listener, BlockPos start, int total, ServerWorld dim, int interval) { 18 | return new C2MEChunkGenWorker(listener, start, total, dim, interval); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/util/log4j2shutdownhookisnomore/MixinMain.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.util.log4j2shutdownhookisnomore; 2 | 3 | import net.minecraft.server.Main; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.core.impl.Log4jContextFactory; 6 | import org.apache.logging.log4j.core.util.DefaultShutdownCallbackRegistry; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Inject; 10 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 11 | 12 | @Mixin(Main.class) 13 | public class MixinMain { 14 | 15 | @Inject(method = "main", at = @At("HEAD")) 16 | private static void preMain(CallbackInfo info) { 17 | try { 18 | ((DefaultShutdownCallbackRegistry) ((Log4jContextFactory) LogManager.getFactory()).getShutdownCallbackRegistry()).stop(); 19 | } catch (Throwable t) { 20 | System.err.println("Unable to remove log4j2 shutdown hook"); 21 | t.printStackTrace(); 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/util/log4j2shutdownhookisnomore/MixinMinecraftDedicatedServer.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.util.log4j2shutdownhookisnomore; 2 | 3 | import net.minecraft.server.dedicated.DedicatedServer; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 9 | 10 | @Mixin(DedicatedServer.class) 11 | public class MixinMinecraftDedicatedServer { 12 | 13 | @Inject(method = "onServerExit", at = @At("RETURN")) 14 | private void onPostShutdown(CallbackInfo ci) { 15 | LogManager.shutdown(); 16 | new Thread(() -> System.exit(0)).start(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/yatopiamc/c2me/mixin/util/progresslogger/MixinWorldGenerationProgressLogger.java: -------------------------------------------------------------------------------- 1 | package org.yatopiamc.c2me.mixin.util.progresslogger; 2 | 3 | import net.minecraft.util.math.ChunkPos; 4 | import net.minecraft.util.math.MathHelper; 5 | import net.minecraft.world.chunk.ChunkStatus; 6 | import net.minecraft.world.chunk.listener.LoggingChunkStatusListener; 7 | import org.apache.logging.log4j.Logger; 8 | import org.spongepowered.asm.mixin.Final; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Overwrite; 11 | import org.spongepowered.asm.mixin.Shadow; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 15 | 16 | @Mixin(LoggingChunkStatusListener.class) 17 | public class MixinWorldGenerationProgressLogger { 18 | 19 | @Shadow 20 | @Final 21 | private static Logger LOGGER; 22 | @Shadow 23 | @Final 24 | private int maxCount; 25 | private volatile ChunkPos spawnPos = null; 26 | private volatile int radius = 0; 27 | private volatile int chunkStatusTransitions = 0; 28 | private volatile int chunkStatuses = 0; 29 | 30 | @Inject(method = "", at = @At("RETURN")) 31 | private void onInit(int radius, CallbackInfo info) { 32 | ChunkStatus status = ChunkStatus.FULL; 33 | this.radius = radius; 34 | chunkStatuses = 0; 35 | chunkStatusTransitions = 0; 36 | while ((status = status.getParent()) != ChunkStatus.EMPTY) 37 | chunkStatuses++; 38 | chunkStatuses++; 39 | } 40 | 41 | @Inject(method = "updateSpawnPos", at = @At("RETURN")) 42 | private void onStart(ChunkPos spawnPos, CallbackInfo ci) { 43 | this.spawnPos = spawnPos; 44 | } 45 | 46 | @Inject(method = "onStatusChange", at = @At("HEAD")) 47 | private void onSetChunkStatus(ChunkPos pos, ChunkStatus status, CallbackInfo ci) { 48 | if (status != null && (this.spawnPos == null || pos.getChessboardDistance(spawnPos) <= radius)) this.chunkStatusTransitions++; 49 | } 50 | 51 | /** 52 | * @author ishland 53 | * @reason replace impl 54 | */ 55 | @Overwrite 56 | public int getProgress() { 57 | // LOGGER.info("{} / {}", chunkStatusTransitions, maxCount * chunkStatuses); 58 | return MathHelper.floor((float) this.chunkStatusTransitions * 100.0F / (float) (this.maxCount * chunkStatuses)); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/accesstransformer.cfg: -------------------------------------------------------------------------------- 1 | public net.minecraft.world.chunk.ChunkStatus$IGenerationWorker 2 | public-f net.minecraft.world.server.ServerChunkProvider$ChunkExecutor 3 | public net.minecraft.world.chunk.storage.IOWorker$Entry 4 | 5 | public net.minecraft.world.chunk.storage.RegionFileCache (Ljava/io/File;Z)V 6 | public net.minecraft.world.chunk.storage.RegionFileCache func_219098_a(Lnet/minecraft/util/math/ChunkPos;)Lnet/minecraft/world/chunk/storage/RegionFile; 7 | public net.minecraft.world.chunk.ChunkStatus$IGenerationWorker doWork(Lnet/minecraft/world/chunk/ChunkStatus;Lnet/minecraft/world/server/ServerWorld;Lnet/minecraft/world/gen/ChunkGenerator;Lnet/minecraft/world/gen/feature/template/TemplateManager;Lnet/minecraft/world/server/ServerWorldLightManager;Ljava/util/function/Function;Ljava/util/List;Lnet/minecraft/world/chunk/IChunk;)Ljava/util/concurrent/CompletableFuture; 8 | public net.minecraft.world.SerializableTickList (Ljava/util/function/Function;Ljava/util/List;)V 9 | public net.minecraft.util.WeightedList (Ljava/util/List;)V 10 | 11 | public net.minecraft.world.server.ServerTickList field_205376_f 12 | public net.minecraft.world.server.ServerChunkProvider field_217243_i 13 | public net.minecraft.world.chunk.storage.RegionSectionCache field_227173_b_ 14 | public net.minecraft.world.chunk.storage.ChunkLoader field_227077_a_ 15 | public net.minecraft.util.WeightedList field_220658_a 16 | public net.minecraft.world.server.ServerWorld field_241102_C_ 17 | public net.minecraft.world.chunk.storage.IOWorker$Entry field_227113_a_ 18 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/mods.toml: -------------------------------------------------------------------------------- 1 | # This is an example mods.toml file. It contains the data relating to the loading mods. 2 | # There are several mandatory fields (#mandatory), and many more that are optional (#optional). 3 | # The overall format is standard TOML format, v0.5.0. 4 | # Note that there are a couple of TOML lists in this file. 5 | # Find more information on toml format here: https://github.com/toml-lang/toml 6 | # The name of the mod loader type to load - for regular FML @Mod mods it should be javafml 7 | modLoader="javafml" #mandatory 8 | # A version range to match for said mod loader - for regular FML @Mod it will be the forge version 9 | loaderVersion="[35,)" #mandatory This is typically bumped every Minecraft version by Forge. See our download page for lists of versions. 10 | # The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. 11 | # Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. 12 | license="MIT" 13 | # A URL to refer people to when problems occur with this mod 14 | #issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional 15 | # A list of mods - how many allowed here is determined by the individual mod loader 16 | [[mods]] #mandatory 17 | # The modid of the mod 18 | modId="c2me" #mandatory 19 | # The version number of the mod - there's a few well known ${} variables useable here or just hardcode it 20 | # ${file.jarVersion} will substitute the value of the Implementation-Version as read from the mod's JAR file metadata 21 | # see the associated build.gradle script for how to populate this completely automatically during a build 22 | version="${file.jarVersion}" #mandatory 23 | # A display name for the mod 24 | displayName="Concurrent Chunk Management Engine" #mandatory 25 | # A URL to query for updates for this mod. See the JSON update specification https://mcforge.readthedocs.io/en/latest/gettingstarted/autoupdate/ 26 | #updateJSONURL="https://change.me.example.invalid/updates.json" #optional 27 | # A URL for the "homepage" for this mod, displayed in the mod UI 28 | #displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional 29 | # A file name (in the root of the mod JAR) containing a logo for display 30 | logoFile="c2me.png" #optional 31 | # A text field displayed in the mod UI 32 | #credits="Thanks for this example mod goes to Java" #optional 33 | # A text field displayed in the mod UI 34 | authors="ishland" #optional 35 | # The description text for the mod (multi line!) (#mandatory) 36 | description=''' 37 | A Forge mod designed to improve the chunk performance of Minecraft. 38 | ''' 39 | # A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. 40 | [[dependencies.examplemod]] #optional 41 | # the modid of the dependency 42 | modId="forge" #mandatory 43 | # Does this dependency have to exist - if not, ordering below must be specified 44 | mandatory=true #mandatory 45 | # The version range of the dependency 46 | versionRange="[35,)" #mandatory 47 | # An ordering relationship for the dependency - BEFORE or AFTER required if the relationship is not mandatory 48 | ordering="NONE" 49 | # Side this dependency is applied on - BOTH, CLIENT or SERVER 50 | side="BOTH" 51 | # Here's another dependency 52 | [[dependencies.c2me]] 53 | modId="minecraft" 54 | mandatory=true 55 | # This version range declares a minimum of the current minecraft version up to but not including the next major version 56 | versionRange="[1.16.4,1.17)" 57 | ordering="NONE" 58 | side="BOTH" 59 | -------------------------------------------------------------------------------- /src/main/resources/c2me.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "org.yatopiamc.c2me.mixin", 5 | "compatibilityLevel": "JAVA_11", 6 | "mixins": [ 7 | "chunkscheduling.fix_unload.MixinThreadedAnvilChunkStorage", 8 | "optimizations.thread_local_biome_sampler.MixinOverworldBiomeProvider", 9 | "threading.chunkio.MixinChunkSerializer", 10 | "threading.chunkio.MixinChunkTickScheduler", 11 | "threading.chunkio.MixinScheduledTick", 12 | "threading.chunkio.MixinSerializingRegionBasedStorage", 13 | "threading.chunkio.MixinServerChunkManagerMainThreadExecutor", 14 | "threading.chunkio.MixinSimpleTickScheduler", 15 | "threading.chunkio.MixinStorageIoWorker", 16 | "threading.chunkio.MixinThreadedAnvilChunkStorage", 17 | "threading.chunkio.MixinVersionedChunkStorage", 18 | "threading.lighting.MixinServerLightingProvider", 19 | "threading.worldgen.MixinChunkStatus", 20 | "threading.worldgen.MixinServerWorld", 21 | "threading.worldgen.MixinStructureManager", 22 | "threading.worldgen.MixinStructurePalettedBlockInfoList", 23 | "threading.worldgen.MixinThreadedAnvilChunkStorage", 24 | "threading.worldgen.MixinWeightedBlockStateProvider", 25 | "util.accessor.IServerChunkProvider", 26 | "util.accessor.IThreadTaskExecutor", 27 | "util.chunkgenerator.MixinCommandGenerate", 28 | "util.progresslogger.MixinWorldGenerationProgressLogger" 29 | ], 30 | "client": [ 31 | ], 32 | "plugin": "org.yatopiamc.c2me.mixin.C2MEMixinPlugin", 33 | "injectors": { 34 | "defaultRequire": 1 35 | }, 36 | "refmap": "c2me.refmap.json", 37 | "setSourceFile": true 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/c2me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RelativityMC/C2ME-forge/3e9c6b49497f6c6bd492c57f9bfde5e16b8b803f/src/main/resources/c2me.png --------------------------------------------------------------------------------