├── .github └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── TECHNICAL_DETAILS.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── ca │ └── spottedleaf │ └── starlight │ ├── common │ ├── ScalableLuxEntrypoint.java │ ├── blockstate │ │ └── ExtendedAbstractBlockState.java │ ├── chunk │ │ └── ExtendedChunk.java │ ├── config │ │ └── Config.java │ ├── light │ │ ├── BlockStarLightEngine.java │ │ ├── SWMRNibbleArray.java │ │ ├── SkyStarLightEngine.java │ │ ├── StarLightEngine.java │ │ ├── StarLightInterface.java │ │ └── StarLightLightingProvider.java │ ├── thread │ │ ├── GlobalExecutors.java │ │ ├── LockTokenImpl.java │ │ ├── SchedulingUtil.java │ │ └── SimpleTask.java │ ├── util │ │ ├── CoordinateUtils.java │ │ ├── IntegerUtil.java │ │ ├── SaveUtil.java │ │ └── WorldUtil.java │ └── world │ │ ├── ExtendedSerializableChunkData.java │ │ └── ExtendedWorld.java │ └── mixin │ ├── client │ ├── multiplayer │ │ └── ClientPacketListenerMixin.java │ └── world │ │ └── ClientLevelMixin.java │ └── common │ ├── blockstate │ └── BlockStateBaseMixin.java │ ├── chunk │ ├── ChunkAccessMixin.java │ ├── EmptyLevelChunkMixin.java │ ├── ImposterProtoChunkMixin.java │ ├── LevelChunkMixin.java │ └── ProtoChunkMixin.java │ ├── lightengine │ ├── LevelLightEngineMixin.java │ └── ThreadedLevelLightEngineMixin.java │ └── world │ ├── LevelMixin.java │ ├── SerializableChunkDataMixin.java │ ├── ServerWorldMixin.java │ └── WorldGenRegionMixin.java └── resources ├── assets └── scalablelux │ └── icon.png ├── fabric.mod.json ├── scalablelux.accesswidener └── scalablelux.mixins.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Automatically build the project and run any configured tests for every push 2 | # and submitted pull request. This can help catch issues that only occur on 3 | # certain platforms or Java versions, and provides a first line of defence 4 | # against bad commits. 5 | 6 | name: build 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: checkout repository 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | submodules: true 18 | - name: validate gradle wrapper 19 | uses: gradle/actions/wrapper-validation@v3 20 | - name: setup jdk 21 21 | uses: actions/setup-java@v4 22 | with: 23 | java-version: 21 24 | distribution: 'microsoft' 25 | - name: build 26 | run: ./gradlew build 27 | - name: capture build artifacts 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: Artifacts 31 | path: build/libs/ -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "FlowSched"] 2 | path = FlowSched 3 | url = https://github.com/RelativityMC/FlowSched.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScalableLux 2 | A Fabric mod based on Starlight that improves the performance of light updates in Minecraft. 3 | 4 | ## Why does this fork exist? 5 | - Starlight is no longer maintained as a mod [since Mar 8, 2024](https://github.com/PaperMC/Starlight/commit/cca03d62da48e876ac79196bad16864e8a96bbeb). 6 | - The performance of vanilla lighting engine is still a bottleneck for high-performance chunk generation. 7 | - The base Starlight is still [100% faster than vanilla](), 8 | allowing the chunk system to scale beyond 24 threads. 9 | - Starlight's "stateless" design allows for parallel light updates, further widening the performance gap. 10 | It is still [rather important for dedicated servers with more players to stress chunk generation](https://gist.github.com/Spottedleaf/6cc1acdd03a9b7ac34699bf5e8f1b85c#is-starlight-obsolete). 11 | Therefore, it is still important for Fabric or other modded servers with plenty of players. 12 | 13 | ## What does this fork do? 14 | - Contains all the performance improvements from Starlight with additional bug fixes. 15 | - Optionally allows for parallel light updates, bringing significant performance improvement in high-speed 16 | world generation and heavy light updates scenarios. 17 | 18 | ## Building and setting up 19 | 20 | #### Initial setup 21 | Run the following commands in the root directory: 22 | 23 | ``` 24 | git submodule update --init 25 | ./build.sh up 26 | ./build.sh patch 27 | ``` 28 | 29 | #### Creating a patch 30 | See [CONTRIBUTING.md](CONTRIBUTING.md) for more detailed information. 31 | 32 | 33 | #### Compiling 34 | Use the command `./build.sh build`. Compiled jars will be placed under `Starlight-Patched/build/libs/`. 35 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'fabric-loom' version '1.7-SNAPSHOT' 3 | id 'maven-publish' 4 | id 'com.github.johnrengelman.shadow' version '8.1.1' 5 | } 6 | 7 | configurations { 8 | shadowInclude 9 | } 10 | 11 | loom.runs.all { 12 | // https://github.com/SpongePowered/Mixin/wiki/Mixin-Java-System-Properties 13 | vmArg("-Dmixin.debug=true") 14 | } 15 | 16 | /* 17 | * Gets the version name from the latest Git tag 18 | */ 19 | // https://stackoverflow.com/questions/28498688/gradle-script-to-autoversion-and-include-the-commit-hash-in-android 20 | def getGitCommit = { -> 21 | def stdout = new ByteArrayOutputStream() 22 | exec { 23 | commandLine 'git', 'rev-parse', '--short', 'HEAD' 24 | standardOutput = stdout 25 | // workingDir = ".." 26 | } 27 | return stdout.toString().trim() 28 | } 29 | 30 | archivesBaseName = project.archives_base_name 31 | version = project.mod_version + "+fabric." + getGitCommit() 32 | group = project.maven_group 33 | 34 | dependencies { 35 | //to change the versions see the gradle.properties file 36 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 37 | //mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 38 | mappings loom.officialMojangMappings() 39 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 40 | 41 | // Fabric API. This is technically optional, but you probably want it anyway. 42 | //modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 43 | 44 | // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. 45 | // You may need to force-disable transitiveness on them. 46 | 47 | shadowInclude("com.ishland.flowsched:flowsched") { 48 | transitive false 49 | } 50 | implementation("com.ishland.flowsched:flowsched") 51 | } 52 | 53 | processResources { 54 | inputs.property "version", project.version 55 | 56 | filesMatching("fabric.mod.json") { 57 | expand "version": project.version, "minecraft_version": minecraft_version, "loader_version": loader_version, "mod_version": mod_version 58 | } 59 | } 60 | 61 | // ensure that the encoding is set to UTF-8, no matter what the system default is 62 | // this fixes some edge cases with special characters not displaying correctly 63 | // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html 64 | tasks.withType(JavaCompile) { 65 | options.encoding = "UTF-8" 66 | } 67 | 68 | loom { 69 | accessWidenerPath = file("src/main/resources/scalablelux.accesswidener") 70 | } 71 | 72 | // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task 73 | // if it is present. 74 | // If you remove this task, sources will not be generated. 75 | java { 76 | // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task 77 | // if it is present. 78 | // If you remove this line, sources will not be generated. 79 | withSourcesJar() 80 | 81 | sourceCompatibility = JavaVersion.VERSION_17 82 | targetCompatibility = JavaVersion.VERSION_17 83 | } 84 | jar { 85 | from "LICENSE" 86 | } 87 | 88 | shadowJar { 89 | archiveClassifier = "all-dev" 90 | configurations = [ project.configurations.shadowInclude ] 91 | 92 | relocate "com.ishland.flowsched", "ca.spottedleaf.starlight.interndep.flowsched" 93 | } 94 | 95 | remapJar { 96 | input = shadowJar.archiveFile 97 | archiveFileName = shadowJar.archiveFileName.get().replaceAll("-dev\\.jar\$", ".jar") 98 | addNestedDependencies = true 99 | dependsOn shadowJar 100 | } 101 | 102 | clean.dependsOn gradle.includedBuild('FlowSched').task(':clean') 103 | 104 | // make build reproducible 105 | tasks.withType(AbstractArchiveTask) { 106 | preserveFileTimestamps = false 107 | reproducibleFileOrder = true 108 | } 109 | 110 | // configure the maven publication 111 | publishing { 112 | publications { 113 | mavenJava(MavenPublication) { 114 | // add all the jars that should be included when publishing to maven 115 | artifact(remapJar) { 116 | builtBy remapJar 117 | } 118 | artifact(sourcesJar) { 119 | builtBy remapSourcesJar 120 | } 121 | } 122 | } 123 | 124 | // select the repositories you want to publish to 125 | repositories { 126 | // uncomment to publish to the local maven 127 | // mavenLocal() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx2G 3 | org.gradle.daemon=false 4 | # Fabric Properties 5 | # check these on https://modmuss50.me/fabric.html 6 | minecraft_version=1.21.4 7 | loader_version=0.16.9 8 | # Mod Properties 9 | mod_version=0.1.2+beta.1 10 | maven_group=ca.spottedleaf.starlight 11 | archives_base_name=ScalableLux 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RelativityMC/ScalableLux/c6ec02104ec2104fbb3c99f9782641aa3b5c6515/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /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% equ 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% equ 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 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | } 11 | 12 | rootProject.name = "ScalableLux" 13 | 14 | includeBuild('FlowSched') { 15 | dependencySubstitution { 16 | substitute module('com.ishland.flowsched:flowsched') using project(':') 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/ScalableLuxEntrypoint.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common; 2 | 3 | import ca.spottedleaf.starlight.common.config.Config; 4 | import net.fabricmc.api.ModInitializer; 5 | 6 | public class ScalableLuxEntrypoint implements ModInitializer { 7 | @Override 8 | public void onInitialize() { 9 | Config.init(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/blockstate/ExtendedAbstractBlockState.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.blockstate; 2 | 3 | public interface ExtendedAbstractBlockState { 4 | 5 | public boolean isConditionallyFullOpaque(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/chunk/ExtendedChunk.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.chunk; 2 | 3 | import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; 4 | 5 | public interface ExtendedChunk { 6 | 7 | public SWMRNibbleArray[] getBlockNibbles(); 8 | public void setBlockNibbles(final SWMRNibbleArray[] nibbles); 9 | 10 | public SWMRNibbleArray[] getSkyNibbles(); 11 | public void setSkyNibbles(final SWMRNibbleArray[] nibbles); 12 | 13 | public boolean[] getSkyEmptinessMap(); 14 | public void setSkyEmptinessMap(final boolean[] emptinessMap); 15 | 16 | public boolean[] getBlockEmptinessMap(); 17 | public void setBlockEmptinessMap(final boolean[] emptinessMap); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/config/Config.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.config; 2 | 3 | import ca.spottedleaf.starlight.common.thread.SchedulingUtil; 4 | import net.fabricmc.loader.api.FabricLoader; 5 | import net.fabricmc.loader.api.ModContainer; 6 | import net.fabricmc.loader.api.metadata.CustomValue; 7 | import org.apache.logging.log4j.LogManager; 8 | import org.apache.logging.log4j.Logger; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.StandardOpenOption; 16 | import java.util.Properties; 17 | 18 | public class Config { 19 | private static final Logger LOGGER = LogManager.getLogger(); 20 | 21 | public static final int PARALLELISM; 22 | public static final boolean USE_STARLIGHT_FORMAT; 23 | 24 | static { 25 | final Properties properties = new Properties(); 26 | final Properties newProperties = new Properties(); 27 | final Path path = FabricLoader.getInstance().getConfigDir().resolve("scalablelux.properties"); 28 | if (Files.isRegularFile(path)) { 29 | try (InputStream in = Files.newInputStream(path, StandardOpenOption.CREATE)) { 30 | properties.load(in); 31 | } catch (IOException e) { 32 | throw new RuntimeException(e); 33 | } 34 | } 35 | 36 | if (!SchedulingUtil.isExternallyManaged()) { 37 | int parallelism = getInt(properties, newProperties, "parallelism", -1); 38 | if (parallelism < 1) { 39 | parallelism = Math.max(1, Runtime.getRuntime().availableProcessors() / 3); 40 | } 41 | PARALLELISM = parallelism; 42 | } else { 43 | PARALLELISM = Math.max(1, Runtime.getRuntime().availableProcessors() / 3); 44 | } 45 | USE_STARLIGHT_FORMAT = getBoolean(properties, newProperties, "use_starlight_format", false); 46 | 47 | if (!newProperties.isEmpty()) { 48 | try (OutputStream out = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { 49 | newProperties.store(out, "Configuration file for ScalableLux"); 50 | } catch (IOException e) { 51 | throw new RuntimeException(e); 52 | } 53 | } 54 | } 55 | 56 | public static void init() { 57 | } 58 | 59 | private static int getInt(Properties properties, Properties newProperties, String key, int def) { 60 | try { 61 | final int i = Integer.parseInt(properties.getProperty(key)); 62 | newProperties.setProperty(key, String.valueOf(i)); 63 | return i; 64 | } catch (NumberFormatException e) { 65 | newProperties.setProperty(key, String.valueOf(def)); 66 | return def; 67 | } 68 | } 69 | 70 | private static boolean getBoolean(Properties properties, Properties newProperties, String key, boolean def) { 71 | boolean boolean0 = getBoolean0(properties, newProperties, key, def); 72 | for (ModContainer modContainer : FabricLoader.getInstance().getAllMods()) { 73 | final CustomValue incompatibilitiesValue = modContainer.getMetadata().getCustomValue("scalablelux:incompatibleConfig"); 74 | if (incompatibilitiesValue != null && incompatibilitiesValue.getType() == CustomValue.CvType.ARRAY) { 75 | final CustomValue.CvArray incompatibilities = incompatibilitiesValue.getAsArray(); 76 | for (CustomValue value : incompatibilities) { 77 | if (value.getType() == CustomValue.CvType.STRING && value.getAsString().equals(key)) { 78 | final String message; 79 | if (Boolean.getBoolean("scalablelux.ignoreIncompatibleConfig")) { 80 | message = String.format("Ignoring incompatibility of %s (defined in %s@%s)", 81 | key, modContainer.getMetadata().getId(), modContainer.getMetadata().getVersion().getFriendlyString()); 82 | } else { 83 | message = String.format("Forcing %s in scalablelux.properties to be disabled (defined in %s@%s)", 84 | key, modContainer.getMetadata().getId(), modContainer.getMetadata().getVersion().getFriendlyString()); 85 | boolean0 = false; 86 | } 87 | LOGGER.warn(message); 88 | } 89 | } 90 | } 91 | } 92 | return boolean0; 93 | } 94 | 95 | private static boolean getBoolean0(Properties properties, Properties newProperties, String key, boolean def) { 96 | try { 97 | final boolean b = parseBoolean(properties.getProperty(key)); 98 | newProperties.setProperty(key, String.valueOf(b)); 99 | return b; 100 | } catch (NumberFormatException e) { 101 | newProperties.setProperty(key, String.valueOf(def)); 102 | return def; 103 | } 104 | } 105 | 106 | private static boolean parseBoolean(String string) { 107 | if (string == null) throw new NumberFormatException("null"); 108 | if (string.trim().equalsIgnoreCase("true")) return true; 109 | if (string.trim().equalsIgnoreCase("false")) return false; 110 | throw new NumberFormatException(string); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/light/BlockStarLightEngine.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.light; 2 | 3 | import ca.spottedleaf.starlight.common.blockstate.ExtendedAbstractBlockState; 4 | import ca.spottedleaf.starlight.common.chunk.ExtendedChunk; 5 | import net.minecraft.core.BlockPos; 6 | import net.minecraft.world.level.Level; 7 | import net.minecraft.world.level.block.state.BlockState; 8 | import net.minecraft.world.level.chunk.ChunkAccess; 9 | import net.minecraft.world.level.chunk.status.ChunkStatus; 10 | import net.minecraft.world.level.chunk.ImposterProtoChunk; 11 | import net.minecraft.world.level.chunk.LevelChunk; 12 | import net.minecraft.world.level.chunk.LevelChunkSection; 13 | import net.minecraft.world.level.chunk.LightChunkGetter; 14 | import net.minecraft.world.level.chunk.PalettedContainer; 15 | import net.minecraft.world.phys.shapes.Shapes; 16 | import net.minecraft.world.phys.shapes.VoxelShape; 17 | import java.util.ArrayList; 18 | import java.util.Iterator; 19 | import java.util.List; 20 | import java.util.Set; 21 | import java.util.stream.Collectors; 22 | 23 | public final class BlockStarLightEngine extends StarLightEngine { 24 | 25 | public BlockStarLightEngine(final Level world) { 26 | super(false, world); 27 | } 28 | 29 | @Override 30 | protected boolean[] getEmptinessMap(final ChunkAccess chunk) { 31 | return ((ExtendedChunk)chunk).getBlockEmptinessMap(); 32 | } 33 | 34 | @Override 35 | protected void setEmptinessMap(final ChunkAccess chunk, final boolean[] to) { 36 | ((ExtendedChunk)chunk).setBlockEmptinessMap(to); 37 | } 38 | 39 | @Override 40 | protected SWMRNibbleArray[] getNibblesOnChunk(final ChunkAccess chunk) { 41 | return ((ExtendedChunk)chunk).getBlockNibbles(); 42 | } 43 | 44 | @Override 45 | protected void setNibbles(final ChunkAccess chunk, final SWMRNibbleArray[] to) { 46 | ((ExtendedChunk)chunk).setBlockNibbles(to); 47 | } 48 | 49 | @Override 50 | protected boolean canUseChunk(final ChunkAccess chunk) { 51 | return chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT) && (this.isClientSide || chunk.isLightCorrect()); 52 | } 53 | 54 | @Override 55 | protected void setNibbleNull(final int chunkX, final int chunkY, final int chunkZ) { 56 | final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); 57 | if (nibble != null) { 58 | // de-initialisation is not as straightforward as with sky data, since deinit of block light is typically 59 | // because a block was removed - which can decrease light. with sky data, block breaking can only result 60 | // in increases, and thus the existing sky block check will actually correctly propagate light through 61 | // a null section. so in order to propagate decreases correctly, we can do a couple of things: not remove 62 | // the data section, or do edge checks on ALL axis (x, y, z). however I do not want edge checks running 63 | // for clients at all, as they are expensive. so we don't remove the section, but to maintain the appearence 64 | // of vanilla data management we "hide" them. 65 | nibble.setHidden(); 66 | } 67 | } 68 | 69 | @Override 70 | protected void initNibble(final int chunkX, final int chunkY, final int chunkZ, final boolean extrude, final boolean initRemovedNibbles) { 71 | if (chunkY < this.minLightSection || chunkY > this.maxLightSection || this.getChunkInCache(chunkX, chunkZ) == null) { 72 | return; 73 | } 74 | 75 | final SWMRNibbleArray nibble = this.getNibbleFromCache(chunkX, chunkY, chunkZ); 76 | if (nibble == null) { 77 | if (!initRemovedNibbles) { 78 | throw new IllegalStateException(); 79 | } else { 80 | this.setNibbleInCache(chunkX, chunkY, chunkZ, new SWMRNibbleArray()); 81 | } 82 | } else { 83 | nibble.setNonNull(); 84 | } 85 | } 86 | 87 | @Override 88 | protected final void checkBlock(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ) { 89 | // blocks can change opacity 90 | // blocks can change emitted light 91 | // blocks can change direction of propagation 92 | 93 | final int encodeOffset = this.coordinateOffset; 94 | final int emittedMask = this.emittedLightMask; 95 | 96 | final int currentLevel = this.getLightLevel(worldX, worldY, worldZ); 97 | final BlockState blockState = this.getBlockState(worldX, worldY, worldZ); 98 | final int emittedLevel = blockState.getLightEmission() & emittedMask; 99 | 100 | this.setLightLevel(worldX, worldY, worldZ, emittedLevel); 101 | // this accounts for change in emitted light that would cause an increase 102 | if (emittedLevel != 0) { 103 | this.appendToIncreaseQueue( 104 | ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) 105 | | (emittedLevel & 0xFL) << (6 + 6 + 16) 106 | | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) 107 | | (((ExtendedAbstractBlockState)blockState).isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) 108 | ); 109 | } 110 | // this also accounts for a change in emitted light that would cause a decrease 111 | // this also accounts for the change of direction of propagation (i.e old block was full transparent, new block is full opaque or vice versa) 112 | // as it checks all neighbours (even if current level is 0) 113 | this.appendToDecreaseQueue( 114 | ((worldX + (worldZ << 6) + (worldY << (6 + 6)) + encodeOffset) & ((1L << (6 + 6 + 16)) - 1)) 115 | | (currentLevel & 0xFL) << (6 + 6 + 16) 116 | | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) 117 | // always keep sided transparent false here, new block might be conditionally transparent which would 118 | // prevent us from decreasing sources in the directions where the new block is opaque 119 | // if it turns out we were wrong to de-propagate the source, the re-propagate logic WILL always 120 | // catch that and fix it. 121 | ); 122 | // re-propagating neighbours (done by the decrease queue) will also account for opacity changes in this block 123 | } 124 | 125 | protected final BlockPos.MutableBlockPos recalcCenterPos = new BlockPos.MutableBlockPos(); 126 | protected final BlockPos.MutableBlockPos recalcNeighbourPos = new BlockPos.MutableBlockPos(); 127 | 128 | @Override 129 | protected int calculateLightValue(final LightChunkGetter lightAccess, final int worldX, final int worldY, final int worldZ, 130 | final int expect) { 131 | final BlockState centerState = this.getBlockState(worldX, worldY, worldZ); 132 | int level = centerState.getLightEmission() & 0xF; 133 | 134 | if (level >= (15 - 1) || level > expect) { 135 | return level; 136 | } 137 | 138 | final int sectionOffset = this.chunkSectionIndexOffset; 139 | final BlockState conditionallyOpaqueState; 140 | int opacity = Math.max(1, centerState.getLightBlock()); 141 | if (opacity >= 15) { 142 | return level; // TODO in older starlight, opacity >= 15 can go into the loop below, but it probably shouldn't 143 | } 144 | if (((ExtendedAbstractBlockState)centerState).isConditionallyFullOpaque()) { 145 | conditionallyOpaqueState = centerState; 146 | } else { 147 | conditionallyOpaqueState = null; 148 | } 149 | 150 | for (final AxisDirection direction : AXIS_DIRECTIONS) { 151 | final int offX = worldX + direction.x; 152 | final int offY = worldY + direction.y; 153 | final int offZ = worldZ + direction.z; 154 | 155 | final int sectionIndex = (offX >> 4) + 5 * (offZ >> 4) + (5 * 5) * (offY >> 4) + sectionOffset; 156 | 157 | final int neighbourLevel = this.getLightLevel(sectionIndex, (offX & 15) | ((offZ & 15) << 4) | ((offY & 15) << 8)); 158 | 159 | if ((neighbourLevel - 1) <= level) { 160 | // don't need to test transparency, we know it wont affect the result. 161 | continue; 162 | } 163 | 164 | final BlockState neighbourState = this.getBlockState(offX, offY, offZ); 165 | if (((ExtendedAbstractBlockState)neighbourState).isConditionallyFullOpaque()) { 166 | // here the block can be conditionally opaque (i.e light cannot propagate from it), so we need to test that 167 | // we don't read the blockstate because most of the time this is false, so using the faster 168 | // known transparency lookup results in a net win 169 | this.recalcNeighbourPos.set(offX, offY, offZ); 170 | final VoxelShape neighbourFace = neighbourState.getFaceOcclusionShape(direction.opposite.nms); 171 | final VoxelShape thisFace = conditionallyOpaqueState == null ? Shapes.empty() : conditionallyOpaqueState.getFaceOcclusionShape(direction.nms); 172 | if (Shapes.faceShapeOccludes(thisFace, neighbourFace)) { 173 | // not allowed to propagate 174 | continue; 175 | } 176 | } 177 | 178 | // passed transparency, 179 | 180 | final int calculated = neighbourLevel - opacity; 181 | level = Math.max(calculated, level); 182 | if (level > expect) { 183 | return level; 184 | } 185 | } 186 | 187 | return level; 188 | } 189 | 190 | @Override 191 | protected void propagateBlockChanges(final LightChunkGetter lightAccess, final ChunkAccess atChunk, final Set positions) { 192 | for (final BlockPos pos : positions) { 193 | this.checkBlock(lightAccess, pos.getX(), pos.getY(), pos.getZ()); 194 | } 195 | 196 | this.performLightDecrease(lightAccess); 197 | } 198 | 199 | protected List getSources(final LightChunkGetter lightAccess, final ChunkAccess chunk) { 200 | final List sources = new ArrayList<>(); 201 | 202 | final int offX = chunk.getPos().x << 4; 203 | final int offZ = chunk.getPos().z << 4; 204 | 205 | final LevelChunkSection[] sections = chunk.getSections(); 206 | for (int sectionY = this.minSection; sectionY <= this.maxSection; ++sectionY) { 207 | final LevelChunkSection section = sections[sectionY - this.minSection]; 208 | if (section == null || section.hasOnlyAir()) { 209 | // no sources in empty sections 210 | continue; 211 | } 212 | if (!section.maybeHas((final BlockState state) -> { 213 | return state.getLightEmission() > 0; 214 | })) { 215 | // no light sources in palette 216 | continue; 217 | } 218 | final PalettedContainer states = section.states; 219 | final int offY = sectionY << 4; 220 | 221 | for (int index = 0; index < (16 * 16 * 16); ++index) { 222 | final BlockState state = states.get(index); 223 | if (state.getLightEmission() <= 0) { 224 | continue; 225 | } 226 | 227 | // index = x | (z << 4) | (y << 8) 228 | sources.add(new BlockPos(offX | (index & 15), offY | (index >>> 8), offZ | ((index >>> 4) & 15))); 229 | } 230 | } 231 | 232 | return sources; 233 | } 234 | 235 | @Override 236 | public void lightChunk(final LightChunkGetter lightAccess, final ChunkAccess chunk, final boolean needsEdgeChecks) { 237 | // setup sources 238 | final int emittedMask = this.emittedLightMask; 239 | final List positions = this.getSources(lightAccess, chunk); 240 | for (int i = 0, len = positions.size(); i < len; ++i) { 241 | final BlockPos pos = positions.get(i); 242 | final BlockState blockState = this.getBlockState(pos.getX(), pos.getY(), pos.getZ()); 243 | final int emittedLight = blockState.getLightEmission() & emittedMask; 244 | 245 | if (emittedLight <= this.getLightLevel(pos.getX(), pos.getY(), pos.getZ())) { 246 | // some other source is brighter 247 | continue; 248 | } 249 | 250 | this.appendToIncreaseQueue( 251 | ((pos.getX() + (pos.getZ() << 6) + (pos.getY() << (6 + 6)) + this.coordinateOffset) & ((1L << (6 + 6 + 16)) - 1)) 252 | | (emittedLight & 0xFL) << (6 + 6 + 16) 253 | | (((long)ALL_DIRECTIONS_BITSET) << (6 + 6 + 16 + 4)) 254 | | (((ExtendedAbstractBlockState)blockState).isConditionallyFullOpaque() ? FLAG_HAS_SIDED_TRANSPARENT_BLOCKS : 0) 255 | ); 256 | 257 | 258 | // propagation wont set this for us 259 | this.setLightLevel(pos.getX(), pos.getY(), pos.getZ(), emittedLight); 260 | } 261 | 262 | if (needsEdgeChecks) { 263 | // not required to propagate here, but this will reduce the hit of the edge checks 264 | this.performLightIncrease(lightAccess); 265 | 266 | // verify neighbour edges 267 | this.checkChunkEdges(lightAccess, chunk, this.minLightSection, this.maxLightSection); 268 | } else { 269 | this.propagateNeighbourLevels(lightAccess, chunk, this.minLightSection, this.maxLightSection); 270 | 271 | this.performLightIncrease(lightAccess); 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/light/SWMRNibbleArray.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.light; 2 | 3 | import net.minecraft.world.level.chunk.DataLayer; 4 | import java.util.ArrayDeque; 5 | import java.util.Arrays; 6 | 7 | // SWMR -> Single Writer Multi Reader Nibble Array 8 | public final class SWMRNibbleArray { 9 | 10 | /* 11 | * Null nibble - nibble does not exist, and should not be written to. Just like vanilla - null 12 | * nibbles are always 0 - and they are never written to directly. Only initialised/uninitialised 13 | * nibbles can be written to. 14 | * 15 | * Uninitialised nibble - They are all 0, but the backing array isn't initialised. 16 | * 17 | * Initialised nibble - Has light data. 18 | */ 19 | 20 | protected static final int INIT_STATE_NULL = 0; // null 21 | protected static final int INIT_STATE_UNINIT = 1; // uninitialised 22 | protected static final int INIT_STATE_INIT = 2; // initialised 23 | protected static final int INIT_STATE_HIDDEN = 3; // initialised, but conversion to Vanilla data should be treated as if NULL 24 | 25 | public static final int ARRAY_SIZE = 16 * 16 * 16 / (8/4); // blocks / bytes per block 26 | // this allows us to maintain only 1 byte array when we're not updating 27 | static final ThreadLocal> WORKING_BYTES_POOL = ThreadLocal.withInitial(ArrayDeque::new); 28 | 29 | private static byte[] allocateBytes() { 30 | final byte[] inPool = WORKING_BYTES_POOL.get().pollFirst(); 31 | if (inPool != null) { 32 | return inPool; 33 | } 34 | 35 | return new byte[ARRAY_SIZE]; 36 | } 37 | 38 | private static void freeBytes(final byte[] bytes) { 39 | WORKING_BYTES_POOL.get().addFirst(bytes); 40 | } 41 | 42 | public static SWMRNibbleArray fromVanilla(final DataLayer nibble) { 43 | if (nibble == null) { 44 | return new SWMRNibbleArray(null, true); 45 | } else if (nibble.isEmpty()) { 46 | return new SWMRNibbleArray(); 47 | } else { 48 | return new SWMRNibbleArray(nibble.getData().clone()); // make sure we don't write to the parameter later 49 | } 50 | } 51 | 52 | protected int stateUpdating; 53 | protected volatile int stateVisible; 54 | 55 | protected byte[] storageUpdating; 56 | protected boolean updatingDirty; // only returns whether storageUpdating is dirty 57 | protected volatile byte[] storageVisible; 58 | 59 | public SWMRNibbleArray() { 60 | this(null, false); // lazy init 61 | } 62 | 63 | public SWMRNibbleArray(final byte[] bytes) { 64 | this(bytes, false); 65 | } 66 | 67 | public SWMRNibbleArray(final byte[] bytes, final boolean isNullNibble) { 68 | if (bytes != null && bytes.length != ARRAY_SIZE) { 69 | throw new IllegalArgumentException("Data of wrong length: " + bytes.length); 70 | } 71 | this.stateVisible = this.stateUpdating = bytes == null ? (isNullNibble ? INIT_STATE_NULL : INIT_STATE_UNINIT) : INIT_STATE_INIT; 72 | this.storageUpdating = this.storageVisible = bytes; 73 | } 74 | 75 | public SWMRNibbleArray(final byte[] bytes, final int state) { 76 | if (bytes != null && bytes.length != ARRAY_SIZE) { 77 | throw new IllegalArgumentException("Data of wrong length: " + bytes.length); 78 | } 79 | if (bytes == null && (state == INIT_STATE_INIT || state == INIT_STATE_HIDDEN)) { 80 | throw new IllegalArgumentException("Data cannot be null and have state be initialised"); 81 | } 82 | this.stateUpdating = this.stateVisible = state; 83 | this.storageUpdating = this.storageVisible = bytes; 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | StringBuilder stringBuilder = new StringBuilder(); 89 | stringBuilder.append("State: "); 90 | switch (this.stateVisible) { 91 | case INIT_STATE_NULL: 92 | stringBuilder.append("null"); 93 | break; 94 | case INIT_STATE_UNINIT: 95 | stringBuilder.append("uninitialised"); 96 | break; 97 | case INIT_STATE_INIT: 98 | stringBuilder.append("initialised"); 99 | break; 100 | case INIT_STATE_HIDDEN: 101 | stringBuilder.append("hidden"); 102 | break; 103 | default: 104 | stringBuilder.append("unknown"); 105 | break; 106 | } 107 | stringBuilder.append("\nData:\n"); 108 | 109 | final byte[] data = this.storageVisible; 110 | if (data != null) { 111 | for (int i = 0; i < 4096; ++i) { 112 | // Copied from NibbleArray#toString 113 | final int level = ((data[i >>> 1] >>> ((i & 1) << 2)) & 0xF); 114 | 115 | stringBuilder.append(Integer.toHexString(level)); 116 | if ((i & 15) == 15) { 117 | stringBuilder.append("\n"); 118 | } 119 | 120 | if ((i & 255) == 255) { 121 | stringBuilder.append("\n"); 122 | } 123 | } 124 | } else { 125 | stringBuilder.append("null"); 126 | } 127 | 128 | return stringBuilder.toString(); 129 | } 130 | 131 | public SaveState getSaveState() { 132 | synchronized (this) { 133 | final int state = this.stateVisible; 134 | final byte[] data = this.storageVisible; 135 | if (state == INIT_STATE_NULL) { 136 | return null; 137 | } 138 | if (state == INIT_STATE_UNINIT) { 139 | return new SaveState(null, state); 140 | } 141 | final boolean zero = isAllZero(data); 142 | if (zero) { 143 | return state == INIT_STATE_INIT ? new SaveState(null, INIT_STATE_UNINIT) : null; 144 | } else { 145 | return new SaveState(data.clone(), state); 146 | } 147 | } 148 | } 149 | 150 | protected static boolean isAllZero(final byte[] data) { 151 | for (int i = 0; i < (ARRAY_SIZE >>> 4); ++i) { 152 | byte whole = data[i << 4]; 153 | 154 | for (int k = 1; k < (1 << 4); ++k) { 155 | whole |= data[(i << 4) | k]; 156 | } 157 | 158 | if (whole != 0) { 159 | return false; 160 | } 161 | } 162 | 163 | return true; 164 | } 165 | 166 | // operation type: updating on src, updating on other 167 | public void extrudeLower(final SWMRNibbleArray other) { 168 | if (other.stateUpdating == INIT_STATE_NULL) { 169 | throw new IllegalArgumentException(); 170 | } 171 | 172 | if (other.storageUpdating == null) { 173 | this.setUninitialised(); 174 | return; 175 | } 176 | 177 | final byte[] src = other.storageUpdating; 178 | final byte[] into; 179 | 180 | if (!this.updatingDirty) { 181 | if (this.storageUpdating != null) { 182 | into = this.storageUpdating = allocateBytes(); 183 | } else { 184 | this.storageUpdating = into = allocateBytes(); 185 | this.stateUpdating = INIT_STATE_INIT; 186 | } 187 | this.updatingDirty = true; 188 | } else { 189 | into = this.storageUpdating; 190 | } 191 | 192 | final int start = 0; 193 | final int end = (15 | (15 << 4)) >>> 1; 194 | 195 | /* x | (z << 4) | (y << 8) */ 196 | for (int y = 0; y <= 15; ++y) { 197 | System.arraycopy(src, start, into, y << (8 - 1), end - start + 1); 198 | } 199 | } 200 | 201 | // operation type: updating 202 | public void setFull() { 203 | if (this.stateUpdating != INIT_STATE_HIDDEN) { 204 | this.stateUpdating = INIT_STATE_INIT; 205 | } 206 | Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)-1); 207 | this.updatingDirty = true; 208 | } 209 | 210 | // operation type: updating 211 | public void setZero() { 212 | if (this.stateUpdating != INIT_STATE_HIDDEN) { 213 | this.stateUpdating = INIT_STATE_INIT; 214 | } 215 | Arrays.fill(this.storageUpdating == null || !this.updatingDirty ? this.storageUpdating = allocateBytes() : this.storageUpdating, (byte)0); 216 | this.updatingDirty = true; 217 | } 218 | 219 | // operation type: updating 220 | public void setNonNull() { 221 | if (this.stateUpdating == INIT_STATE_HIDDEN) { 222 | this.stateUpdating = INIT_STATE_INIT; 223 | return; 224 | } 225 | if (this.stateUpdating != INIT_STATE_NULL) { 226 | return; 227 | } 228 | this.stateUpdating = INIT_STATE_UNINIT; 229 | } 230 | 231 | // operation type: updating 232 | public void setNull() { 233 | this.stateUpdating = INIT_STATE_NULL; 234 | if (this.updatingDirty && this.storageUpdating != null) { 235 | freeBytes(this.storageUpdating); 236 | } 237 | this.storageUpdating = null; 238 | this.updatingDirty = false; 239 | } 240 | 241 | // operation type: updating 242 | public void setUninitialised() { 243 | this.stateUpdating = INIT_STATE_UNINIT; 244 | if (this.storageUpdating != null && this.updatingDirty) { 245 | freeBytes(this.storageUpdating); 246 | } 247 | this.storageUpdating = null; 248 | this.updatingDirty = false; 249 | } 250 | 251 | // operation type: updating 252 | public void setHidden() { 253 | if (this.stateUpdating == INIT_STATE_HIDDEN) { 254 | return; 255 | } 256 | if (this.stateUpdating != INIT_STATE_INIT) { 257 | this.setNull(); 258 | } else { 259 | this.stateUpdating = INIT_STATE_HIDDEN; 260 | } 261 | } 262 | 263 | // operation type: updating 264 | public boolean isDirty() { 265 | return this.stateUpdating != this.stateVisible || this.updatingDirty; 266 | } 267 | 268 | // operation type: updating 269 | public boolean isNullNibbleUpdating() { 270 | return this.stateUpdating == INIT_STATE_NULL; 271 | } 272 | 273 | // operation type: visible 274 | public boolean isNullNibbleVisible() { 275 | return this.stateVisible == INIT_STATE_NULL; 276 | } 277 | 278 | // opeartion type: updating 279 | public boolean isUninitialisedUpdating() { 280 | return this.stateUpdating == INIT_STATE_UNINIT; 281 | } 282 | 283 | // operation type: visible 284 | public boolean isUninitialisedVisible() { 285 | return this.stateVisible == INIT_STATE_UNINIT; 286 | } 287 | 288 | // operation type: updating 289 | public boolean isInitialisedUpdating() { 290 | return this.stateUpdating == INIT_STATE_INIT; 291 | } 292 | 293 | // operation type: visible 294 | public boolean isInitialisedVisible() { 295 | return this.stateVisible == INIT_STATE_INIT; 296 | } 297 | 298 | // operation type: updating 299 | public boolean isHiddenUpdating() { 300 | return this.stateUpdating == INIT_STATE_HIDDEN; 301 | } 302 | 303 | // operation type: updating 304 | public boolean isHiddenVisible() { 305 | return this.stateVisible == INIT_STATE_HIDDEN; 306 | } 307 | 308 | // operation type: updating 309 | protected void swapUpdatingAndMarkDirty() { 310 | if (this.updatingDirty) { 311 | return; 312 | } 313 | 314 | if (this.storageUpdating == null) { 315 | this.storageUpdating = allocateBytes(); 316 | Arrays.fill(this.storageUpdating, (byte)0); 317 | } else { 318 | System.arraycopy(this.storageUpdating, 0, this.storageUpdating = allocateBytes(), 0, ARRAY_SIZE); 319 | } 320 | 321 | if (this.stateUpdating != INIT_STATE_HIDDEN) { 322 | this.stateUpdating = INIT_STATE_INIT; 323 | } 324 | this.updatingDirty = true; 325 | } 326 | 327 | // operation type: updating 328 | public boolean updateVisible() { 329 | if (!this.isDirty()) { 330 | return false; 331 | } 332 | 333 | synchronized (this) { 334 | if (this.stateUpdating == INIT_STATE_NULL || this.stateUpdating == INIT_STATE_UNINIT) { 335 | this.storageVisible = null; 336 | } else { 337 | if (this.storageVisible == null) { 338 | this.storageVisible = this.storageUpdating.clone(); 339 | } else { 340 | if (this.storageUpdating != this.storageVisible) { 341 | System.arraycopy(this.storageUpdating, 0, this.storageVisible, 0, ARRAY_SIZE); 342 | } 343 | } 344 | 345 | if (this.storageUpdating != this.storageVisible) { 346 | freeBytes(this.storageUpdating); 347 | } 348 | this.storageUpdating = this.storageVisible; 349 | } 350 | this.updatingDirty = false; 351 | this.stateVisible = this.stateUpdating; 352 | } 353 | 354 | return true; 355 | } 356 | 357 | // operation type: visible 358 | public DataLayer toVanillaNibble() { 359 | synchronized (this) { 360 | switch (this.stateVisible) { 361 | case INIT_STATE_HIDDEN: 362 | case INIT_STATE_NULL: 363 | return null; 364 | case INIT_STATE_UNINIT: 365 | return new DataLayer(); 366 | case INIT_STATE_INIT: 367 | return new DataLayer(this.storageVisible.clone()); 368 | default: 369 | throw new IllegalStateException(); 370 | } 371 | } 372 | } 373 | 374 | /* x | (z << 4) | (y << 8) */ 375 | 376 | // operation type: updating 377 | public int getUpdating(final int x, final int y, final int z) { 378 | return this.getUpdating((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); 379 | } 380 | 381 | // operation type: updating 382 | public int getUpdating(final int index) { 383 | // indices range from 0 -> 4096 384 | final byte[] bytes = this.storageUpdating; 385 | if (bytes == null) { 386 | return 0; 387 | } 388 | final byte value = bytes[index >>> 1]; 389 | 390 | // if we are an even index, we want lower 4 bits 391 | // if we are an odd index, we want upper 4 bits 392 | return ((value >>> ((index & 1) << 2)) & 0xF); 393 | } 394 | 395 | // operation type: visible 396 | public int getVisible(final int x, final int y, final int z) { 397 | return this.getVisible((x & 15) | ((z & 15) << 4) | ((y & 15) << 8)); 398 | } 399 | 400 | // operation type: visible 401 | public int getVisible(final int index) { 402 | // indices range from 0 -> 4096 403 | final byte[] visibleBytes = this.storageVisible; 404 | if (visibleBytes == null) { 405 | return 0; 406 | } 407 | final byte value = visibleBytes[index >>> 1]; 408 | 409 | // if we are an even index, we want lower 4 bits 410 | // if we are an odd index, we want upper 4 bits 411 | return ((value >>> ((index & 1) << 2)) & 0xF); 412 | } 413 | 414 | // operation type: updating 415 | public void set(final int x, final int y, final int z, final int value) { 416 | this.set((x & 15) | ((z & 15) << 4) | ((y & 15) << 8), value); 417 | } 418 | 419 | // operation type: updating 420 | public void set(final int index, final int value) { 421 | if (!this.updatingDirty) { 422 | this.swapUpdatingAndMarkDirty(); 423 | } 424 | final int shift = (index & 1) << 2; 425 | final int i = index >>> 1; 426 | 427 | this.storageUpdating[i] = (byte)((this.storageUpdating[i] & (0xF0 >>> shift)) | (value << shift)); 428 | } 429 | 430 | public static final class SaveState { 431 | 432 | public final byte[] data; 433 | public final int state; 434 | 435 | public SaveState(final byte[] data, final int state) { 436 | this.data = data; 437 | this.state = state; 438 | } 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/light/StarLightInterface.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.light; 2 | 3 | import ca.spottedleaf.starlight.common.chunk.ExtendedChunk; 4 | import ca.spottedleaf.starlight.common.thread.GlobalExecutors; 5 | import ca.spottedleaf.starlight.common.thread.SchedulingUtil; 6 | import ca.spottedleaf.starlight.common.util.CoordinateUtils; 7 | import ca.spottedleaf.starlight.common.util.WorldUtil; 8 | import ca.spottedleaf.starlight.common.world.ExtendedWorld; 9 | import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; 10 | import it.unimi.dsi.fastutil.longs.Long2ObjectMap; 11 | import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; 12 | import it.unimi.dsi.fastutil.longs.Long2ReferenceMaps; 13 | import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; 14 | import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator; 15 | import it.unimi.dsi.fastutil.shorts.ShortCollection; 16 | import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; 17 | import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; 18 | import net.minecraft.core.BlockPos; 19 | import net.minecraft.core.SectionPos; 20 | import net.minecraft.server.level.ServerLevel; 21 | import net.minecraft.server.level.ThreadedLevelLightEngine; 22 | import net.minecraft.server.level.TicketType; 23 | import net.minecraft.world.level.ChunkPos; 24 | import net.minecraft.world.level.Level; 25 | import net.minecraft.world.level.chunk.ChunkAccess; 26 | import net.minecraft.world.level.chunk.status.ChunkStatus; 27 | import net.minecraft.world.level.chunk.DataLayer; 28 | import net.minecraft.world.level.chunk.LightChunkGetter; 29 | import net.minecraft.world.level.lighting.LayerLightEventListener; 30 | import net.minecraft.world.level.lighting.LevelLightEngine; 31 | import java.util.ArrayDeque; 32 | import java.util.ArrayList; 33 | import java.util.List; 34 | import java.util.Set; 35 | import java.util.concurrent.CompletableFuture; 36 | import java.util.concurrent.atomic.AtomicInteger; 37 | import java.util.function.Consumer; 38 | import java.util.function.Function; 39 | import java.util.function.IntConsumer; 40 | 41 | public final class StarLightInterface { 42 | 43 | public static final TicketType CHUNK_WORK_TICKET = TicketType.create("starlight_chunk_work_ticket", (p1, p2) -> Long.compare(p1.toLong(), p2.toLong())); 44 | 45 | /** 46 | * Can be {@code null}, indicating the light is all empty. 47 | */ 48 | protected final Level world; 49 | protected final LightChunkGetter lightAccess; 50 | 51 | protected final ArrayDeque cachedSkyPropagators; 52 | protected final ArrayDeque cachedBlockPropagators; 53 | 54 | protected final LightQueue lightQueue = new LightQueue(this); 55 | 56 | protected final LayerLightEventListener skyReader; 57 | protected final LayerLightEventListener blockReader; 58 | protected final boolean isClientSide; 59 | 60 | protected final int minSection; 61 | protected final int maxSection; 62 | protected final int minLightSection; 63 | protected final int maxLightSection; 64 | 65 | public final LevelLightEngine lightEngine; 66 | 67 | private final boolean hasBlockLight; 68 | private final boolean hasSkyLight; 69 | 70 | public StarLightInterface(final LightChunkGetter lightAccess, final boolean hasSkyLight, final boolean hasBlockLight, final LevelLightEngine lightEngine) { 71 | this.lightAccess = lightAccess; 72 | this.world = lightAccess == null ? null : (Level)lightAccess.getLevel(); 73 | this.cachedSkyPropagators = hasSkyLight && lightAccess != null ? new ArrayDeque<>() : null; 74 | this.cachedBlockPropagators = hasBlockLight && lightAccess != null ? new ArrayDeque<>() : null; 75 | this.isClientSide = !(this.world instanceof ServerLevel); 76 | if (this.world == null) { 77 | this.minSection = -4; 78 | this.maxSection = 19; 79 | this.minLightSection = -5; 80 | this.maxLightSection = 20; 81 | } else { 82 | this.minSection = WorldUtil.getMinSection(this.world); 83 | this.maxSection = WorldUtil.getMaxSection(this.world); 84 | this.minLightSection = WorldUtil.getMinLightSection(this.world); 85 | this.maxLightSection = WorldUtil.getMaxLightSection(this.world); 86 | } 87 | this.lightEngine = lightEngine; 88 | this.hasBlockLight = hasBlockLight; 89 | this.hasSkyLight = hasSkyLight; 90 | this.skyReader = !hasSkyLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { 91 | @Override 92 | public void checkBlock(final BlockPos blockPos) { 93 | StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); 94 | } 95 | 96 | @Override 97 | public void propagateLightSources(final ChunkPos chunkPos) { 98 | throw new UnsupportedOperationException(); 99 | } 100 | 101 | @Override 102 | public boolean hasLightWork() { 103 | // not really correct... 104 | return StarLightInterface.this.hasUpdates(); 105 | } 106 | 107 | @Override 108 | public int runLightUpdates() { 109 | throw new UnsupportedOperationException(); 110 | } 111 | 112 | @Override 113 | public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { 114 | throw new UnsupportedOperationException(); 115 | } 116 | 117 | @Override 118 | public DataLayer getDataLayerData(final SectionPos pos) { 119 | final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); 120 | if (chunk == null || (!StarLightInterface.this.isClientSide && !chunk.isLightCorrect())) { 121 | return null; 122 | } 123 | 124 | final int sectionY = pos.getY(); 125 | 126 | if (sectionY > StarLightInterface.this.maxLightSection || sectionY < StarLightInterface.this.minLightSection) { 127 | return null; 128 | } 129 | 130 | // if (((ExtendedChunk)chunk).getSkyEmptinessMap() == null) { 131 | // return null; 132 | // } 133 | 134 | return ((ExtendedChunk)chunk).getSkyNibbles()[sectionY - StarLightInterface.this.minLightSection].toVanillaNibble(); 135 | } 136 | 137 | @Override 138 | public int getLightValue(final BlockPos blockPos) { 139 | return StarLightInterface.this.getSkyLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); 140 | } 141 | 142 | @Override 143 | public void updateSectionStatus(final SectionPos pos, final boolean notReady) { 144 | StarLightInterface.this.sectionChange(pos, notReady); 145 | } 146 | }; 147 | this.blockReader = !hasBlockLight ? LayerLightEventListener.DummyLightLayerEventListener.INSTANCE : new LayerLightEventListener() { 148 | @Override 149 | public void checkBlock(final BlockPos blockPos) { 150 | StarLightInterface.this.lightEngine.checkBlock(blockPos.immutable()); 151 | } 152 | 153 | @Override 154 | public void propagateLightSources(final ChunkPos chunkPos) { 155 | throw new UnsupportedOperationException(); 156 | } 157 | 158 | @Override 159 | public boolean hasLightWork() { 160 | // not really correct... 161 | return StarLightInterface.this.hasUpdates(); 162 | } 163 | 164 | @Override 165 | public int runLightUpdates() { 166 | throw new UnsupportedOperationException(); 167 | } 168 | 169 | @Override 170 | public void setLightEnabled(final ChunkPos chunkPos, final boolean bl) { 171 | throw new UnsupportedOperationException(); 172 | } 173 | 174 | @Override 175 | public DataLayer getDataLayerData(final SectionPos pos) { 176 | final ChunkAccess chunk = StarLightInterface.this.getAnyChunkNow(pos.getX(), pos.getZ()); 177 | 178 | if (chunk == null || pos.getY() < StarLightInterface.this.minLightSection || pos.getY() > StarLightInterface.this.maxLightSection) { 179 | return null; 180 | } 181 | 182 | return ((ExtendedChunk)chunk).getBlockNibbles()[pos.getY() - StarLightInterface.this.minLightSection].toVanillaNibble(); 183 | } 184 | 185 | @Override 186 | public int getLightValue(final BlockPos blockPos) { 187 | return StarLightInterface.this.getBlockLightValue(blockPos, StarLightInterface.this.getAnyChunkNow(blockPos.getX() >> 4, blockPos.getZ() >> 4)); 188 | } 189 | 190 | @Override 191 | public void updateSectionStatus(final SectionPos pos, final boolean notReady) { 192 | StarLightInterface.this.sectionChange(pos, notReady); 193 | } 194 | }; 195 | } 196 | 197 | public boolean hasSkyLight() { 198 | return this.hasSkyLight; 199 | } 200 | 201 | public boolean hasBlockLight() { 202 | return this.hasBlockLight; 203 | } 204 | 205 | public int getSkyLightValue(final BlockPos blockPos, final ChunkAccess chunk) { 206 | if (!this.hasSkyLight) { 207 | return 0; 208 | } 209 | final int x = blockPos.getX(); 210 | int y = blockPos.getY(); 211 | final int z = blockPos.getZ(); 212 | 213 | final int minSection = this.minSection; 214 | final int maxSection = this.maxSection; 215 | final int minLightSection = this.minLightSection; 216 | final int maxLightSection = this.maxLightSection; 217 | 218 | if (chunk == null || (!this.isClientSide && !chunk.isLightCorrect()) || !chunk.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { 219 | return 15; 220 | } 221 | 222 | int sectionY = y >> 4; 223 | 224 | if (sectionY > maxLightSection) { 225 | return 15; 226 | } 227 | 228 | if (sectionY < minLightSection) { 229 | sectionY = minLightSection; 230 | y = sectionY << 4; 231 | } 232 | 233 | final SWMRNibbleArray[] nibbles = ((ExtendedChunk)chunk).getSkyNibbles(); 234 | final SWMRNibbleArray immediate = nibbles[sectionY - minLightSection]; 235 | 236 | if (!immediate.isNullNibbleVisible()) { 237 | return immediate.getVisible(x, y, z); 238 | } 239 | 240 | final boolean[] emptinessMap = ((ExtendedChunk)chunk).getSkyEmptinessMap(); 241 | 242 | if (emptinessMap == null) { 243 | return 15; 244 | } 245 | 246 | // are we above this chunk's lowest empty section? 247 | int lowestY = minLightSection - 1; 248 | for (int currY = maxSection; currY >= minSection; --currY) { 249 | if (emptinessMap[currY - minSection]) { 250 | continue; 251 | } 252 | 253 | // should always be full lit here 254 | lowestY = currY; 255 | break; 256 | } 257 | 258 | if (sectionY > lowestY) { 259 | return 15; 260 | } 261 | 262 | // this nibble is going to depend solely on the skylight data above it 263 | // find first non-null data above (there does exist one, as we just found it above) 264 | for (int currY = sectionY + 1; currY <= maxLightSection; ++currY) { 265 | final SWMRNibbleArray nibble = nibbles[currY - minLightSection]; 266 | if (!nibble.isNullNibbleVisible()) { 267 | return nibble.getVisible(x, 0, z); 268 | } 269 | } 270 | 271 | // should never reach here 272 | return 15; 273 | } 274 | 275 | public int getBlockLightValue(final BlockPos blockPos, final ChunkAccess chunk) { 276 | if (!this.hasBlockLight) { 277 | return 0; 278 | } 279 | final int y = blockPos.getY(); 280 | final int cy = y >> 4; 281 | 282 | final int minLightSection = this.minLightSection; 283 | final int maxLightSection = this.maxLightSection; 284 | 285 | if (cy < minLightSection || cy > maxLightSection) { 286 | return 0; 287 | } 288 | 289 | if (chunk == null) { 290 | return 0; 291 | } 292 | 293 | final SWMRNibbleArray nibble = ((ExtendedChunk)chunk).getBlockNibbles()[cy - minLightSection]; 294 | return nibble.getVisible(blockPos.getX(), y, blockPos.getZ()); 295 | } 296 | 297 | public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { 298 | final ChunkAccess chunk = this.getAnyChunkNow(pos.getX() >> 4, pos.getZ() >> 4); 299 | 300 | final int sky = this.getSkyLightValue(pos, chunk) - ambientDarkness; 301 | // Don't fetch the block light level if the skylight level is 15, since the value will never be higher. 302 | if (sky == 15) { 303 | return 15; 304 | } 305 | final int block = this.getBlockLightValue(pos, chunk); 306 | return Math.max(sky, block); 307 | } 308 | 309 | public LayerLightEventListener getSkyReader() { 310 | return this.skyReader; 311 | } 312 | 313 | public LayerLightEventListener getBlockReader() { 314 | return this.blockReader; 315 | } 316 | 317 | public boolean isClientSide() { 318 | return this.isClientSide; 319 | } 320 | 321 | public ChunkAccess getAnyChunkNow(final int chunkX, final int chunkZ) { 322 | if (this.world == null) { 323 | // empty world 324 | return null; 325 | } 326 | return ((ExtendedWorld)this.world).getAnyChunkImmediately(chunkX, chunkZ); 327 | } 328 | 329 | public boolean hasUpdates() { 330 | return !this.lightQueue.isEmpty(); 331 | } 332 | 333 | public Level getWorld() { 334 | return this.world; 335 | } 336 | 337 | public LightChunkGetter getLightAccess() { 338 | return this.lightAccess; 339 | } 340 | 341 | protected final SkyStarLightEngine getSkyLightEngine() { 342 | if (this.cachedSkyPropagators == null) { 343 | return null; 344 | } 345 | final SkyStarLightEngine ret; 346 | synchronized (this.cachedSkyPropagators) { 347 | ret = this.cachedSkyPropagators.pollFirst(); 348 | } 349 | 350 | if (ret == null) { 351 | return new SkyStarLightEngine(this.world); 352 | } 353 | return ret; 354 | } 355 | 356 | protected final void releaseSkyLightEngine(final SkyStarLightEngine engine) { 357 | if (this.cachedSkyPropagators == null) { 358 | return; 359 | } 360 | synchronized (this.cachedSkyPropagators) { 361 | this.cachedSkyPropagators.addFirst(engine); 362 | } 363 | } 364 | 365 | protected final BlockStarLightEngine getBlockLightEngine() { 366 | if (this.cachedBlockPropagators == null) { 367 | return null; 368 | } 369 | final BlockStarLightEngine ret; 370 | synchronized (this.cachedBlockPropagators) { 371 | ret = this.cachedBlockPropagators.pollFirst(); 372 | } 373 | 374 | if (ret == null) { 375 | return new BlockStarLightEngine(this.world); 376 | } 377 | return ret; 378 | } 379 | 380 | protected final void releaseBlockLightEngine(final BlockStarLightEngine engine) { 381 | if (this.cachedBlockPropagators == null) { 382 | return; 383 | } 384 | synchronized (this.cachedBlockPropagators) { 385 | this.cachedBlockPropagators.addFirst(engine); 386 | } 387 | } 388 | 389 | public LightQueue.ChunkTasks blockChange(final BlockPos pos) { 390 | if (this.world == null || pos.getY() < WorldUtil.getMinBlockY(this.world) || pos.getY() > WorldUtil.getMaxBlockY(this.world)) { // empty world 391 | return null; 392 | } 393 | 394 | return this.lightQueue.queueBlockChange(pos); 395 | } 396 | 397 | public LightQueue.ChunkTasks sectionChange(final SectionPos pos, final boolean newEmptyValue) { 398 | if (this.world == null) { // empty world 399 | return null; 400 | } 401 | 402 | return this.lightQueue.queueSectionChange(pos, newEmptyValue); 403 | } 404 | 405 | public void forceLoadInChunk(final ChunkAccess chunk, final Boolean[] emptySections) { 406 | final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); 407 | final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); 408 | 409 | try { 410 | if (skyEngine != null) { 411 | skyEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); 412 | } 413 | if (blockEngine != null) { 414 | blockEngine.forceHandleEmptySectionChanges(this.lightAccess, chunk, emptySections); 415 | } 416 | } finally { 417 | this.releaseSkyLightEngine(skyEngine); 418 | this.releaseBlockLightEngine(blockEngine); 419 | } 420 | } 421 | 422 | public void loadInChunk(final int chunkX, final int chunkZ, final Boolean[] emptySections) { 423 | final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); 424 | final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); 425 | 426 | try { 427 | if (skyEngine != null) { 428 | skyEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); 429 | } 430 | if (blockEngine != null) { 431 | blockEngine.handleEmptySectionChanges(this.lightAccess, chunkX, chunkZ, emptySections); 432 | } 433 | } finally { 434 | this.releaseSkyLightEngine(skyEngine); 435 | this.releaseBlockLightEngine(blockEngine); 436 | } 437 | } 438 | 439 | public void lightChunk(final ChunkAccess chunk, final Boolean[] emptySections) { 440 | final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); 441 | final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); 442 | 443 | try { 444 | if (skyEngine != null) { 445 | skyEngine.light(this.lightAccess, chunk, emptySections); 446 | } 447 | if (blockEngine != null) { 448 | blockEngine.light(this.lightAccess, chunk, emptySections); 449 | } 450 | } finally { 451 | this.releaseSkyLightEngine(skyEngine); 452 | this.releaseBlockLightEngine(blockEngine); 453 | } 454 | } 455 | 456 | public void relightChunks(final Set chunks, final Consumer chunkLightCallback, 457 | final IntConsumer onComplete) { 458 | final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); 459 | final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); 460 | 461 | try { 462 | if (skyEngine != null) { 463 | skyEngine.relightChunks(this.lightAccess, chunks, blockEngine == null ? chunkLightCallback : null, 464 | blockEngine == null ? onComplete : null); 465 | } 466 | if (blockEngine != null) { 467 | blockEngine.relightChunks(this.lightAccess, chunks, chunkLightCallback, onComplete); 468 | } 469 | } finally { 470 | this.releaseSkyLightEngine(skyEngine); 471 | this.releaseBlockLightEngine(blockEngine); 472 | } 473 | } 474 | 475 | public void checkChunkEdges(final int chunkX, final int chunkZ) { 476 | this.checkSkyEdges(chunkX, chunkZ); 477 | this.checkBlockEdges(chunkX, chunkZ); 478 | } 479 | 480 | public void checkSkyEdges(final int chunkX, final int chunkZ) { 481 | final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); 482 | 483 | try { 484 | if (skyEngine != null) { 485 | skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); 486 | } 487 | } finally { 488 | this.releaseSkyLightEngine(skyEngine); 489 | } 490 | } 491 | 492 | public void checkBlockEdges(final int chunkX, final int chunkZ) { 493 | final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); 494 | try { 495 | if (blockEngine != null) { 496 | blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ); 497 | } 498 | } finally { 499 | this.releaseBlockLightEngine(blockEngine); 500 | } 501 | } 502 | 503 | public void checkSkyEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { 504 | final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); 505 | 506 | try { 507 | if (skyEngine != null) { 508 | skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); 509 | } 510 | } finally { 511 | this.releaseSkyLightEngine(skyEngine); 512 | } 513 | } 514 | 515 | public void checkBlockEdges(final int chunkX, final int chunkZ, final ShortCollection sections) { 516 | final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); 517 | try { 518 | if (blockEngine != null) { 519 | blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, sections); 520 | } 521 | } finally { 522 | this.releaseBlockLightEngine(blockEngine); 523 | } 524 | } 525 | 526 | public void scheduleChunkLight(final ChunkPos pos, final Runnable run) { 527 | this.lightQueue.queueChunkLighting(pos, run); 528 | } 529 | 530 | public CompletableFuture syncFuture(final int chunkX, final int chunkZ) { 531 | return this.lightQueue.getChunkSyncFuture(chunkX, chunkZ).thenApply(Function.identity()); 532 | } 533 | 534 | public void removeChunkTasks(final ChunkPos pos) { 535 | this.lightQueue.removeChunk(pos); 536 | } 537 | 538 | public void propagateChanges() { 539 | if (this.lightQueue.isEmpty()) { 540 | return; 541 | } 542 | 543 | if (GlobalExecutors.ENABLED && this.lightEngine instanceof ThreadedLevelLightEngine threadedLevelLightEngine) { 544 | this.schedulePropagation0(threadedLevelLightEngine); 545 | return; 546 | } 547 | 548 | final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); 549 | final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); 550 | 551 | try { 552 | LightQueue.ChunkTasks task; 553 | while ((task = this.lightQueue.removeFirstTask()) != null) { 554 | handleUpdateInternal(task, skyEngine, blockEngine); 555 | } 556 | } finally { 557 | this.releaseSkyLightEngine(skyEngine); 558 | this.releaseBlockLightEngine(blockEngine); 559 | } 560 | } 561 | 562 | private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(0); 563 | private static final CompletableFuture COMPLETED_FUTURE = CompletableFuture.completedFuture(null); 564 | private final int instanceId = INSTANCE_COUNTER.getAndIncrement(); 565 | private final Long2ReferenceMap> chunkFutures = Long2ReferenceMaps.synchronize(new Long2ReferenceOpenHashMap<>()); 566 | 567 | private void schedulePropagation0(ThreadedLevelLightEngine threadedLevelLightEngine) { 568 | synchronized (this.lightQueue) { 569 | final ObjectBidirectionalIterator> iterator = this.lightQueue.chunkTasks.long2ObjectEntrySet().fastIterator(); 570 | while (iterator.hasNext()) { 571 | final Long2ObjectMap.Entry entry = iterator.next(); 572 | final long pos = entry.getLongKey(); 573 | if (!this.chunkFutures.getOrDefault(pos, COMPLETED_FUTURE).isDone()) { 574 | continue; 575 | } 576 | CompletableFuture future = new CompletableFuture<>(); 577 | SchedulingUtil.scheduleTask( 578 | this.instanceId, 579 | () -> { 580 | try { 581 | final SkyStarLightEngine skyEngine = this.getSkyLightEngine(); 582 | final BlockStarLightEngine blockEngine = this.getBlockLightEngine(); 583 | 584 | LightQueue.ChunkTasks tasks; 585 | synchronized (this.lightQueue) { 586 | tasks = this.lightQueue.chunkTasks.remove(pos); 587 | } 588 | if (tasks != null) { 589 | try { 590 | handleUpdateInternal(tasks, skyEngine, blockEngine); 591 | } finally { 592 | this.releaseSkyLightEngine(skyEngine); 593 | this.releaseBlockLightEngine(blockEngine); 594 | } 595 | 596 | threadedLevelLightEngine.tryScheduleUpdate(); 597 | } 598 | future.complete(null); 599 | } catch (Throwable t) { 600 | future.completeExceptionally(t); 601 | t.printStackTrace(); 602 | } 603 | }, 604 | CoordinateUtils.getChunkX(pos), 605 | CoordinateUtils.getChunkZ(pos), 606 | 2 607 | ); 608 | chunkFutures.put(pos, future); 609 | future.whenComplete((unused, throwable) -> this.chunkFutures.remove(pos, future)); 610 | } 611 | this.lightQueue.queueDirty = false; 612 | } 613 | } 614 | 615 | /** 616 | * Only relevant on server lighting with scaling enabled, best-effort check if the queue is dirty. 617 | */ 618 | public boolean isQueueDirty() { 619 | return this.lightQueue.queueDirty; 620 | } 621 | 622 | private void handleUpdateInternal(LightQueue.ChunkTasks task, SkyStarLightEngine skyEngine, BlockStarLightEngine blockEngine) { // keep indentation 623 | if (task.lightTasks != null) { 624 | for (final Runnable run : task.lightTasks) { 625 | run.run(); 626 | } 627 | } 628 | 629 | final long coordinate = task.chunkCoordinate; 630 | final int chunkX = CoordinateUtils.getChunkX(coordinate); 631 | final int chunkZ = CoordinateUtils.getChunkZ(coordinate); 632 | 633 | final Set positions = task.changedPositions; 634 | final Boolean[] sectionChanges = task.changedSectionSet; 635 | 636 | if (skyEngine != null && (!positions.isEmpty() || sectionChanges != null)) { 637 | skyEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); 638 | } 639 | if (blockEngine != null && (!positions.isEmpty() || sectionChanges != null)) { 640 | blockEngine.blocksChangedInChunk(this.lightAccess, chunkX, chunkZ, positions, sectionChanges); 641 | } 642 | 643 | if (skyEngine != null && task.queuedEdgeChecksSky != null) { 644 | skyEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksSky); 645 | } 646 | if (blockEngine != null && task.queuedEdgeChecksBlock != null) { 647 | blockEngine.checkChunkEdges(this.lightAccess, chunkX, chunkZ, task.queuedEdgeChecksBlock); 648 | } 649 | 650 | task.onComplete.complete(null); 651 | } 652 | 653 | public static final class LightQueue { 654 | 655 | protected final Long2ObjectLinkedOpenHashMap chunkTasks = new Long2ObjectLinkedOpenHashMap<>(); 656 | protected final StarLightInterface manager; 657 | protected volatile boolean queueDirty = false; 658 | 659 | public LightQueue(final StarLightInterface manager) { 660 | this.manager = manager; 661 | } 662 | 663 | public synchronized boolean isEmpty() { 664 | return this.chunkTasks.isEmpty(); 665 | } 666 | 667 | public synchronized LightQueue.ChunkTasks queueBlockChange(final BlockPos pos) { 668 | final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); 669 | tasks.changedPositions.add(pos.immutable()); 670 | this.queueDirty = true; 671 | return tasks; 672 | } 673 | 674 | public synchronized LightQueue.ChunkTasks queueSectionChange(final SectionPos pos, final boolean newEmptyValue) { 675 | final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); 676 | 677 | if (tasks.changedSectionSet == null) { 678 | tasks.changedSectionSet = new Boolean[this.manager.maxSection - this.manager.minSection + 1]; 679 | } 680 | tasks.changedSectionSet[pos.getY() - this.manager.minSection] = Boolean.valueOf(newEmptyValue); 681 | 682 | this.queueDirty = true; 683 | return tasks; 684 | } 685 | 686 | public synchronized LightQueue.ChunkTasks queueChunkLighting(final ChunkPos pos, final Runnable lightTask) { 687 | final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); 688 | if (tasks.lightTasks == null) { 689 | tasks.lightTasks = new ArrayList<>(); 690 | } 691 | tasks.lightTasks.add(lightTask); 692 | 693 | this.queueDirty = true; 694 | return tasks; 695 | } 696 | 697 | public synchronized LightQueue.ChunkTasks queueChunkSkylightEdgeCheck(final SectionPos pos, final ShortCollection sections) { 698 | final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); 699 | 700 | ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksSky; 701 | if (queuedEdges == null) { 702 | queuedEdges = tasks.queuedEdgeChecksSky = new ShortOpenHashSet(); 703 | } 704 | queuedEdges.addAll(sections); 705 | 706 | this.queueDirty = true; 707 | return tasks; 708 | } 709 | 710 | public synchronized LightQueue.ChunkTasks queueChunkBlocklightEdgeCheck(final SectionPos pos, final ShortCollection sections) { 711 | final ChunkTasks tasks = this.chunkTasks.computeIfAbsent(CoordinateUtils.getChunkKey(pos), ChunkTasks::new); 712 | 713 | ShortOpenHashSet queuedEdges = tasks.queuedEdgeChecksBlock; 714 | if (queuedEdges == null) { 715 | queuedEdges = tasks.queuedEdgeChecksBlock = new ShortOpenHashSet(); 716 | } 717 | queuedEdges.addAll(sections); 718 | 719 | this.queueDirty = true; 720 | return tasks; 721 | } 722 | 723 | public synchronized CompletableFuture getChunkSyncFuture(final int chunkX, final int chunkZ) { 724 | final ChunkTasks tasks = this.chunkTasks.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); 725 | if (tasks == null) { 726 | return CompletableFuture.completedFuture(null); 727 | } else { 728 | return tasks.onComplete; 729 | } 730 | } 731 | 732 | public void removeChunk(final ChunkPos pos) { 733 | final ChunkTasks tasks; 734 | synchronized (this) { 735 | tasks = this.chunkTasks.remove(CoordinateUtils.getChunkKey(pos)); 736 | } 737 | if (tasks != null) { 738 | tasks.onComplete.complete(null); 739 | } 740 | this.queueDirty = true; 741 | } 742 | 743 | public synchronized ChunkTasks removeFirstTask() { 744 | if (this.chunkTasks.isEmpty()) { 745 | return null; 746 | } 747 | return this.chunkTasks.removeFirst(); 748 | } 749 | 750 | public static final class ChunkTasks { 751 | 752 | public final Set changedPositions = new ObjectOpenHashSet<>(); 753 | public Boolean[] changedSectionSet; 754 | public ShortOpenHashSet queuedEdgeChecksSky; 755 | public ShortOpenHashSet queuedEdgeChecksBlock; 756 | public List lightTasks; 757 | 758 | public boolean isTicketAdded = false; 759 | public final CompletableFuture onComplete = new CompletableFuture<>(); 760 | 761 | public final long chunkCoordinate; 762 | 763 | public ChunkTasks(final long chunkCoordinate) { 764 | this.chunkCoordinate = chunkCoordinate; 765 | } 766 | } 767 | } 768 | } 769 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/light/StarLightLightingProvider.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.light; 2 | 3 | import net.minecraft.core.SectionPos; 4 | import net.minecraft.world.level.ChunkPos; 5 | import net.minecraft.world.level.LightLayer; 6 | import net.minecraft.world.level.chunk.DataLayer; 7 | import net.minecraft.world.level.chunk.LevelChunk; 8 | 9 | public interface StarLightLightingProvider { 10 | 11 | public StarLightInterface getLightEngine(); 12 | 13 | public void clientUpdateLight(final LightLayer lightType, final SectionPos pos, 14 | final DataLayer nibble, final boolean trustEdges); 15 | 16 | public void clientRemoveLightData(final ChunkPos chunkPos); 17 | 18 | public void clientChunkLoad(final ChunkPos pos, final LevelChunk chunk); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/thread/GlobalExecutors.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.thread; 2 | 3 | import ca.spottedleaf.starlight.common.config.Config; 4 | import com.ishland.flowsched.executor.ExecutorManager; 5 | 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | public class GlobalExecutors { 9 | 10 | private static final AtomicInteger prioritizedSchedulerCounter = new AtomicInteger(0); 11 | public static final ExecutorManager prioritizedScheduler = new ExecutorManager(Config.PARALLELISM, thread -> { 12 | thread.setDaemon(true); 13 | thread.setName("scalablelux-%d".formatted(prioritizedSchedulerCounter.getAndIncrement())); 14 | }); 15 | private static final boolean FORCE_ENABLED = Boolean.getBoolean("scalablelux.force_enabled"); 16 | public static final boolean ENABLED = SchedulingUtil.isExternallyManaged() || FORCE_ENABLED || Config.PARALLELISM > 1; 17 | 18 | static { 19 | if (SchedulingUtil.isExternallyManaged()) { 20 | System.out.println("[ScalableLux] Lighting scaling is enabled in externally managed mode"); 21 | } else if (FORCE_ENABLED) { 22 | System.out.println("[ScalableLux] Lighting scaling is forced enabled, using %d threads".formatted(Config.PARALLELISM)); 23 | } else if (ENABLED) { 24 | System.out.println("[ScalableLux] Lighting scaling is enabled, using %d threads".formatted(Config.PARALLELISM)); 25 | } else { 26 | System.out.println("[ScalableLux] Lighting scaling is disabled (due to low parallelism in the settings)"); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/thread/LockTokenImpl.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.thread; 2 | 3 | import com.ishland.flowsched.executor.LockToken; 4 | 5 | public record LockTokenImpl(int ownerTag, long pos) implements LockToken { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/thread/SchedulingUtil.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.thread; 2 | 3 | import ca.spottedleaf.starlight.common.util.CoordinateUtils; 4 | import com.ishland.flowsched.executor.LockToken; 5 | 6 | import java.util.ArrayList; 7 | 8 | public class SchedulingUtil { 9 | 10 | public static void scheduleTask(int ownerTag, Runnable task, int x, int z, int radius) { 11 | final ArrayList lockTokens = new ArrayList<>((radius * 2 + 1) * (radius * 2 + 1)); 12 | for (int i = -radius; i <= radius; i++) { 13 | for (int j = -radius; j <= radius; j++) { 14 | lockTokens.add(new LockTokenImpl(ownerTag, CoordinateUtils.getChunkKey(x + i, z + j))); 15 | } 16 | } 17 | final SimpleTask simpleTask = new SimpleTask(task, lockTokens.toArray(LockToken[]::new), 60); 18 | GlobalExecutors.prioritizedScheduler.schedule(simpleTask); 19 | } 20 | 21 | public static boolean isExternallyManaged() { 22 | return false; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/thread/SimpleTask.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.thread; 2 | 3 | import com.ishland.flowsched.executor.LockToken; 4 | import com.ishland.flowsched.executor.Task; 5 | 6 | import java.util.Objects; 7 | 8 | public class SimpleTask implements Task { 9 | 10 | private final Runnable task; 11 | private final LockToken[] lockTokens; 12 | private final int priority; 13 | 14 | public SimpleTask(Runnable task, LockToken[] lockTokens, int priority) { 15 | this.task = Objects.requireNonNull(task, "task"); 16 | this.lockTokens = Objects.requireNonNull(lockTokens, "lockTokens"); 17 | this.priority = priority; 18 | } 19 | 20 | @Override 21 | public void run(Runnable releaseLocks) { 22 | try { 23 | this.task.run(); 24 | } finally { 25 | releaseLocks.run(); 26 | } 27 | } 28 | 29 | @Override 30 | public void propagateException(Throwable t) { 31 | t.printStackTrace(); 32 | } 33 | 34 | @Override 35 | public LockToken[] lockTokens() { 36 | return this.lockTokens; 37 | } 38 | 39 | @Override 40 | public int priority() { 41 | return this.priority; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/util/CoordinateUtils.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.util; 2 | 3 | import net.minecraft.core.BlockPos; 4 | import net.minecraft.core.SectionPos; 5 | import net.minecraft.util.Mth; 6 | import net.minecraft.world.entity.Entity; 7 | import net.minecraft.world.level.ChunkPos; 8 | 9 | public final class CoordinateUtils { 10 | 11 | // dx, dz are relative to the target chunk 12 | // dx, dz in [-radius, radius] 13 | public static int getNeighbourMappedIndex(final int dx, final int dz, final int radius) { 14 | return (dx + radius) + (2 * radius + 1)*(dz + radius); 15 | } 16 | 17 | // the chunk keys are compatible with vanilla 18 | 19 | public static long getChunkKey(final BlockPos pos) { 20 | return ((long)(pos.getZ() >> 4) << 32) | ((pos.getX() >> 4) & 0xFFFFFFFFL); 21 | } 22 | 23 | public static long getChunkKey(final Entity entity) { 24 | return ((long)(Mth.floor(entity.getZ()) >> 4) << 32) | ((Mth.floor(entity.getX()) >> 4) & 0xFFFFFFFFL); 25 | } 26 | 27 | public static long getChunkKey(final ChunkPos pos) { 28 | return ((long)pos.z << 32) | (pos.x & 0xFFFFFFFFL); 29 | } 30 | 31 | public static long getChunkKey(final SectionPos pos) { 32 | return ((long)pos.getZ() << 32) | (pos.getX() & 0xFFFFFFFFL); 33 | } 34 | 35 | public static long getChunkKey(final int x, final int z) { 36 | return ((long)z << 32) | (x & 0xFFFFFFFFL); 37 | } 38 | 39 | public static int getChunkX(final long chunkKey) { 40 | return (int)chunkKey; 41 | } 42 | 43 | public static int getChunkZ(final long chunkKey) { 44 | return (int)(chunkKey >>> 32); 45 | } 46 | 47 | public static int getChunkCoordinate(final double blockCoordinate) { 48 | return Mth.floor(blockCoordinate) >> 4; 49 | } 50 | 51 | // the section keys are compatible with vanilla's 52 | 53 | static final int SECTION_X_BITS = 22; 54 | static final long SECTION_X_MASK = (1L << SECTION_X_BITS) - 1; 55 | static final int SECTION_Y_BITS = 20; 56 | static final long SECTION_Y_MASK = (1L << SECTION_Y_BITS) - 1; 57 | static final int SECTION_Z_BITS = 22; 58 | static final long SECTION_Z_MASK = (1L << SECTION_Z_BITS) - 1; 59 | // format is y,z,x (in order of LSB to MSB) 60 | static final int SECTION_Y_SHIFT = 0; 61 | static final int SECTION_Z_SHIFT = SECTION_Y_SHIFT + SECTION_Y_BITS; 62 | static final int SECTION_X_SHIFT = SECTION_Z_SHIFT + SECTION_X_BITS; 63 | static final int SECTION_TO_BLOCK_SHIFT = 4; 64 | 65 | public static long getChunkSectionKey(final int x, final int y, final int z) { 66 | return ((x & SECTION_X_MASK) << SECTION_X_SHIFT) 67 | | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) 68 | | ((z & SECTION_Z_MASK) << SECTION_Z_SHIFT); 69 | } 70 | 71 | public static long getChunkSectionKey(final SectionPos pos) { 72 | return ((pos.getX() & SECTION_X_MASK) << SECTION_X_SHIFT) 73 | | ((pos.getY() & SECTION_Y_MASK) << SECTION_Y_SHIFT) 74 | | ((pos.getZ() & SECTION_Z_MASK) << SECTION_Z_SHIFT); 75 | } 76 | 77 | public static long getChunkSectionKey(final ChunkPos pos, final int y) { 78 | return ((pos.x & SECTION_X_MASK) << SECTION_X_SHIFT) 79 | | ((y & SECTION_Y_MASK) << SECTION_Y_SHIFT) 80 | | ((pos.z & SECTION_Z_MASK) << SECTION_Z_SHIFT); 81 | } 82 | 83 | public static long getChunkSectionKey(final BlockPos pos) { 84 | return (((long)pos.getX() << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | 85 | ((pos.getY() >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | 86 | (((long)pos.getZ() << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); 87 | } 88 | 89 | public static long getChunkSectionKey(final Entity entity) { 90 | return ((Mth.lfloor(entity.getX()) << (SECTION_X_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_X_MASK << SECTION_X_SHIFT)) | 91 | ((Mth.lfloor(entity.getY()) >> SECTION_TO_BLOCK_SHIFT) & (SECTION_Y_MASK << SECTION_Y_SHIFT)) | 92 | ((Mth.lfloor(entity.getZ()) << (SECTION_Z_SHIFT - SECTION_TO_BLOCK_SHIFT)) & (SECTION_Z_MASK << SECTION_Z_SHIFT)); 93 | } 94 | 95 | public static int getChunkSectionX(final long key) { 96 | return (int)(key << (Long.SIZE - (SECTION_X_SHIFT + SECTION_X_BITS)) >> (Long.SIZE - SECTION_X_BITS)); 97 | } 98 | 99 | public static int getChunkSectionY(final long key) { 100 | return (int)(key << (Long.SIZE - (SECTION_Y_SHIFT + SECTION_Y_BITS)) >> (Long.SIZE - SECTION_Y_BITS)); 101 | } 102 | 103 | public static int getChunkSectionZ(final long key) { 104 | return (int)(key << (Long.SIZE - (SECTION_Z_SHIFT + SECTION_Z_BITS)) >> (Long.SIZE - SECTION_Z_BITS)); 105 | } 106 | 107 | // the block coordinates are not necessarily compatible with vanilla's 108 | 109 | public static int getBlockCoordinate(final double blockCoordinate) { 110 | return Mth.floor(blockCoordinate); 111 | } 112 | 113 | public static long getBlockKey(final int x, final int y, final int z) { 114 | return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54); 115 | } 116 | 117 | public static long getBlockKey(final BlockPos pos) { 118 | return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54); 119 | } 120 | 121 | public static long getBlockKey(final Entity entity) { 122 | return ((long)entity.getX() & 0x7FFFFFF) | (((long)entity.getZ() & 0x7FFFFFF) << 27) | ((long)entity.getY() << 54); 123 | } 124 | 125 | private CoordinateUtils() { 126 | throw new RuntimeException(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/util/IntegerUtil.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.util; 2 | 3 | public final class IntegerUtil { 4 | 5 | public static final int HIGH_BIT_U32 = Integer.MIN_VALUE; 6 | public static final long HIGH_BIT_U64 = Long.MIN_VALUE; 7 | 8 | public static int ceilLog2(final int value) { 9 | return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros 10 | } 11 | 12 | public static long ceilLog2(final long value) { 13 | return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros 14 | } 15 | 16 | public static int floorLog2(final int value) { 17 | // xor is optimized subtract for 2^n -1 18 | // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) 19 | return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros 20 | } 21 | 22 | public static int floorLog2(final long value) { 23 | // xor is optimized subtract for 2^n -1 24 | // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1) 25 | return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros 26 | } 27 | 28 | public static int roundCeilLog2(final int value) { 29 | // optimized variant of 1 << (32 - leading(val - 1)) 30 | // given 31 | // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) 32 | // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) 33 | // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1))) 34 | // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1)) 35 | // HIGH_BIT_32 >>> (-1 + leading(val - 1)) 36 | return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1); 37 | } 38 | 39 | public static long roundCeilLog2(final long value) { 40 | // see logic documented above 41 | return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1); 42 | } 43 | 44 | public static int roundFloorLog2(final int value) { 45 | // optimized variant of 1 << (31 - leading(val)) 46 | // given 47 | // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32) 48 | // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val))) 49 | // HIGH_BIT_32 >> (31 - (31 - leading(val))) 50 | // HIGH_BIT_32 >> (31 - 31 + leading(val)) 51 | return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value); 52 | } 53 | 54 | public static long roundFloorLog2(final long value) { 55 | // see logic documented above 56 | return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value); 57 | } 58 | 59 | public static boolean isPowerOfTwo(final int n) { 60 | // 2^n has one bit 61 | // note: this rets true for 0 still 62 | return IntegerUtil.getTrailingBit(n) == n; 63 | } 64 | 65 | public static boolean isPowerOfTwo(final long n) { 66 | // 2^n has one bit 67 | // note: this rets true for 0 still 68 | return IntegerUtil.getTrailingBit(n) == n; 69 | } 70 | 71 | public static int getTrailingBit(final int n) { 72 | return -n & n; 73 | } 74 | 75 | public static long getTrailingBit(final long n) { 76 | return -n & n; 77 | } 78 | 79 | public static int trailingZeros(final int n) { 80 | return Integer.numberOfTrailingZeros(n); 81 | } 82 | 83 | public static int trailingZeros(final long n) { 84 | return Long.numberOfTrailingZeros(n); 85 | } 86 | 87 | // from hacker's delight (signed division magic value) 88 | public static int getDivisorMultiple(final long numbers) { 89 | return (int)(numbers >>> 32); 90 | } 91 | 92 | // from hacker's delight (signed division magic value) 93 | public static int getDivisorShift(final long numbers) { 94 | return (int)numbers; 95 | } 96 | 97 | // copied from hacker's delight (signed division magic value) 98 | // http://www.hackersdelight.org/hdcodetxt/magic.c.txt 99 | public static long getDivisorNumbers(final int d) { 100 | final int ad = branchlessAbs(d); 101 | 102 | if (ad < 2) { 103 | throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d); 104 | } 105 | 106 | final int two31 = 0x80000000; 107 | final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour 108 | 109 | /* 110 | Signed usage: 111 | int number; 112 | long magic = getDivisorNumbers(div); 113 | long mul = magic >>> 32; 114 | int sign = number >> 31; 115 | int result = (int)(((long)number * mul) >>> magic) - sign; 116 | */ 117 | /* 118 | Unsigned usage: 119 | int number; 120 | long magic = getDivisorNumbers(div); 121 | long mul = magic >>> 32; 122 | int result = (int)(((long)number * mul) >>> magic); 123 | */ 124 | 125 | int p = 31; 126 | 127 | // all these variables are UNSIGNED! 128 | int t = two31 + (d >>> 31); 129 | int anc = t - 1 - (int)((t & mask)%ad); 130 | int q1 = (int)((two31 & mask)/(anc & mask)); 131 | int r1 = two31 - q1*anc; 132 | int q2 = (int)((two31 & mask)/(ad & mask)); 133 | int r2 = two31 - q2*ad; 134 | int delta; 135 | 136 | do { 137 | p = p + 1; 138 | q1 = 2*q1; // Update q1 = 2**p/|nc|. 139 | r1 = 2*r1; // Update r1 = rem(2**p, |nc|). 140 | if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here) 141 | q1 = q1 + 1; 142 | r1 = r1 - anc; 143 | } 144 | q2 = 2*q2; // Update q2 = 2**p/|d|. 145 | r2 = 2*r2; // Update r2 = rem(2**p, |d|). 146 | if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here) 147 | q2 = q2 + 1; 148 | r2 = r2 - ad; 149 | } 150 | delta = ad - r2; 151 | } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0)); 152 | 153 | int magicNum = q2 + 1; 154 | if (d < 0) { 155 | magicNum = -magicNum; 156 | } 157 | int shift = p; 158 | return ((long)magicNum << 32) | shift; 159 | } 160 | 161 | public static int branchlessAbs(final int val) { 162 | // -n = -1 ^ n + 1 163 | final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0 164 | return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 165 | } 166 | 167 | public static long branchlessAbs(final long val) { 168 | // -n = -1 ^ n + 1 169 | final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0 170 | return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1 171 | } 172 | 173 | //https://github.com/skeeto/hash-prospector for hash functions 174 | 175 | //score = ~590.47984224483832 176 | public static int hash0(int x) { 177 | x *= 0x36935555; 178 | x ^= x >>> 16; 179 | return x; 180 | } 181 | 182 | //score = ~310.01596637036749 183 | public static int hash1(int x) { 184 | x ^= x >>> 15; 185 | x *= 0x356aaaad; 186 | x ^= x >>> 17; 187 | return x; 188 | } 189 | 190 | public static int hash2(int x) { 191 | x ^= x >>> 16; 192 | x *= 0x7feb352d; 193 | x ^= x >>> 15; 194 | x *= 0x846ca68b; 195 | x ^= x >>> 16; 196 | return x; 197 | } 198 | 199 | public static int hash3(int x) { 200 | x ^= x >>> 17; 201 | x *= 0xed5ad4bb; 202 | x ^= x >>> 11; 203 | x *= 0xac4c1b51; 204 | x ^= x >>> 15; 205 | x *= 0x31848bab; 206 | x ^= x >>> 14; 207 | return x; 208 | } 209 | 210 | //score = ~365.79959673201887 211 | public static long hash1(long x) { 212 | x ^= x >>> 27; 213 | x *= 0xb24924b71d2d354bL; 214 | x ^= x >>> 28; 215 | return x; 216 | } 217 | 218 | //h2 hash 219 | public static long hash2(long x) { 220 | x ^= x >>> 32; 221 | x *= 0xd6e8feb86659fd93L; 222 | x ^= x >>> 32; 223 | x *= 0xd6e8feb86659fd93L; 224 | x ^= x >>> 32; 225 | return x; 226 | } 227 | 228 | public static long hash3(long x) { 229 | x ^= x >>> 45; 230 | x *= 0xc161abe5704b6c79L; 231 | x ^= x >>> 41; 232 | x *= 0xe3e5389aedbc90f7L; 233 | x ^= x >>> 56; 234 | x *= 0x1f9aba75a52db073L; 235 | x ^= x >>> 53; 236 | return x; 237 | } 238 | 239 | private IntegerUtil() { 240 | throw new RuntimeException(); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/util/SaveUtil.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.util; 2 | 3 | import ca.spottedleaf.starlight.common.chunk.ExtendedChunk; 4 | import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; 5 | import ca.spottedleaf.starlight.common.light.StarLightEngine; 6 | import ca.spottedleaf.starlight.common.world.ExtendedSerializableChunkData; 7 | import com.mojang.logging.LogUtils; 8 | import net.minecraft.nbt.CompoundTag; 9 | import net.minecraft.nbt.ListTag; 10 | import net.minecraft.server.level.ServerLevel; 11 | import net.minecraft.world.level.ChunkPos; 12 | import net.minecraft.world.level.Level; 13 | import net.minecraft.world.level.LevelHeightAccessor; 14 | import net.minecraft.world.level.chunk.ChunkAccess; 15 | import net.minecraft.world.level.chunk.DataLayer; 16 | import net.minecraft.world.level.chunk.status.ChunkStatus; 17 | import net.minecraft.world.level.chunk.storage.SerializableChunkData; 18 | import org.slf4j.Logger; 19 | 20 | import java.util.Arrays; 21 | import java.util.List; 22 | import java.util.ListIterator; 23 | 24 | public final class SaveUtil { 25 | 26 | private static final Logger LOGGER = LogUtils.getLogger(); 27 | 28 | private static final int STARLIGHT_LIGHT_VERSION = 9; 29 | 30 | public static int getLightVersion() { 31 | return STARLIGHT_LIGHT_VERSION; 32 | } 33 | 34 | private static final String BLOCKLIGHT_STATE_TAG = "starlight.blocklight_state"; 35 | private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; 36 | private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; 37 | 38 | public static void prepareSaveLightHook(final ChunkAccess chunk, final SerializableChunkData data) { 39 | try { 40 | prepareSaveLightHookReal(chunk, data); 41 | } catch (final Throwable ex) { 42 | // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false 43 | // for Vanilla to relight on load and it will not set our lit tag so we will relight on load 44 | if (ex instanceof ThreadDeath) { 45 | throw (ThreadDeath)ex; 46 | } 47 | LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex); 48 | } 49 | } 50 | 51 | private static void prepareSaveLightHookReal(final ChunkAccess chunk, final SerializableChunkData data) { 52 | // strip existing lighting data 53 | ListIterator iterator = data.sectionData().listIterator(); // mutable in vanilla 54 | while (iterator.hasNext()) { 55 | SerializableChunkData.SectionData sectionData = iterator.next(); 56 | iterator.set(new SerializableChunkData.SectionData(sectionData.y(), sectionData.chunkSection(), null, null)); 57 | } 58 | 59 | // store lighting data 60 | ((ExtendedSerializableChunkData) (Object) data).scalablelux$setBlockLight( 61 | Arrays.stream(((ExtendedChunk) chunk).getBlockNibbles()) 62 | .map(SWMRNibbleArray::getSaveState) 63 | .toArray(SWMRNibbleArray.SaveState[]::new) 64 | ); 65 | ((ExtendedSerializableChunkData) (Object) data).scalablelux$setSkyLight( 66 | Arrays.stream(((ExtendedChunk) chunk).getSkyNibbles()) 67 | .map(SWMRNibbleArray::getSaveState) 68 | .toArray(SWMRNibbleArray.SaveState[]::new) 69 | ); 70 | } 71 | 72 | public static void prepareSaveVanillaLightHook(final ServerLevel serverLevel, final ChunkAccess chunk, final SerializableChunkData data) { 73 | boolean lightCorrect = data.lightCorrect(); 74 | ((ExtendedSerializableChunkData) (Object) data).scalablelux$setLightCorrect(false); 75 | try { 76 | prepareSaveVanillaLightHookReal(serverLevel, chunk, data); 77 | ((ExtendedSerializableChunkData) (Object) data).scalablelux$setLightCorrect(lightCorrect); 78 | } catch (final Throwable ex) { 79 | // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false 80 | // for Vanilla to relight on load and it will not set our lit tag so we will relight on load 81 | if (ex instanceof ThreadDeath) { 82 | throw (ThreadDeath)ex; 83 | } 84 | LOGGER.warn("Failed to inject light data into save data for chunk " + chunk.getPos() + ", chunk light will be recalculated on its next load", ex); 85 | } 86 | } 87 | 88 | private static void prepareSaveVanillaLightHookReal(final ServerLevel serverLevel, final ChunkAccess chunk, final SerializableChunkData data) { 89 | // replace existing lighting data 90 | SWMRNibbleArray.SaveState[] blockNibbles = Arrays.stream(((ExtendedChunk) chunk).getBlockNibbles()) 91 | .map(SWMRNibbleArray::getSaveState) 92 | .toArray(SWMRNibbleArray.SaveState[]::new); 93 | SWMRNibbleArray.SaveState[] skyNibbles = Arrays.stream(((ExtendedChunk) chunk).getSkyNibbles()) 94 | .map(SWMRNibbleArray::getSaveState) 95 | .toArray(SWMRNibbleArray.SaveState[]::new); 96 | 97 | ListIterator iterator = data.sectionData().listIterator(); // mutable in vanilla 98 | while (iterator.hasNext()) { 99 | SerializableChunkData.SectionData sectionData = iterator.next(); 100 | int index = sectionData.y() - WorldUtil.getMinLightSection(serverLevel); 101 | byte[] blockRaw = blockNibbles[index] != null ? blockNibbles[index].data : null; 102 | byte[] skyRaw = skyNibbles[index] != null ? skyNibbles[index].data : null; 103 | iterator.set(new SerializableChunkData.SectionData(sectionData.y(), sectionData.chunkSection(), blockRaw != null ? new DataLayer(blockRaw) : new DataLayer(), skyRaw != null ? new DataLayer(skyRaw) : new DataLayer())); 104 | } 105 | } 106 | 107 | public static void saveLightHook(final SerializableChunkData data, final CompoundTag nbt) { 108 | try { 109 | saveLightHookReal(data, nbt); 110 | } catch (final Throwable ex) { 111 | // failing to inject is not fatal so we catch anything here. if it fails, it will have correctly set lit to false 112 | // for Vanilla to relight on load and it will not set our lit tag so we will relight on load 113 | if (ex instanceof ThreadDeath) { 114 | throw (ThreadDeath)ex; 115 | } 116 | LOGGER.warn("Failed to inject light data into save data for chunk " + data.chunkPos() + ", chunk light will be recalculated on its next load", ex); 117 | } 118 | } 119 | 120 | private static void saveLightHookReal(final SerializableChunkData data, final CompoundTag tag) { 121 | if (tag == null) { 122 | return; 123 | } 124 | 125 | // light sections are exclusive 126 | final int minSection = data.minSectionY() - 1; // exclusive 127 | 128 | SWMRNibbleArray.SaveState[] blockNibbles = ((ExtendedSerializableChunkData) (Object) data).scalablelux$getBlockLight(); 129 | SWMRNibbleArray.SaveState[] skyNibbles = ((ExtendedSerializableChunkData) (Object) data).scalablelux$getSkyLight(); 130 | 131 | final int maxSection = minSection + blockNibbles.length - 1; // exclusive 132 | 133 | boolean lit = data.lightCorrect(); 134 | // diff start - store our tag for whether light data is init'd 135 | if (lit) { 136 | tag.putBoolean("isLightOn", false); 137 | } 138 | // diff end - store our tag for whether light data is init'd 139 | ChunkStatus status = ChunkStatus.byName(tag.getString("Status")); 140 | 141 | CompoundTag[] sections = new CompoundTag[maxSection - minSection + 1]; 142 | 143 | ListTag sectionsStored = tag.getList("sections", 10); 144 | 145 | for (int i = 0; i < sectionsStored.size(); ++i) { 146 | CompoundTag sectionStored = sectionsStored.getCompound(i); 147 | int k = sectionStored.getByte("Y"); 148 | 149 | // strip light data 150 | sectionStored.remove("BlockLight"); 151 | sectionStored.remove("SkyLight"); 152 | 153 | if (!sectionStored.isEmpty()) { 154 | sections[k - minSection] = sectionStored; 155 | } 156 | } 157 | 158 | if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { 159 | for (int i = minSection; i <= maxSection; ++i) { 160 | SWMRNibbleArray.SaveState blockNibble = blockNibbles[i - minSection]; 161 | SWMRNibbleArray.SaveState skyNibble = skyNibbles[i - minSection]; 162 | if (blockNibble != null || skyNibble != null) { 163 | CompoundTag section = sections[i - minSection]; 164 | if (section == null) { 165 | section = new CompoundTag(); 166 | section.putByte("Y", (byte)i); 167 | sections[i - minSection] = section; 168 | } 169 | 170 | // we store under the same key so mod programs editing nbt 171 | // can still read the data, hopefully. 172 | // however, for compatibility we store chunks as unlit so vanilla 173 | // is forced to re-light them if it encounters our data. It's too much of a burden 174 | // to try and maintain compatibility with a broken and inferior skylight management system. 175 | 176 | if (blockNibble != null) { 177 | if (blockNibble.data != null) { 178 | section.putByteArray("BlockLight", blockNibble.data); 179 | } 180 | section.putInt(BLOCKLIGHT_STATE_TAG, blockNibble.state); 181 | } 182 | 183 | if (skyNibble != null) { 184 | if (skyNibble.data != null) { 185 | section.putByteArray("SkyLight", skyNibble.data); 186 | } 187 | section.putInt(SKYLIGHT_STATE_TAG, skyNibble.state); 188 | } 189 | } 190 | } 191 | } 192 | 193 | // rewrite section list 194 | sectionsStored.clear(); 195 | for (CompoundTag section : sections) { 196 | if (section != null) { 197 | sectionsStored.add(section); 198 | } 199 | } 200 | tag.put("sections", sectionsStored); 201 | if (lit) { 202 | tag.putInt(STARLIGHT_VERSION_TAG, STARLIGHT_LIGHT_VERSION); // only mark as fully lit after we have successfully injected our data 203 | } 204 | } 205 | 206 | public static void prepareLoadLightHook(final LevelHeightAccessor levelHeightAccessor, final CompoundTag tag, final SerializableChunkData data) { 207 | try { 208 | prepareLoadLightHookReal(levelHeightAccessor, tag, data); 209 | } catch (final Throwable ex) { 210 | // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct 211 | // lighting in both cases. 212 | if (ex instanceof ThreadDeath) { 213 | throw (ThreadDeath)ex; 214 | } 215 | LOGGER.warn("Failed to load light for chunk " + data.chunkPos() + ", light will be recalculated", ex); 216 | } 217 | } 218 | 219 | private static void prepareLoadLightHookReal(final LevelHeightAccessor world, final CompoundTag tag, final SerializableChunkData data) { 220 | final int minSection = WorldUtil.getMinLightSection(world); 221 | final int maxSection = WorldUtil.getMaxLightSection(world); 222 | 223 | // mark as unlit in case we fail parsing 224 | ((ExtendedSerializableChunkData) (Object) data).scalablelux$setLightCorrect(false); 225 | ((ExtendedSerializableChunkData) (Object) data).scalableLux$setActuallyCorrect(false); 226 | 227 | SWMRNibbleArray.SaveState[] blockLight = StarLightEngine.getFilledEmptySaveState(world); 228 | SWMRNibbleArray.SaveState[] skyLight = StarLightEngine.getFilledEmptySaveState(world); 229 | 230 | boolean lit = tag.get("isLightOn") != null && tag.getInt(STARLIGHT_VERSION_TAG) == STARLIGHT_LIGHT_VERSION; 231 | // not enough context: assumes always reads skylight 232 | ChunkStatus status = data.chunkStatus(); 233 | 234 | if (lit && status.isOrAfter(ChunkStatus.LIGHT)) { 235 | ListTag sections = tag.getList("sections", 10); 236 | 237 | for (int i = 0; i < sections.size(); ++i) { 238 | CompoundTag sectionData = sections.getCompound(i); 239 | int y = sectionData.getByte("Y"); 240 | 241 | if (sectionData.contains("BlockLight", 7)) { 242 | blockLight[y - minSection] = new SWMRNibbleArray.SaveState(sectionData.getByteArray("BlockLight").clone(), sectionData.getInt(BLOCKLIGHT_STATE_TAG)); // clone for data safety 243 | } else { 244 | blockLight[y - minSection] = new SWMRNibbleArray.SaveState(null, sectionData.getInt(BLOCKLIGHT_STATE_TAG)); 245 | } 246 | 247 | if (sectionData.contains("SkyLight", 7)) { 248 | // we store under the same key so mod programs editing nbt 249 | // can still read the data, hopefully. 250 | // however, for compatibility we store chunks as unlit so vanilla 251 | // is forced to re-light them if it encounters our data. It's too much of a burden 252 | // to try and maintain compatibility with a broken and inferior skylight management system. 253 | skyLight[y - minSection] = new SWMRNibbleArray.SaveState(sectionData.getByteArray("SkyLight").clone(), sectionData.getInt(SKYLIGHT_STATE_TAG)); // clone for data safety 254 | } else { 255 | skyLight[y - minSection] = new SWMRNibbleArray.SaveState(null, sectionData.getInt(SKYLIGHT_STATE_TAG)); 256 | } 257 | } 258 | } 259 | 260 | ((ExtendedSerializableChunkData) (Object) data).scalablelux$setBlockLight(blockLight); 261 | ((ExtendedSerializableChunkData) (Object) data).scalablelux$setSkyLight(skyLight); 262 | 263 | ((ExtendedSerializableChunkData) (Object) data).scalableLux$setActuallyCorrect(lit); 264 | } 265 | 266 | public static void loadLightHook(final Level world, final SerializableChunkData data, final ChunkAccess into) { 267 | try { 268 | loadLightHookReal(world, data, into); 269 | } catch (final Throwable ex) { 270 | // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct 271 | // lighting in both cases. 272 | if (ex instanceof ThreadDeath) { 273 | throw (ThreadDeath)ex; 274 | } 275 | LOGGER.warn("Failed to load light for chunk " + data.chunkPos() + ", light will be recalculated", ex); 276 | } 277 | } 278 | 279 | private static void loadLightHookReal(final Level world, final SerializableChunkData data, final ChunkAccess into) { 280 | if (into == null) { 281 | return; 282 | } 283 | final int minSection = WorldUtil.getMinLightSection(world); 284 | final int maxSection = WorldUtil.getMaxLightSection(world); 285 | 286 | into.setLightCorrect(false); // mark as unlit in case we fail parsing 287 | 288 | SWMRNibbleArray[] blockNibbles = Arrays.stream(((ExtendedSerializableChunkData) (Object) data).scalablelux$getBlockLight()) 289 | .map(state -> new SWMRNibbleArray(state.data, state.state)) 290 | .toArray(SWMRNibbleArray[]::new); 291 | SWMRNibbleArray[] skyNibbles = Arrays.stream(((ExtendedSerializableChunkData) (Object) data).scalablelux$getSkyLight()) 292 | .map(state -> new SWMRNibbleArray(state.data, state.state)) 293 | .toArray(SWMRNibbleArray[]::new); 294 | 295 | boolean lit = ((ExtendedSerializableChunkData) (Object) data).scalablelux$getActuallyCorrect(); 296 | 297 | ((ExtendedChunk)into).setBlockNibbles(blockNibbles); 298 | ((ExtendedChunk)into).setSkyNibbles(skyNibbles); 299 | 300 | into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data 301 | } 302 | 303 | public static void loadVanillaLightHook(final Level world, final SerializableChunkData data, final ChunkAccess into) { 304 | try { 305 | loadVanillaLightHookReal(world, data, into); 306 | } catch (final Throwable ex) { 307 | // failing to inject is not fatal so we catch anything here. if it fails, then we simply relight. Not a problem, we get correct 308 | // lighting in both cases. 309 | if (ex instanceof ThreadDeath) { 310 | throw (ThreadDeath)ex; 311 | } 312 | LOGGER.warn("Failed to load light for chunk " + data.chunkPos() + ", light will be recalculated", ex); 313 | } 314 | } 315 | 316 | private static void loadVanillaLightHookReal(final Level world, final SerializableChunkData data, final ChunkAccess into) { 317 | if (into == null) { 318 | return; 319 | } 320 | final int minSection = WorldUtil.getMinLightSection(world); 321 | final int maxSection = WorldUtil.getMaxLightSection(world); 322 | 323 | ChunkStatus status = data.chunkStatus(); 324 | boolean lit = into.isLightCorrect() && status.isOrAfter(ChunkStatus.LIGHT); 325 | 326 | into.setLightCorrect(false); // mark as unlit in case we fail parsing 327 | 328 | SWMRNibbleArray[] blockNibbles = StarLightEngine.getFilledEmptyLight(world); 329 | SWMRNibbleArray[] skyNibbles = StarLightEngine.getFilledEmptyLight(world); 330 | 331 | if (lit) { 332 | List sectionData = data.sectionData(); 333 | for (int i = 0, sectionDataSize = sectionData.size(); i < sectionDataSize; i++) { 334 | SerializableChunkData.SectionData section = sectionData.get(i); 335 | int y = section.y(); 336 | 337 | if (section.blockLight() != null) { 338 | // this is where our diff is 339 | blockNibbles[y - minSection] = SWMRNibbleArray.fromVanilla(section.blockLight()); // clone for data safety 340 | } 341 | 342 | if (section.skyLight() != null) { 343 | skyNibbles[y - minSection] = SWMRNibbleArray.fromVanilla(section.skyLight()); // clone for data safety 344 | } 345 | } 346 | // workaround vanilla quirk: skylight in sections below sections with initialized skylight is zero 347 | { 348 | boolean fillWithZero = false; 349 | for (int i = skyNibbles.length - 1; i >= 0; i--) { 350 | if (!skyNibbles[i].isNullNibbleVisible()) { 351 | fillWithZero = true; 352 | continue; 353 | } 354 | if (fillWithZero) { 355 | skyNibbles[i].setNonNull(); 356 | skyNibbles[i].updateVisible(); 357 | } 358 | } 359 | } 360 | } 361 | 362 | ((ExtendedChunk)into).setBlockNibbles(blockNibbles); 363 | ((ExtendedChunk)into).setSkyNibbles(skyNibbles); 364 | 365 | into.setLightCorrect(lit); // now we set lit here, only after we've correctly parsed data 366 | } 367 | 368 | private SaveUtil() {} 369 | } 370 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/util/WorldUtil.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.util; 2 | 3 | import net.minecraft.world.level.LevelHeightAccessor; 4 | 5 | public final class WorldUtil { 6 | 7 | // min, max are inclusive 8 | 9 | public static int getMaxSection(final LevelHeightAccessor world) { 10 | return world.getMaxSectionY(); // getMaxSection() is ~~exclusive~~ inclusive since 24w33a 11 | } 12 | 13 | public static int getMinSection(final LevelHeightAccessor world) { 14 | return world.getMinSectionY(); 15 | } 16 | 17 | public static int getMaxLightSection(final LevelHeightAccessor world) { 18 | return getMaxSection(world) + 1; 19 | } 20 | 21 | public static int getMinLightSection(final LevelHeightAccessor world) { 22 | return getMinSection(world) - 1; 23 | } 24 | 25 | 26 | 27 | public static int getTotalSections(final LevelHeightAccessor world) { 28 | return getMaxSection(world) - getMinSection(world) + 1; 29 | } 30 | 31 | public static int getTotalLightSections(final LevelHeightAccessor world) { 32 | return getMaxLightSection(world) - getMinLightSection(world) + 1; 33 | } 34 | 35 | public static int getMinBlockY(final LevelHeightAccessor world) { 36 | return getMinSection(world) << 4; 37 | } 38 | 39 | public static int getMaxBlockY(final LevelHeightAccessor world) { 40 | return (getMaxSection(world) << 4) | 15; 41 | } 42 | 43 | private WorldUtil() { 44 | throw new RuntimeException(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/world/ExtendedSerializableChunkData.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.world; 2 | 3 | import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; 4 | 5 | public interface ExtendedSerializableChunkData { 6 | 7 | void scalablelux$setBlockLight(SWMRNibbleArray.SaveState[] light); 8 | 9 | void scalablelux$setSkyLight(SWMRNibbleArray.SaveState[] light); 10 | 11 | void scalableLux$setActuallyCorrect(boolean correct); 12 | 13 | SWMRNibbleArray.SaveState[] scalablelux$getBlockLight(); 14 | 15 | SWMRNibbleArray.SaveState[] scalablelux$getSkyLight(); 16 | 17 | boolean scalablelux$getActuallyCorrect(); 18 | 19 | void scalablelux$setLightCorrect(boolean correct); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/common/world/ExtendedWorld.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.common.world; 2 | 3 | import net.minecraft.world.level.chunk.ChunkAccess; 4 | import net.minecraft.world.level.chunk.LevelChunk; 5 | 6 | public interface ExtendedWorld { 7 | 8 | // rets full chunk without blocking 9 | public LevelChunk getChunkAtImmediately(final int chunkX, final int chunkZ); 10 | 11 | // rets chunk at any stage, if it exists, immediately 12 | public ChunkAccess getAnyChunkImmediately(final int chunkX, final int chunkZ); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/client/multiplayer/ClientPacketListenerMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.client.multiplayer; 2 | 3 | import ca.spottedleaf.starlight.common.light.StarLightLightingProvider; 4 | import net.minecraft.client.multiplayer.ClientLevel; 5 | import net.minecraft.client.multiplayer.ClientPacketListener; 6 | import net.minecraft.core.SectionPos; 7 | import net.minecraft.network.protocol.game.ClientGamePacketListener; 8 | import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; 9 | import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; 10 | import net.minecraft.network.protocol.game.ClientboundLightUpdatePacketData; 11 | import net.minecraft.world.level.ChunkPos; 12 | import net.minecraft.world.level.LightLayer; 13 | import net.minecraft.world.level.chunk.status.ChunkStatus; 14 | import net.minecraft.world.level.chunk.DataLayer; 15 | import net.minecraft.world.level.chunk.LevelChunk; 16 | import net.minecraft.world.level.lighting.LevelLightEngine; 17 | import org.jetbrains.annotations.Nullable; 18 | import org.spongepowered.asm.mixin.Mixin; 19 | import org.spongepowered.asm.mixin.Shadow; 20 | import org.spongepowered.asm.mixin.injection.At; 21 | import org.spongepowered.asm.mixin.injection.Inject; 22 | import org.spongepowered.asm.mixin.injection.Redirect; 23 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 24 | 25 | @Mixin(value = ClientPacketListener.class, priority = 1001) 26 | public abstract class ClientPacketListenerMixin implements ClientGamePacketListener { 27 | 28 | /* 29 | The call behaviors in the packet handler are much more clear about how they should affect the light engine, 30 | and as a result makes the client light load/unload more reliable 31 | */ 32 | 33 | @Shadow 34 | private ClientLevel level; 35 | 36 | /* 37 | Now in 1.18 Mojang has added logic to delay rendering chunks until their lighting is ready (as they are delaying 38 | light updates). Fortunately for us, Starlight doesn't take any kind of hit loading in light data. So we have no reason 39 | to delay the light updates at all (and we shouldn't delay them or else desync might occur - such as with block updates). 40 | */ 41 | 42 | @Shadow 43 | protected abstract void applyLightData(final int chunkX, final int chunkZ, final ClientboundLightUpdatePacketData clientboundLightUpdatePacketData, boolean markDirty); 44 | 45 | @Shadow 46 | protected abstract void enableChunkLight(final LevelChunk levelChunk, final int chunkX, final int chunkZ); 47 | 48 | /** 49 | * Call the runnable immediately to prevent desync 50 | * @author Spottedleaf 51 | */ 52 | @Redirect( 53 | method = "handleLightUpdatePacket", 54 | at = @At( 55 | value = "INVOKE", 56 | target = "Lnet/minecraft/client/multiplayer/ClientLevel;queueLightUpdate(Ljava/lang/Runnable;)V" 57 | ) 58 | ) 59 | private void starlightCallUpdateImmediately(final ClientLevel instance, final Runnable runnable) { 60 | runnable.run(); 61 | } 62 | 63 | /** 64 | * Re-route light update packet to our own logic 65 | * @author Spottedleaf 66 | */ 67 | @Redirect( 68 | method = "readSectionList", 69 | at = @At( 70 | target = "Lnet/minecraft/world/level/lighting/LevelLightEngine;queueSectionData(Lnet/minecraft/world/level/LightLayer;Lnet/minecraft/core/SectionPos;Lnet/minecraft/world/level/chunk/DataLayer;)V", 71 | value = "INVOKE", 72 | ordinal = 0 73 | ) 74 | ) 75 | private void loadLightDataHook(final LevelLightEngine lightEngine, final LightLayer lightType, final SectionPos pos, 76 | final @Nullable DataLayer nibble) { 77 | ((StarLightLightingProvider)this.level.getChunkSource().getLightEngine()).clientUpdateLight(lightType, pos, nibble, true); 78 | } 79 | 80 | 81 | /** 82 | * Avoid calling Vanilla's logic here, and instead call our own. 83 | * @author Spottedleaf 84 | */ 85 | @Redirect( 86 | method = "handleForgetLevelChunk", 87 | at = @At( 88 | value = "INVOKE", 89 | target = "Lnet/minecraft/client/multiplayer/ClientPacketListener;queueLightRemoval(Lnet/minecraft/network/protocol/game/ClientboundForgetLevelChunkPacket;)V" 90 | ) 91 | ) 92 | private void unloadLightDataHook(final ClientPacketListener instance, final ClientboundForgetLevelChunkPacket clientboundForgetLevelChunkPacket) { 93 | ((StarLightLightingProvider)this.level.getChunkSource().getLightEngine()).clientRemoveLightData(new ChunkPos(clientboundForgetLevelChunkPacket.pos().x, clientboundForgetLevelChunkPacket.pos().z)); 94 | } 95 | 96 | /** 97 | * Don't call vanilla's load logic 98 | */ 99 | @Redirect( 100 | method = "handleLevelChunkWithLight", 101 | at = @At( 102 | target = "Lnet/minecraft/client/multiplayer/ClientLevel;queueLightUpdate(Ljava/lang/Runnable;)V", 103 | value = "INVOKE", 104 | ordinal = 0 105 | ) 106 | ) 107 | private void postChunkLoadHookRedirect(final ClientLevel instance, final Runnable runnable) { 108 | // don't call vanilla's logic, see below 109 | } 110 | 111 | /** 112 | * Hook for loading in a chunk to the world 113 | * @author Spottedleaf 114 | */ 115 | @Inject( 116 | method = "handleLevelChunkWithLight", 117 | at = @At( 118 | value = "RETURN" 119 | ) 120 | ) 121 | private void postChunkLoadHook(final ClientboundLevelChunkWithLightPacket clientboundLevelChunkWithLightPacket, final CallbackInfo ci) { 122 | final int chunkX = clientboundLevelChunkWithLightPacket.getX(); 123 | final int chunkZ = clientboundLevelChunkWithLightPacket.getZ(); 124 | final LevelChunk chunk = this.level.getChunkSource().getChunk(chunkX, chunkZ, ChunkStatus.FULL, false); 125 | if (chunk == null) { 126 | // failed to load 127 | return; 128 | } 129 | // load in light data from packet immediately 130 | this.applyLightData(chunkX, chunkZ, clientboundLevelChunkWithLightPacket.getLightData(), true); 131 | ((StarLightLightingProvider)this.level.getChunkSource().getLightEngine()).clientChunkLoad(new ChunkPos(chunkX, chunkZ), chunk); 132 | 133 | // we need this for the update chunk status call, so that it can tell starlight what sections are empty and such 134 | this.enableChunkLight(chunk, chunkX, chunkZ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/client/world/ClientLevelMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.client.world; 2 | 3 | import ca.spottedleaf.starlight.common.world.ExtendedWorld; 4 | import net.minecraft.client.multiplayer.ClientChunkCache; 5 | import net.minecraft.client.multiplayer.ClientLevel; 6 | import net.minecraft.core.Holder; 7 | import net.minecraft.core.RegistryAccess; 8 | import net.minecraft.resources.ResourceKey; 9 | import net.minecraft.util.profiling.ProfilerFiller; 10 | import net.minecraft.world.level.Level; 11 | import net.minecraft.world.level.chunk.ChunkAccess; 12 | import net.minecraft.world.level.chunk.LevelChunk; 13 | import net.minecraft.world.level.dimension.DimensionType; 14 | import net.minecraft.world.level.storage.WritableLevelData; 15 | import org.spongepowered.asm.mixin.Mixin; 16 | import org.spongepowered.asm.mixin.Shadow; 17 | import java.util.function.Supplier; 18 | 19 | @Mixin(ClientLevel.class) 20 | public abstract class ClientLevelMixin extends Level implements ExtendedWorld { 21 | 22 | protected ClientLevelMixin(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, boolean bl, boolean bl2, long l, int i) { 23 | super(writableLevelData, resourceKey, registryAccess, holder, bl, bl2, l, i); 24 | } 25 | 26 | @Shadow 27 | public abstract ClientChunkCache getChunkSource(); 28 | 29 | @Override 30 | public final LevelChunk getChunkAtImmediately(final int chunkX, final int chunkZ) { 31 | return this.getChunkSource().getChunk(chunkX, chunkZ, false); 32 | } 33 | 34 | @Override 35 | public final ChunkAccess getAnyChunkImmediately(int chunkX, int chunkZ) { 36 | return this.getChunkSource().getChunk(chunkX, chunkZ, false); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/blockstate/BlockStateBaseMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.blockstate; 2 | 3 | import ca.spottedleaf.starlight.common.blockstate.ExtendedAbstractBlockState; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.mojang.serialization.MapCodec; 6 | import it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap; 7 | import net.minecraft.world.level.block.Block; 8 | import net.minecraft.world.level.block.state.BlockBehaviour; 9 | import net.minecraft.world.level.block.state.BlockState; 10 | import net.minecraft.world.level.block.state.StateHolder; 11 | import net.minecraft.world.level.block.state.properties.Property; 12 | import org.spongepowered.asm.mixin.Final; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.Shadow; 15 | import org.spongepowered.asm.mixin.Unique; 16 | import org.spongepowered.asm.mixin.injection.At; 17 | import org.spongepowered.asm.mixin.injection.Inject; 18 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 19 | 20 | @Mixin(BlockBehaviour.BlockStateBase.class) 21 | public abstract class BlockStateBaseMixin extends StateHolder implements ExtendedAbstractBlockState { 22 | 23 | @Shadow 24 | @Final 25 | private boolean useShapeForLightOcclusion; 26 | 27 | @Shadow 28 | @Final 29 | private boolean canOcclude; 30 | 31 | @Unique 32 | private boolean isConditionallyFullOpaque; 33 | 34 | protected BlockStateBaseMixin(Block object, Reference2ObjectArrayMap, Comparable> reference2ObjectArrayMap, MapCodec mapCodec) { 35 | super(object, reference2ObjectArrayMap, mapCodec); 36 | } 37 | 38 | /** 39 | * Initialises our light state for this block. 40 | */ 41 | @Inject( 42 | method = "initCache", 43 | at = @At("RETURN") 44 | ) 45 | public void initLightAccessState(final CallbackInfo ci) { 46 | this.isConditionallyFullOpaque = this.canOcclude & this.useShapeForLightOcclusion; 47 | } 48 | 49 | @Override 50 | public final boolean isConditionallyFullOpaque() { 51 | return this.isConditionallyFullOpaque; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/chunk/ChunkAccessMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.chunk; 2 | 3 | import ca.spottedleaf.starlight.common.chunk.ExtendedChunk; 4 | import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; 5 | import ca.spottedleaf.starlight.common.light.StarLightEngine; 6 | import net.minecraft.core.Registry; 7 | import net.minecraft.world.level.ChunkPos; 8 | import net.minecraft.world.level.LevelHeightAccessor; 9 | import net.minecraft.world.level.chunk.ChunkAccess; 10 | import net.minecraft.world.level.chunk.ImposterProtoChunk; 11 | import net.minecraft.world.level.chunk.LevelChunkSection; 12 | import net.minecraft.world.level.chunk.UpgradeData; 13 | import net.minecraft.world.level.levelgen.blending.BlendingData; 14 | import net.minecraft.world.level.lighting.ChunkSkyLightSources; 15 | import org.spongepowered.asm.mixin.Mixin; 16 | import org.spongepowered.asm.mixin.Shadow; 17 | import org.spongepowered.asm.mixin.Unique; 18 | import org.spongepowered.asm.mixin.injection.At; 19 | import org.spongepowered.asm.mixin.injection.Inject; 20 | import org.spongepowered.asm.mixin.injection.Redirect; 21 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 22 | 23 | @Mixin(ChunkAccess.class) 24 | public abstract class ChunkAccessMixin implements ExtendedChunk { 25 | 26 | @Shadow 27 | protected ChunkSkyLightSources skyLightSources; 28 | 29 | 30 | @Unique 31 | private volatile SWMRNibbleArray[] blockNibbles; 32 | 33 | @Unique 34 | private volatile SWMRNibbleArray[] skyNibbles; 35 | 36 | @Unique 37 | private volatile boolean[] skyEmptinessMap; 38 | 39 | @Unique 40 | private volatile boolean[] blockEmptinessMap; 41 | 42 | @Override 43 | public SWMRNibbleArray[] getBlockNibbles() { 44 | return this.blockNibbles; 45 | } 46 | 47 | @Override 48 | public void setBlockNibbles(final SWMRNibbleArray[] nibbles) { 49 | this.blockNibbles = nibbles; 50 | } 51 | 52 | @Override 53 | public SWMRNibbleArray[] getSkyNibbles() { 54 | return this.skyNibbles; 55 | } 56 | 57 | @Override 58 | public void setSkyNibbles(final SWMRNibbleArray[] nibbles) { 59 | this.skyNibbles = nibbles; 60 | } 61 | 62 | @Override 63 | public boolean[] getSkyEmptinessMap() { 64 | return this.skyEmptinessMap; 65 | } 66 | 67 | @Override 68 | public void setSkyEmptinessMap(final boolean[] emptinessMap) { 69 | this.skyEmptinessMap = emptinessMap; 70 | } 71 | 72 | @Override 73 | public boolean[] getBlockEmptinessMap() { 74 | return this.blockEmptinessMap; 75 | } 76 | 77 | @Override 78 | public void setBlockEmptinessMap(final boolean[] emptinessMap) { 79 | this.blockEmptinessMap = emptinessMap; 80 | } 81 | 82 | /** 83 | * @reason Remove unused skylight sources, and initialise nibble arrays. 84 | * @author Spottedleaf 85 | */ 86 | @Inject( 87 | method = "", 88 | at = @At( 89 | value = "RETURN" 90 | ) 91 | ) 92 | private void nullSources(ChunkPos chunkPos, UpgradeData upgradeData, LevelHeightAccessor levelHeightAccessor, 93 | Registry registry, long l, LevelChunkSection[] levelChunkSections, BlendingData blendingData, 94 | CallbackInfo ci) { 95 | this.skyLightSources = null; 96 | if (!((Object)this instanceof ImposterProtoChunk)) { 97 | this.setBlockNibbles(StarLightEngine.getFilledEmptyLight(levelHeightAccessor)); 98 | this.setSkyNibbles(StarLightEngine.getFilledEmptyLight(levelHeightAccessor)); 99 | } 100 | } 101 | 102 | /** 103 | * @reason Remove unused skylight sources 104 | * @author Spottedleaf 105 | */ 106 | @Redirect( 107 | method = "initializeLightSources", 108 | at = @At( 109 | value = "INVOKE", 110 | target = "Lnet/minecraft/world/level/lighting/ChunkSkyLightSources;fillFrom(Lnet/minecraft/world/level/chunk/ChunkAccess;)V" 111 | ) 112 | ) 113 | private void skipInit(final ChunkSkyLightSources instance, final ChunkAccess chunkAccess) {} 114 | } -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/chunk/EmptyLevelChunkMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.chunk; 2 | 3 | import ca.spottedleaf.starlight.common.chunk.ExtendedChunk; 4 | import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; 5 | import ca.spottedleaf.starlight.common.light.StarLightEngine; 6 | import net.minecraft.world.level.ChunkPos; 7 | import net.minecraft.world.level.Level; 8 | import net.minecraft.world.level.chunk.EmptyLevelChunk; 9 | import net.minecraft.world.level.chunk.LevelChunk; 10 | import org.spongepowered.asm.mixin.Mixin; 11 | 12 | @Mixin(EmptyLevelChunk.class) 13 | public abstract class EmptyLevelChunkMixin extends LevelChunk implements ExtendedChunk { 14 | 15 | public EmptyLevelChunkMixin(final Level level, final ChunkPos pos) { 16 | super(level, pos); 17 | } 18 | 19 | @Override 20 | public SWMRNibbleArray[] getBlockNibbles() { 21 | return StarLightEngine.getFilledEmptyLight(this.getLevel()); 22 | } 23 | 24 | @Override 25 | public void setBlockNibbles(final SWMRNibbleArray[] nibbles) {} 26 | 27 | @Override 28 | public SWMRNibbleArray[] getSkyNibbles() { 29 | return StarLightEngine.getFilledEmptyLight(this.getLevel()); 30 | } 31 | 32 | @Override 33 | public void setSkyNibbles(final SWMRNibbleArray[] nibbles) {} 34 | 35 | @Override 36 | public boolean[] getSkyEmptinessMap() { 37 | return null; 38 | } 39 | 40 | @Override 41 | public void setSkyEmptinessMap(final boolean[] emptinessMap) {} 42 | 43 | @Override 44 | public boolean[] getBlockEmptinessMap() { 45 | return null; 46 | } 47 | 48 | @Override 49 | public void setBlockEmptinessMap(final boolean[] emptinessMap) {} 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/chunk/ImposterProtoChunkMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.chunk; 2 | 3 | import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; 4 | import ca.spottedleaf.starlight.common.chunk.ExtendedChunk; 5 | import net.minecraft.core.Registry; 6 | import net.minecraft.core.registries.Registries; 7 | import net.minecraft.world.level.chunk.ImposterProtoChunk; 8 | import net.minecraft.world.level.chunk.LevelChunk; 9 | import net.minecraft.world.level.chunk.ProtoChunk; 10 | import net.minecraft.world.level.chunk.UpgradeData; 11 | import org.spongepowered.asm.mixin.Final; 12 | import org.spongepowered.asm.mixin.Mixin; 13 | import org.spongepowered.asm.mixin.Shadow; 14 | 15 | @Mixin(ImposterProtoChunk.class) 16 | public abstract class ImposterProtoChunkMixin extends ProtoChunk implements ExtendedChunk { 17 | 18 | @Final 19 | @Shadow 20 | private LevelChunk wrapped; 21 | 22 | public ImposterProtoChunkMixin(final LevelChunk levelChunk, final boolean bl) { 23 | super(levelChunk.getPos(), UpgradeData.EMPTY, levelChunk, levelChunk.getLevel().registryAccess().lookupOrThrow(Registries.BIOME), levelChunk.getBlendingData()); 24 | } 25 | 26 | @Override 27 | public SWMRNibbleArray[] getBlockNibbles() { 28 | return ((ExtendedChunk)this.wrapped).getBlockNibbles(); 29 | } 30 | 31 | @Override 32 | public void setBlockNibbles(final SWMRNibbleArray[] nibbles) { 33 | ((ExtendedChunk)this.wrapped).setBlockNibbles(nibbles); 34 | } 35 | 36 | @Override 37 | public SWMRNibbleArray[] getSkyNibbles() { 38 | return ((ExtendedChunk)this.wrapped).getSkyNibbles(); 39 | } 40 | 41 | @Override 42 | public void setSkyNibbles(final SWMRNibbleArray[] nibbles) { 43 | ((ExtendedChunk)this.wrapped).setSkyNibbles(nibbles); 44 | } 45 | 46 | @Override 47 | public boolean[] getSkyEmptinessMap() { 48 | return ((ExtendedChunk)this.wrapped).getSkyEmptinessMap(); 49 | } 50 | 51 | @Override 52 | public void setSkyEmptinessMap(final boolean[] emptinessMap) { 53 | ((ExtendedChunk)this.wrapped).setSkyEmptinessMap(emptinessMap); 54 | } 55 | 56 | @Override 57 | public boolean[] getBlockEmptinessMap() { 58 | return ((ExtendedChunk)this.wrapped).getBlockEmptinessMap(); 59 | } 60 | 61 | @Override 62 | public void setBlockEmptinessMap(final boolean[] emptinessMap) { 63 | ((ExtendedChunk)this.wrapped).setBlockEmptinessMap(emptinessMap); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/chunk/LevelChunkMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.chunk; 2 | 3 | import ca.spottedleaf.starlight.common.chunk.ExtendedChunk; 4 | import net.minecraft.server.level.ServerLevel; 5 | import net.minecraft.world.level.BlockGetter; 6 | import net.minecraft.world.level.chunk.LevelChunk; 7 | import net.minecraft.world.level.chunk.ProtoChunk; 8 | import net.minecraft.world.level.lighting.ChunkSkyLightSources; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Inject; 12 | import org.spongepowered.asm.mixin.injection.Redirect; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | 15 | @Mixin(LevelChunk.class) 16 | public abstract class LevelChunkMixin implements ExtendedChunk { 17 | 18 | /** 19 | * Copies the nibble data from the protochunk. 20 | * TODO since this is a constructor inject, check for new constructors on update. 21 | */ 22 | @Inject( 23 | method = "(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/level/chunk/ProtoChunk;Lnet/minecraft/world/level/chunk/LevelChunk$PostLoadProcessor;)V", 24 | at = @At("TAIL") 25 | ) 26 | public void onTransitionToFull(ServerLevel serverLevel, ProtoChunk protoChunk, LevelChunk.PostLoadProcessor postLoadProcessor, CallbackInfo ci) { 27 | this.setBlockNibbles(((ExtendedChunk)protoChunk).getBlockNibbles()); 28 | this.setSkyNibbles(((ExtendedChunk)protoChunk).getSkyNibbles()); 29 | this.setSkyEmptinessMap(((ExtendedChunk)protoChunk).getSkyEmptinessMap()); 30 | this.setBlockEmptinessMap(((ExtendedChunk)protoChunk).getBlockEmptinessMap()); 31 | } 32 | 33 | /** 34 | * @reason Remove unused skylight sources 35 | * @author Spottedleaf 36 | */ 37 | @Redirect( 38 | method = "setBlockState", 39 | at = @At( 40 | value = "INVOKE", 41 | target = "Lnet/minecraft/world/level/lighting/ChunkSkyLightSources;update(Lnet/minecraft/world/level/BlockGetter;III)Z" 42 | ) 43 | ) 44 | private boolean skipLightSources(final ChunkSkyLightSources instance, final BlockGetter blockGetter, 45 | final int x, final int y, final int z) { 46 | return false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/chunk/ProtoChunkMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.chunk; 2 | 3 | import ca.spottedleaf.starlight.common.chunk.ExtendedChunk; 4 | import net.minecraft.world.level.BlockGetter; 5 | import net.minecraft.world.level.chunk.ProtoChunk; 6 | import net.minecraft.world.level.lighting.ChunkSkyLightSources; 7 | import org.spongepowered.asm.mixin.Mixin; 8 | import org.spongepowered.asm.mixin.injection.At; 9 | import org.spongepowered.asm.mixin.injection.Redirect; 10 | 11 | @Mixin(ProtoChunk.class) 12 | public abstract class ProtoChunkMixin implements ExtendedChunk { 13 | 14 | /** 15 | * @reason Remove unused skylight sources 16 | * @author Spottedleaf 17 | */ 18 | @Redirect( 19 | method = "setBlockState", 20 | at = @At( 21 | value = "INVOKE", 22 | target = "Lnet/minecraft/world/level/lighting/ChunkSkyLightSources;update(Lnet/minecraft/world/level/BlockGetter;III)Z" 23 | ) 24 | ) 25 | private boolean skipLightSources(final ChunkSkyLightSources instance, final BlockGetter blockGetter, 26 | final int x, final int y, final int z) { 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/lightengine/LevelLightEngineMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.lightengine; 2 | 3 | import ca.spottedleaf.starlight.common.chunk.ExtendedChunk; 4 | import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; 5 | import ca.spottedleaf.starlight.common.light.StarLightEngine; 6 | import ca.spottedleaf.starlight.common.light.StarLightInterface; 7 | import ca.spottedleaf.starlight.common.light.StarLightLightingProvider; 8 | import ca.spottedleaf.starlight.common.util.CoordinateUtils; 9 | import ca.spottedleaf.starlight.common.util.WorldUtil; 10 | import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; 11 | import net.minecraft.core.BlockPos; 12 | import net.minecraft.core.SectionPos; 13 | import net.minecraft.world.level.ChunkPos; 14 | import net.minecraft.world.level.Level; 15 | import net.minecraft.world.level.LightLayer; 16 | import net.minecraft.world.level.chunk.ChunkAccess; 17 | import net.minecraft.world.level.chunk.DataLayer; 18 | import net.minecraft.world.level.chunk.LevelChunk; 19 | import net.minecraft.world.level.chunk.LightChunkGetter; 20 | import net.minecraft.world.level.lighting.LayerLightEventListener; 21 | import net.minecraft.world.level.lighting.LevelLightEngine; 22 | import net.minecraft.world.level.lighting.LightEngine; 23 | import net.minecraft.world.level.lighting.LightEventListener; 24 | import org.jetbrains.annotations.Nullable; 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.Unique; 29 | import org.spongepowered.asm.mixin.injection.At; 30 | import org.spongepowered.asm.mixin.injection.Inject; 31 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 32 | 33 | @Mixin(LevelLightEngine.class) 34 | public abstract class LevelLightEngineMixin implements LightEventListener, StarLightLightingProvider { 35 | 36 | @Shadow 37 | @Nullable 38 | private LightEngine blockEngine; 39 | 40 | @Shadow 41 | @Nullable 42 | private LightEngine skyEngine; 43 | 44 | @Unique 45 | protected StarLightInterface lightEngine; 46 | 47 | @Override 48 | public final StarLightInterface getLightEngine() { 49 | return this.lightEngine; 50 | } 51 | 52 | /** 53 | * 54 | * TODO since this is a constructor inject, check on update for new constructors 55 | */ 56 | @Inject( 57 | method = "", at = @At("TAIL") 58 | ) 59 | public void construct(final LightChunkGetter chunkProvider, final boolean hasBlockLight, final boolean hasSkyLight, 60 | final CallbackInfo ci) { 61 | // avoid ClassCastException in cases where custom LightChunkGetters do not return a Level from getLevel() 62 | if (chunkProvider.getLevel() instanceof Level) { 63 | this.lightEngine = new StarLightInterface(chunkProvider, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this); 64 | } else { 65 | this.lightEngine = new StarLightInterface(null, hasSkyLight, hasBlockLight, (LevelLightEngine)(Object)this); 66 | } 67 | // intentionally destroy mods hooking into old light engine state 68 | this.blockEngine = null; 69 | this.skyEngine = null; 70 | } 71 | 72 | /** 73 | * @reason Route to new light engine 74 | * @author Spottedleaf 75 | */ 76 | @Overwrite 77 | public void checkBlock(final BlockPos pos) { 78 | this.lightEngine.blockChange(pos.immutable()); 79 | } 80 | 81 | /** 82 | * @reason Route to new light engine 83 | * @author Spottedleaf 84 | */ 85 | @Overwrite 86 | public boolean hasLightWork() { 87 | // route to new light engine 88 | return this.lightEngine.hasUpdates(); 89 | } 90 | 91 | /** 92 | * @reason Hook into new light engine for light updates 93 | * @author Spottedleaf 94 | */ 95 | @Overwrite 96 | public int runLightUpdates() { 97 | // replace impl 98 | final boolean hadUpdates = this.hasLightWork(); 99 | this.lightEngine.propagateChanges(); 100 | return hadUpdates ? 1 : 0; 101 | } 102 | 103 | /** 104 | * @reason New light engine hook for handling empty section changes 105 | * @author Spottedleaf 106 | */ 107 | @Overwrite 108 | public void updateSectionStatus(final SectionPos pos, final boolean notReady) { 109 | this.lightEngine.sectionChange(pos, notReady); 110 | } 111 | 112 | /** 113 | * @reason Avoid messing with the vanilla light engine state 114 | * @author Spottedleaf 115 | */ 116 | @Overwrite 117 | public void setLightEnabled(final ChunkPos pos, final boolean lightEnabled) { 118 | // not invoked by the client 119 | } 120 | 121 | /** 122 | * @reason Avoid messing with the vanilla light engine state 123 | * @author Spottedleaf 124 | */ 125 | @Overwrite 126 | public void propagateLightSources(ChunkPos param0) { 127 | // not invoked by the client 128 | } 129 | 130 | /** 131 | * @reason Replace light views with our own that hook into the new light engine instead of vanilla's 132 | * @author Spottedleaf 133 | */ 134 | @Overwrite 135 | public LayerLightEventListener getLayerListener(final LightLayer lightType) { 136 | return lightType == LightLayer.BLOCK ? this.lightEngine.getBlockReader() : this.lightEngine.getSkyReader(); 137 | } 138 | 139 | /** 140 | * @reason Avoid messing with the vanilla light engine state 141 | * @author Spottedleaf 142 | */ 143 | @Overwrite 144 | public void queueSectionData(final LightLayer lightType, final SectionPos pos, @Nullable final DataLayer nibble) { 145 | // do not allow modification of data from the non-chunk load hooks 146 | } 147 | 148 | /** 149 | * @reason Avoid messing with the vanilla light engine state 150 | * @author Spottedleaf 151 | */ 152 | @Overwrite 153 | public String getDebugData(final LightLayer lightType, final SectionPos pos) { 154 | // TODO would be nice to make use of this 155 | return "n/a"; 156 | } 157 | 158 | /** 159 | * @reason Avoid messing with the vanilla light engine state 160 | * @author Spottedleaf 161 | */ 162 | @Overwrite 163 | public void retainData(final ChunkPos pos, final boolean retainData) { 164 | // not used by new light impl 165 | } 166 | 167 | /** 168 | * @reason Need to use our own hooks for retrieving light data 169 | * @author Spottedleaf 170 | */ 171 | @Overwrite 172 | public int getRawBrightness(final BlockPos pos, final int ambientDarkness) { 173 | // need to use new light hooks for this 174 | return this.lightEngine.getRawBrightness(pos, ambientDarkness); 175 | } 176 | 177 | /** 178 | * @reason Need to use our own hooks for checking this state 179 | * @author Spottedleaf 180 | */ 181 | @Overwrite 182 | public boolean lightOnInColumn(final long pos) { 183 | final long key = CoordinateUtils.getChunkKey(SectionPos.x(pos), SectionPos.z(pos)); 184 | return (!this.lightEngine.hasBlockLight() || this.blockLightMap.get(key) != null) && (!this.lightEngine.hasSkyLight() || this.skyLightMap.get(key) != null); 185 | } 186 | 187 | @Unique 188 | protected final Long2ObjectOpenHashMap blockLightMap = new Long2ObjectOpenHashMap<>(); 189 | 190 | @Unique 191 | protected final Long2ObjectOpenHashMap skyLightMap = new Long2ObjectOpenHashMap<>(); 192 | 193 | @Override 194 | public void clientUpdateLight(final LightLayer lightType, final SectionPos pos, 195 | final DataLayer nibble, final boolean trustEdges) { 196 | if (((Object)this).getClass() != LevelLightEngine.class) { 197 | throw new IllegalStateException("This hook is for the CLIENT ONLY"); 198 | } 199 | // data storage changed with new light impl 200 | final ChunkAccess chunk = this.getLightEngine().getAnyChunkNow(pos.getX(), pos.getZ()); 201 | switch (lightType) { 202 | case BLOCK: { 203 | final SWMRNibbleArray[] blockNibbles = this.blockLightMap.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { 204 | return StarLightEngine.getFilledEmptyLight(this.lightEngine.getWorld()); 205 | }); 206 | 207 | blockNibbles[pos.getY() - WorldUtil.getMinLightSection(this.lightEngine.getWorld())] = SWMRNibbleArray.fromVanilla(nibble); 208 | 209 | if (chunk != null) { 210 | ((ExtendedChunk)chunk).setBlockNibbles(blockNibbles); 211 | this.lightEngine.getLightAccess().onLightUpdate(LightLayer.BLOCK, pos); 212 | } 213 | break; 214 | } 215 | case SKY: { 216 | final SWMRNibbleArray[] skyNibbles = this.skyLightMap.computeIfAbsent(CoordinateUtils.getChunkKey(pos), (final long keyInMap) -> { 217 | return StarLightEngine.getFilledEmptyLight(this.lightEngine.getWorld()); 218 | }); 219 | 220 | skyNibbles[pos.getY() - WorldUtil.getMinLightSection(this.lightEngine.getWorld())] = SWMRNibbleArray.fromVanilla(nibble); 221 | 222 | if (chunk != null) { 223 | ((ExtendedChunk)chunk).setSkyNibbles(skyNibbles); 224 | this.lightEngine.getLightAccess().onLightUpdate(LightLayer.SKY, pos); 225 | } 226 | break; 227 | } 228 | } 229 | } 230 | 231 | @Override 232 | public void clientRemoveLightData(final ChunkPos chunkPos) { 233 | if (((Object)this).getClass() != LevelLightEngine.class) { 234 | throw new IllegalStateException("This hook is for the CLIENT ONLY"); 235 | } 236 | this.blockLightMap.remove(CoordinateUtils.getChunkKey(chunkPos)); 237 | this.skyLightMap.remove(CoordinateUtils.getChunkKey(chunkPos)); 238 | } 239 | 240 | @Override 241 | public void clientChunkLoad(final ChunkPos pos, final LevelChunk chunk) { 242 | if (((Object)this).getClass() != LevelLightEngine.class) { 243 | throw new IllegalStateException("This hook is for the CLIENT ONLY"); 244 | } 245 | final long key = CoordinateUtils.getChunkKey(pos); 246 | final SWMRNibbleArray[] blockNibbles = this.blockLightMap.get(key); 247 | final SWMRNibbleArray[] skyNibbles = this.skyLightMap.get(key); 248 | if (blockNibbles != null) { 249 | ((ExtendedChunk)chunk).setBlockNibbles(blockNibbles); 250 | } 251 | if (skyNibbles != null) { 252 | ((ExtendedChunk)chunk).setSkyNibbles(skyNibbles); 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/lightengine/ThreadedLevelLightEngineMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.lightengine; 2 | 3 | import ca.spottedleaf.starlight.common.light.StarLightEngine; 4 | import ca.spottedleaf.starlight.common.light.StarLightInterface; 5 | import ca.spottedleaf.starlight.common.light.StarLightLightingProvider; 6 | import ca.spottedleaf.starlight.common.thread.GlobalExecutors; 7 | import ca.spottedleaf.starlight.common.util.CoordinateUtils; 8 | import com.llamalad7.mixinextras.injector.wrapoperation.Operation; 9 | import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; 10 | import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; 11 | import net.minecraft.core.BlockPos; 12 | import net.minecraft.core.SectionPos; 13 | import net.minecraft.server.level.ChunkMap; 14 | import net.minecraft.server.level.ServerLevel; 15 | import net.minecraft.server.level.ThreadedLevelLightEngine; 16 | import net.minecraft.world.level.ChunkPos; 17 | import net.minecraft.world.level.LightLayer; 18 | import net.minecraft.world.level.chunk.ChunkAccess; 19 | import net.minecraft.world.level.chunk.status.ChunkStatus; 20 | import net.minecraft.world.level.chunk.DataLayer; 21 | import net.minecraft.world.level.chunk.LightChunkGetter; 22 | import net.minecraft.world.level.lighting.LevelLightEngine; 23 | import org.jetbrains.annotations.Nullable; 24 | import org.slf4j.Logger; 25 | import org.spongepowered.asm.mixin.Final; 26 | import org.spongepowered.asm.mixin.Mixin; 27 | import org.spongepowered.asm.mixin.Overwrite; 28 | import org.spongepowered.asm.mixin.Shadow; 29 | import org.spongepowered.asm.mixin.Unique; 30 | import org.spongepowered.asm.mixin.injection.At; 31 | 32 | import java.util.concurrent.CompletableFuture; 33 | import java.util.concurrent.atomic.AtomicLong; 34 | import java.util.function.Supplier; 35 | 36 | @Mixin(ThreadedLevelLightEngine.class) 37 | public abstract class ThreadedLevelLightEngineMixin extends LevelLightEngine implements StarLightLightingProvider { 38 | 39 | @Final 40 | @Shadow 41 | private ChunkMap chunkMap; 42 | 43 | @Final 44 | @Shadow 45 | private static Logger LOGGER; 46 | 47 | @Shadow 48 | public abstract void tryScheduleUpdate(); 49 | 50 | public ThreadedLevelLightEngineMixin(final LightChunkGetter chunkProvider, final boolean hasBlockLight, final boolean hasSkyLight) { 51 | super(chunkProvider, hasBlockLight, hasSkyLight); 52 | } 53 | 54 | @Unique 55 | private final Long2IntOpenHashMap chunksBeingWorkedOn = new Long2IntOpenHashMap(); 56 | 57 | @Unique 58 | private void queueTaskForSection(final int chunkX, final int chunkY, final int chunkZ, 59 | final Supplier runnable) { 60 | final ServerLevel world = (ServerLevel)this.getLightEngine().getWorld(); 61 | 62 | final ChunkAccess center = this.getLightEngine().getAnyChunkNow(chunkX, chunkZ); 63 | if (center == null || !center.getPersistedStatus().isOrAfter(ChunkStatus.LIGHT)) { 64 | // do not accept updates in unlit chunks, unless we might be generating a chunk. thanks to the amazing 65 | // chunk scheduling, we could be lighting and generating a chunk at the same time 66 | return; 67 | } 68 | 69 | if (center.getPersistedStatus() != ChunkStatus.FULL) { // TODO check if getHighestGeneratedStatus() is a better idea 70 | // do not keep chunk loaded, we are probably in a gen thread 71 | // if we proceed to add a ticket the chunk will be loaded, which is not what we want (avoid cascading gen) 72 | runnable.get(); 73 | return; 74 | } 75 | 76 | if (!world.getChunkSource().chunkMap.mainThreadExecutor.isSameThread()) { 77 | // ticket logic is not safe to run off-main, re-schedule 78 | world.getChunkSource().chunkMap.mainThreadExecutor.execute(() -> { 79 | this.queueTaskForSection(chunkX, chunkY, chunkZ, runnable); 80 | }); 81 | return; 82 | } 83 | 84 | final long key = CoordinateUtils.getChunkKey(chunkX, chunkZ); 85 | 86 | final StarLightInterface.LightQueue.ChunkTasks updateFuture = runnable.get(); 87 | 88 | if (updateFuture == null) { 89 | // not scheduled 90 | return; 91 | } 92 | 93 | if (updateFuture.isTicketAdded) { 94 | // ticket already added 95 | return; 96 | } 97 | updateFuture.isTicketAdded = true; 98 | 99 | final int references = this.chunksBeingWorkedOn.addTo(key, 1); 100 | if (references == 0) { 101 | final ChunkPos pos = new ChunkPos(chunkX, chunkZ); 102 | world.getChunkSource().addRegionTicket(StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); 103 | } 104 | 105 | updateFuture.onComplete.thenAcceptAsync((final Void ignore) -> { 106 | final int newReferences = this.chunksBeingWorkedOn.get(key); 107 | if (newReferences == 1) { 108 | this.chunksBeingWorkedOn.remove(key); 109 | final ChunkPos pos = new ChunkPos(chunkX, chunkZ); 110 | world.getChunkSource().removeRegionTicket(StarLightInterface.CHUNK_WORK_TICKET, pos, 0, pos); 111 | } else { 112 | this.chunksBeingWorkedOn.put(key, newReferences - 1); 113 | } 114 | }, world.getChunkSource().chunkMap.mainThreadExecutor).whenComplete((final Void ignore, final Throwable thr) -> { 115 | if (thr != null) { 116 | LOGGER.error("Failed to remove ticket level for post chunk task " + new ChunkPos(chunkX, chunkZ), thr); 117 | } 118 | }); 119 | } 120 | 121 | /** 122 | * @reason Redirect scheduling call away from the vanilla light engine, as well as enforce 123 | * that chunk neighbours are loaded before the processing can occur 124 | * @author Spottedleaf 125 | */ 126 | @Overwrite 127 | public void checkBlock(final BlockPos pos) { 128 | final BlockPos posCopy = pos.immutable(); 129 | this.queueTaskForSection(posCopy.getX() >> 4, posCopy.getY() >> 4, posCopy.getZ() >> 4, () -> { 130 | return this.getLightEngine().blockChange(posCopy); 131 | }); 132 | } 133 | 134 | /** 135 | * @reason Avoid messing with the vanilla light engine state 136 | * @author Spottedleaf 137 | */ 138 | @Overwrite 139 | public void updateChunkStatus(final ChunkPos pos) {} 140 | 141 | /** 142 | * @reason Redirect to schedule for our own logic, as well as ensure 1 radius neighbours 143 | * are loaded 144 | * Note: Our scheduling logic will discard this call if the chunk is not lit, unloaded, or not at LIGHT stage yet. 145 | * @author Spottedleaf 146 | */ 147 | @Overwrite 148 | public void updateSectionStatus(final SectionPos pos, final boolean notReady) { 149 | this.queueTaskForSection(pos.getX(), pos.getY(), pos.getZ(), () -> { 150 | return this.getLightEngine().sectionChange(pos, notReady); 151 | }); 152 | } 153 | 154 | /** 155 | * @reason Avoid messing with the vanilla light engine state 156 | * @author Spottedleaf 157 | */ 158 | @Overwrite 159 | public void propagateLightSources(final ChunkPos pos) { 160 | // handled by light() 161 | } 162 | 163 | /** 164 | * @reason Avoid messing with the vanilla light engine state 165 | * @author Spottedleaf 166 | */ 167 | @Overwrite 168 | public void setLightEnabled(final ChunkPos pos, final boolean lightEnabled) { 169 | // light impl does not need to do this 170 | } 171 | 172 | /** 173 | * @reason Light data is now attached to chunks, and this means we need to hook into chunk loading logic 174 | * to load the data rather than rely on this call. This call also would mess with the vanilla light engine state. 175 | * @author Spottedleaf 176 | */ 177 | @Overwrite 178 | public void queueSectionData(final LightLayer lightType, final SectionPos pos, final @Nullable DataLayer nibbles) { 179 | // load hooks inside ChunkSerializer 180 | } 181 | 182 | /** 183 | * @reason Avoid messing with the vanilla light engine state 184 | * @author Spottedleaf 185 | */ 186 | @Overwrite 187 | public void retainData(final ChunkPos pos, final boolean retainData) { 188 | // light impl does not need to do this 189 | } 190 | 191 | /** 192 | * @reason Starlight does not have to do this 193 | * @author Spottedleaf 194 | */ 195 | @Overwrite 196 | public CompletableFuture initializeLight(final ChunkAccess chunk, final boolean lit) { 197 | return CompletableFuture.completedFuture(chunk); 198 | } 199 | 200 | /** 201 | * @reason Route to new logic to either light or just load the data 202 | * @author Spottedleaf 203 | */ 204 | @Overwrite 205 | public CompletableFuture lightChunk(final ChunkAccess chunk, final boolean lit) { 206 | final ChunkPos chunkPos = chunk.getPos(); 207 | 208 | return CompletableFuture.supplyAsync(() -> { 209 | final Boolean[] emptySections = StarLightEngine.getEmptySectionsForChunk(chunk); 210 | if (!lit) { 211 | chunk.setLightCorrect(false); 212 | this.getLightEngine().lightChunk(chunk, emptySections); 213 | chunk.setLightCorrect(true); 214 | } else { 215 | this.getLightEngine().forceLoadInChunk(chunk, emptySections); 216 | // can't really force the chunk to be edged checked, as we need neighbouring chunks - but we don't have 217 | // them, so if it's not loaded then i guess we can't do edge checks. later loads of the chunk should 218 | // catch what we miss here. 219 | this.getLightEngine().checkChunkEdges(chunkPos.x, chunkPos.z); 220 | } 221 | 222 | // this.chunkMap.releaseLightTicket(chunkPos); // vanilla 1.21 no longer does this 223 | return chunk; 224 | }, (runnable) -> { 225 | this.getLightEngine().scheduleChunkLight(chunkPos, runnable); 226 | this.tryScheduleUpdate(); 227 | }).whenComplete((final ChunkAccess c, final Throwable throwable) -> { 228 | if (throwable != null) { 229 | LOGGER.error("Failed to light chunk " + chunkPos, throwable); 230 | } 231 | }); 232 | } 233 | 234 | @Unique 235 | private final AtomicLong scalablelux$lastLightUpdate = new AtomicLong(0); 236 | 237 | @WrapOperation(method = "tryScheduleUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/lighting/LevelLightEngine;hasLightWork()Z")) 238 | private boolean scheduleOnlyWhenDirty(ThreadedLevelLightEngine instance, Operation original) { 239 | if (!GlobalExecutors.ENABLED) { 240 | return original.call(instance); 241 | } 242 | final boolean queueDirty = ((StarLightLightingProvider) instance).getLightEngine().isQueueDirty(); 243 | if (queueDirty) { 244 | return original.call(instance); 245 | } 246 | final long lastUpdate = this.scalablelux$lastLightUpdate.get(); 247 | final long currentTime = System.nanoTime(); 248 | if (currentTime - lastUpdate >= 10_000_000L) { // 10ms 249 | if (this.scalablelux$lastLightUpdate.compareAndSet(lastUpdate, currentTime)) { 250 | return original.call(instance); 251 | } 252 | } 253 | return false; 254 | } 255 | 256 | /** 257 | * @author ishland 258 | * @reason implement waitForPendingTasks hook 259 | */ 260 | @Overwrite 261 | public CompletableFuture waitForPendingTasks(int x, int z) { 262 | return this.getLightEngine().syncFuture(x, z); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/world/LevelMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.world; 2 | 3 | import ca.spottedleaf.starlight.common.world.ExtendedWorld; 4 | import net.minecraft.world.level.Level; 5 | import net.minecraft.world.level.LevelAccessor; 6 | import net.minecraft.world.level.chunk.ChunkAccess; 7 | import net.minecraft.world.level.chunk.status.ChunkStatus; 8 | import net.minecraft.world.level.chunk.LevelChunk; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | 11 | @Mixin(Level.class) 12 | public abstract class LevelMixin implements LevelAccessor, AutoCloseable, ExtendedWorld { 13 | 14 | @Override 15 | public LevelChunk getChunkAtImmediately(final int chunkX, final int chunkZ) { 16 | return this.getChunkSource().getChunk(chunkX, chunkZ, false); 17 | } 18 | 19 | @Override 20 | public ChunkAccess getAnyChunkImmediately(final int chunkX, final int chunkZ) { 21 | return this.getChunkSource().getChunk(chunkX, chunkX, ChunkStatus.EMPTY, false); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/world/SerializableChunkDataMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.world; 2 | 3 | import ca.spottedleaf.starlight.common.config.Config; 4 | import ca.spottedleaf.starlight.common.light.SWMRNibbleArray; 5 | import ca.spottedleaf.starlight.common.util.SaveUtil; 6 | import ca.spottedleaf.starlight.common.world.ExtendedSerializableChunkData; 7 | import net.minecraft.core.RegistryAccess; 8 | import net.minecraft.nbt.CompoundTag; 9 | import net.minecraft.server.level.ServerLevel; 10 | import net.minecraft.world.entity.ai.village.poi.PoiManager; 11 | import net.minecraft.world.level.ChunkPos; 12 | import net.minecraft.world.level.LevelHeightAccessor; 13 | import net.minecraft.world.level.chunk.ChunkAccess; 14 | import net.minecraft.world.level.chunk.ProtoChunk; 15 | import net.minecraft.world.level.chunk.storage.RegionStorageInfo; 16 | import net.minecraft.world.level.chunk.storage.SerializableChunkData; 17 | import org.spongepowered.asm.mixin.*; 18 | import org.spongepowered.asm.mixin.injection.At; 19 | import org.spongepowered.asm.mixin.injection.Inject; 20 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 21 | 22 | @Mixin(SerializableChunkData.class) 23 | public abstract class SerializableChunkDataMixin implements ExtendedSerializableChunkData { 24 | 25 | @Mutable 26 | @Shadow 27 | @Final 28 | private boolean lightCorrect; 29 | @Unique 30 | private SWMRNibbleArray.SaveState[] scalablelux$blocklight; 31 | @Unique 32 | private SWMRNibbleArray.SaveState[] scalablelux$skylight; 33 | @Unique 34 | private boolean scalablelux$actuallyCorrect; 35 | 36 | @Override 37 | public void scalablelux$setBlockLight(SWMRNibbleArray.SaveState[] light) { 38 | this.scalablelux$blocklight = light; 39 | } 40 | 41 | @Override 42 | public void scalablelux$setSkyLight(SWMRNibbleArray.SaveState[] light) { 43 | this.scalablelux$skylight = light; 44 | } 45 | 46 | @Override 47 | public void scalableLux$setActuallyCorrect(boolean correct) { 48 | this.scalablelux$actuallyCorrect = correct; 49 | } 50 | 51 | @Override 52 | public SWMRNibbleArray.SaveState[] scalablelux$getBlockLight() { 53 | return this.scalablelux$blocklight; 54 | } 55 | 56 | @Override 57 | public SWMRNibbleArray.SaveState[] scalablelux$getSkyLight() { 58 | return this.scalablelux$skylight; 59 | } 60 | 61 | @Override 62 | public boolean scalablelux$getActuallyCorrect() { 63 | return this.scalablelux$actuallyCorrect; 64 | } 65 | 66 | @Override 67 | public void scalablelux$setLightCorrect(boolean correct) { 68 | this.lightCorrect = correct; 69 | } 70 | 71 | /** 72 | * Overwrites vanilla's light data with our own. 73 | * TODO this needs to be checked on update to account for format changes 74 | */ 75 | @Inject( 76 | method = "copyOf", 77 | at = @At("RETURN") 78 | ) 79 | private static void prepareSaveLightHook(ServerLevel world, ChunkAccess chunk, CallbackInfoReturnable cir) { 80 | if (Config.USE_STARLIGHT_FORMAT) { 81 | SaveUtil.prepareSaveLightHook(chunk, cir.getReturnValue()); 82 | } else { 83 | SaveUtil.prepareSaveVanillaLightHook(world, chunk, cir.getReturnValue()); 84 | } 85 | } 86 | 87 | @Inject( 88 | method = "write", 89 | at = @At("RETURN") 90 | ) 91 | private void saveLightHook(CallbackInfoReturnable cir) { 92 | if (Config.USE_STARLIGHT_FORMAT) { 93 | SaveUtil.saveLightHook((SerializableChunkData) (Object) this, cir.getReturnValue()); 94 | } 95 | } 96 | 97 | @Inject( 98 | method = "parse", 99 | at = @At("RETURN") 100 | ) 101 | private static void prepareLoadLightHook(LevelHeightAccessor levelHeightAccessor, RegistryAccess registryAccess, CompoundTag compoundTag, CallbackInfoReturnable cir) { 102 | if (Config.USE_STARLIGHT_FORMAT) { 103 | SaveUtil.prepareLoadLightHook(levelHeightAccessor, compoundTag, cir.getReturnValue()); 104 | } 105 | } 106 | 107 | /** 108 | * Loads our light data into the returned chunk object from the tag. 109 | * TODO this needs to be checked on update to account for format changes 110 | */ 111 | @Inject( 112 | method = "read", 113 | at = @At("RETURN") 114 | ) 115 | private void loadLightHook(ServerLevel serverLevel, PoiManager poiManager, RegionStorageInfo regionStorageInfo, ChunkPos chunkPos, CallbackInfoReturnable cir) { 116 | if (Config.USE_STARLIGHT_FORMAT) { 117 | SaveUtil.loadLightHook(serverLevel, (SerializableChunkData) (Object) this, cir.getReturnValue()); 118 | } else { 119 | SaveUtil.loadVanillaLightHook(serverLevel, (SerializableChunkData) (Object) this, cir.getReturnValue()); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/world/ServerWorldMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.world; 2 | 3 | import ca.spottedleaf.starlight.common.util.CoordinateUtils; 4 | import ca.spottedleaf.starlight.common.world.ExtendedWorld; 5 | import net.minecraft.core.Holder; 6 | import net.minecraft.core.RegistryAccess; 7 | import net.minecraft.resources.ResourceKey; 8 | import net.minecraft.server.level.ChunkHolder; 9 | import net.minecraft.server.level.ChunkMap; 10 | import net.minecraft.server.level.ChunkResult; 11 | import net.minecraft.server.level.ServerChunkCache; 12 | import net.minecraft.server.level.ServerLevel; 13 | import net.minecraft.util.profiling.ProfilerFiller; 14 | import net.minecraft.world.level.Level; 15 | import net.minecraft.world.level.WorldGenLevel; 16 | import net.minecraft.world.level.chunk.ChunkAccess; 17 | import net.minecraft.world.level.chunk.LevelChunk; 18 | import net.minecraft.world.level.dimension.DimensionType; 19 | import net.minecraft.world.level.storage.WritableLevelData; 20 | import org.spongepowered.asm.mixin.Final; 21 | import org.spongepowered.asm.mixin.Mixin; 22 | import org.spongepowered.asm.mixin.Shadow; 23 | import java.util.function.Supplier; 24 | 25 | @Mixin(ServerLevel.class) 26 | public abstract class ServerWorldMixin extends Level implements WorldGenLevel, ExtendedWorld { 27 | 28 | @Shadow 29 | @Final 30 | private ServerChunkCache chunkSource; 31 | 32 | protected ServerWorldMixin(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, boolean bl, boolean bl2, long l, int i) { 33 | super(writableLevelData, resourceKey, registryAccess, holder, bl, bl2, l, i); 34 | } 35 | 36 | @Override 37 | public final LevelChunk getChunkAtImmediately(final int chunkX, final int chunkZ) { 38 | final ChunkMap storage = this.chunkSource.chunkMap; 39 | final ChunkHolder holder = storage.getVisibleChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ)); 40 | 41 | if (holder == null) { 42 | return null; 43 | } 44 | 45 | final ChunkResult result = holder.getFullChunkFuture().getNow(null); 46 | 47 | return result == null ? null : result.orElse(null); 48 | } 49 | 50 | @Override 51 | public final ChunkAccess getAnyChunkImmediately(final int chunkX, final int chunkZ) { 52 | final ChunkMap storage = this.chunkSource.chunkMap; 53 | final ChunkHolder holder = storage.getVisibleChunkIfPresent(CoordinateUtils.getChunkKey(chunkX, chunkZ)); 54 | 55 | return holder == null ? null : holder.getLatestChunk(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/ca/spottedleaf/starlight/mixin/common/world/WorldGenRegionMixin.java: -------------------------------------------------------------------------------- 1 | package ca.spottedleaf.starlight.mixin.common.world; 2 | 3 | import net.minecraft.core.BlockPos; 4 | import net.minecraft.server.level.WorldGenRegion; 5 | import net.minecraft.world.level.LightLayer; 6 | import net.minecraft.world.level.WorldGenLevel; 7 | import net.minecraft.world.level.chunk.ChunkAccess; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.Shadow; 10 | 11 | @Mixin(WorldGenRegion.class) 12 | public abstract class WorldGenRegionMixin implements WorldGenLevel { 13 | 14 | @Shadow 15 | public abstract ChunkAccess getChunk(int i, int j); 16 | 17 | /** 18 | * @reason During feature generation, light data is not initialised and will always return 15 in Starlight. Vanilla 19 | * can possibly return 0 if partially initialised, which allows some mushroom blocks to generate. 20 | * In general, the brightness value from the light engine should not be used until the chunk is ready. To emulate 21 | * Vanilla behavior better, we return 0 as the brightness during world gen unless the target chunk is finished 22 | * lighting. 23 | * @author Spottedleaf 24 | */ 25 | @Override 26 | public int getBrightness(final LightLayer lightLayer, final BlockPos blockPos) { 27 | final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4); 28 | if (!chunk.isLightCorrect()) { 29 | return 0; 30 | } 31 | return this.getLightEngine().getLayerListener(lightLayer).getLightValue(blockPos); 32 | } 33 | 34 | /** 35 | * @reason See above 36 | * @author Spottedleaf 37 | */ 38 | @Override 39 | public int getRawBrightness(final BlockPos blockPos, final int subtract) { 40 | final ChunkAccess chunk = this.getChunk(blockPos.getX() >> 4, blockPos.getZ() >> 4); 41 | if (!chunk.isLightCorrect()) { 42 | return 0; 43 | } 44 | return this.getLightEngine().getRawBrightness(blockPos, subtract); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/resources/assets/scalablelux/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RelativityMC/ScalableLux/c6ec02104ec2104fbb3c99f9782641aa3b5c6515/src/main/resources/assets/scalablelux/icon.png -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "scalablelux", 4 | "version": "${version}", 5 | "name": "ScalableLux", 6 | "description": "Rewrites the light engine to fix lighting performance and lighting errors", 7 | "authors": [ 8 | "Spottedleaf", 9 | "ishland" 10 | ], 11 | "contact": { 12 | "issues": "https://github.com/RelativityMC/ScalableLux/issues", 13 | "sources": "https://github.com/RelativityMC/ScalableLux" 14 | }, 15 | "provides": [ 16 | "starlight" 17 | ], 18 | "breaks": { 19 | "phosphor" : "*" 20 | }, 21 | "license": "LGPL-3.0-only", 22 | "icon": "assets/scalablelux/icon.png", 23 | "environment": "*", 24 | "entrypoints": { 25 | "main": [ 26 | "ca.spottedleaf.starlight.common.ScalableLuxEntrypoint" 27 | ] 28 | }, 29 | "mixins": [ 30 | "scalablelux.mixins.json" 31 | ], 32 | "accessWidener" : "scalablelux.accesswidener", 33 | "depends": { 34 | "fabricloader": ">=${loader_version}", 35 | "minecraft": ">1.21.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/scalablelux.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v1 named 2 | 3 | # BlockStateBase.ShapeCache 4 | accessible class net/minecraft/world/level/block/state/BlockBehaviour$BlockStateBase$Cache 5 | #accessible field net/minecraft/world/level/block/state/BlockBehaviour$BlockStateBase$Cache lightBlock I 6 | 7 | 8 | # LevelChunkSection 9 | accessible field net/minecraft/world/level/chunk/LevelChunkSection states Lnet/minecraft/world/level/chunk/PalettedContainer; 10 | 11 | 12 | # PalettedContainer 13 | accessible method net/minecraft/world/level/chunk/PalettedContainer get (I)Ljava/lang/Object; 14 | 15 | 16 | # ChunkMap 17 | accessible field net/minecraft/server/level/ChunkMap level Lnet/minecraft/server/level/ServerLevel; 18 | accessible field net/minecraft/server/level/ChunkMap mainThreadExecutor Lnet/minecraft/util/thread/BlockableEventLoop; 19 | 20 | accessible method net/minecraft/server/level/ChunkMap getUpdatingChunkIfPresent (J)Lnet/minecraft/server/level/ChunkHolder; 21 | accessible method net/minecraft/server/level/ChunkMap getVisibleChunkIfPresent (J)Lnet/minecraft/server/level/ChunkHolder; 22 | accessible method net/minecraft/server/level/ChunkMap getChunkQueueLevel (J)Ljava/util/function/IntSupplier; 23 | 24 | # LevelLightEngine 25 | mutable field net/minecraft/world/level/lighting/LevelLightEngine blockEngine Lnet/minecraft/world/level/lighting/LightEngine; 26 | mutable field net/minecraft/world/level/lighting/LevelLightEngine skyEngine Lnet/minecraft/world/level/lighting/LightEngine; 27 | 28 | # ThreadedLevelLightEngine 29 | accessible class net/minecraft/server/level/ThreadedLevelLightEngine$TaskType 30 | -------------------------------------------------------------------------------- /src/main/resources/scalablelux.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "ca.spottedleaf.starlight.mixin", 5 | "compatibilityLevel": "JAVA_17", 6 | "mixins": [ 7 | "common.blockstate.BlockStateBaseMixin", 8 | "common.chunk.ChunkAccessMixin", 9 | "common.chunk.EmptyLevelChunkMixin", 10 | "common.chunk.ImposterProtoChunkMixin", 11 | "common.chunk.LevelChunkMixin", 12 | "common.chunk.ProtoChunkMixin", 13 | "common.lightengine.LevelLightEngineMixin", 14 | "common.lightengine.ThreadedLevelLightEngineMixin", 15 | "common.world.SerializableChunkDataMixin", 16 | "common.world.LevelMixin", 17 | "common.world.ServerWorldMixin", 18 | "common.world.WorldGenRegionMixin" 19 | ], 20 | "client": [ 21 | "client.multiplayer.ClientPacketListenerMixin", 22 | "client.world.ClientLevelMixin" 23 | ], 24 | "injectors": { 25 | "defaultRequire": 1 26 | } 27 | } 28 | --------------------------------------------------------------------------------