├── .gitattributes ├── .github └── workflows │ ├── release.yml │ └── test-build.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── docs ├── img1.png ├── img2.png ├── img3.png ├── img4.png └── img5.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── org │ └── teacon │ └── slides │ ├── ModClientRegistries.java │ ├── ModRegistries.java │ ├── SlideShow.java │ ├── admin │ ├── SlideCommand.java │ └── SlidePermission.java │ ├── block │ ├── ProjectorBlock.java │ └── ProjectorBlockEntity.java │ ├── cache │ ├── CacheStorage.java │ ├── FilenameAllocation.java │ ├── ImageCache.java │ └── ResourceReference.java │ ├── calc │ ├── CalcBasic.java │ └── CalcMicros.java │ ├── inventory │ ├── ProjectorContainerMenu.java │ └── SlideItemContainerMenu.java │ ├── item │ ├── ProjectorItem.java │ └── SlideItem.java │ ├── network │ ├── ProjectorUpdatePacket.java │ ├── SlideItemUpdatePacket.java │ ├── SlideSummaryPacket.java │ ├── SlideURLPrefetchPacket.java │ └── SlideURLRequestPacket.java │ ├── renderer │ ├── ProjectorRenderer.java │ ├── SlideRenderType.java │ └── SlideState.java │ ├── screen │ ├── LazyWidget.java │ ├── ProjectorScreen.java │ └── SlideItemScreen.java │ ├── slide │ ├── IconSlide.java │ ├── ImageSlide.java │ └── Slide.java │ ├── texture │ ├── AnimatedTextureProvider.java │ ├── GIFDecoder.java │ ├── LZWDecoder.java │ ├── StaticTextureProvider.java │ ├── TextureProvider.java │ └── WebPDecoder.java │ └── url │ ├── ProjectorURL.java │ ├── ProjectorURLArgument.java │ ├── ProjectorURLPatternArgument.java │ └── ProjectorURLSavedData.java └── resources ├── META-INF ├── accesstransformer.cfg └── neoforge.mods.toml ├── assets └── slide_show │ ├── blockstates │ └── projector.json │ ├── lang │ ├── en_us.json │ ├── zh_cn.json │ └── zh_tw.json │ ├── models │ ├── block │ │ ├── projector.json │ │ ├── projector_down.json │ │ ├── projector_inverted.json │ │ └── projector_up.json │ └── item │ │ ├── projector.json │ │ ├── slide_item.json │ │ ├── slide_item_allowed.json │ │ └── slide_item_blocked.json │ ├── shaders │ ├── core │ │ ├── rendertype_palette_slide.fsh │ │ └── rendertype_palette_slide.json │ └── post │ │ └── projector_outline.json │ └── textures │ ├── block │ ├── projector_base.png │ └── projector_main.png │ ├── gui │ ├── projector_gui.png │ ├── slide_default.png │ ├── slide_icon_blocked.png │ ├── slide_icon_empty.png │ ├── slide_icon_failed.png │ └── slide_icon_loading.png │ └── item │ ├── slide_item.png │ ├── slide_item_allowed.png │ └── slide_item_blocked.png └── data └── slide_show ├── loot_table └── blocks │ └── projector.json └── tags └── item └── slide_items.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Disable autocrlf on generated files, they always generate with LF 2 | # Add any extra files or paths here to make git stop saying they 3 | # are changed when only line endings change. 4 | src/generated/**/.cache/cache text eol=lf 5 | src/generated/**/*.json text eol=lf 6 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "**" 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | with: 13 | # Fetch all history 14 | fetch-depth: 0 15 | - name: Setup Java 21 16 | uses: actions/setup-java@v2 17 | with: 18 | distribution: 'temurin' 19 | java-version: '21' 20 | - name: Maven Publish 21 | id: maven_publish 22 | env: 23 | ARCHIVE_URL: ${{ secrets.TEACON_ARCHIVE_URL }} 24 | ARCHIVE_ENDPOINT: ${{ secrets.TEACON_ARCHIVE_ENDPOINT }} 25 | ARCHIVE_ACCESS_KEY: ${{ secrets.TEACON_ARCHIVE_ACCESS_KEY }} 26 | ARCHIVE_SECRET_KEY: ${{ secrets.TEACON_ARCHIVE_SECRET_KEY }} 27 | run: ./gradlew -Dorg.gradle.s3.endpoint=$ARCHIVE_ENDPOINT publishReleasePublicationToTeaconRepository githubActionOutput 28 | - name: Generate Changelog 29 | id: changelog 30 | shell: bash 31 | env: 32 | CURRENT: ${{ github.ref }} 33 | # Special thanks to this post on Stack Overflow regarding change set between two tags: 34 | # https://stackoverflow.com/questions/12082981 35 | # Do note that actions/checkout will enter detach mode by default, so you won't have 36 | # access to HEAD ref. Use GitHub-Action-supplied `github.ref` instead. 37 | # Special thanks to this issue ticket regarding escaping newline: 38 | # https://github.com/actions/create-release/issues/25 39 | # We use Bash parameter expansion to do find-and-replace. 40 | # https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html 41 | # Also we cannot use git rev-list because it always prepend "commit " 42 | # See https://stackoverflow.com/questions/36927089/ 43 | run: | 44 | current_tag=${CURRENT/refs\/tags\//} 45 | last_tag=`git describe --tags --abbrev=0 "$current_tag"^ 2>/dev/null || echo` 46 | if [ last_tag ]; then 47 | changelog=`git log --pretty="format:%H: %s" ${last_tag}..$current_tag` 48 | else 49 | changelog=`git log --pretty="format:%H: %s"` 50 | fi 51 | changelog="${changelog//'%'/'%25'}" 52 | changelog="${changelog//$'\n'/' %0A'}" 53 | echo "::set-output name=value::Change set since ${last_tag:-the beginning}: %0A%0A$changelog" 54 | - name: GitHub Release 55 | id: create_release 56 | uses: actions/create-release@v1.1.1 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | tag_name: ${{ github.ref }} 61 | release_name: ${{ github.ref }} 62 | draft: false 63 | prerelease: false 64 | body: | 65 | ${{ steps.changelog.outputs.value }} 66 | - name: GitHub Release Artifact 67 | uses: actions/upload-release-asset@v1.0.2 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | upload_url: ${{ steps.create_release.outputs.upload_url }} 72 | asset_path: ${{ steps.maven_publish.outputs.artifact_path }} 73 | asset_name: ${{ steps.maven_publish.outputs.artifact_publish_name }} 74 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types 75 | asset_content_type: "application/java-archive" 76 | -------------------------------------------------------------------------------- /.github/workflows/test-build.yml: -------------------------------------------------------------------------------- 1 | name: test-build 2 | on: 3 | push: 4 | paths: 5 | - 'src/**' 6 | - 'build.gradle' 7 | jobs: 8 | build: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | - name: Setup Java 21 14 | uses: actions/setup-java@v2 15 | with: 16 | distribution: 'temurin' 17 | java-version: '21' 18 | - name: Get Short Identifier 19 | uses: benjlevesque/short-sha@v1.2 20 | id: short-sha 21 | - name: Build 22 | id: build 23 | env: 24 | VERSION_IDENTIFIER: SNAPSHOT+${{ steps.short-sha.outputs.sha }} 25 | run: ./gradlew build githubActionOutput --stacktrace 26 | - name: GitHub Action Artifact 27 | uses: actions/upload-artifact@v3 28 | with: 29 | name: ${{ steps.build.outputs.artifact_name }} 30 | path: ${{ steps.build.outputs.artifact_path }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # eclipse 2 | bin 3 | *.launch 4 | .settings 5 | .metadata 6 | .classpath 7 | .project 8 | 9 | # idea 10 | out 11 | *.ipr 12 | *.iws 13 | *.iml 14 | .idea 15 | 16 | # vscode 17 | .vscode 18 | 19 | # gradle 20 | build 21 | .gradle 22 | 23 | # other 24 | eclipse 25 | run 26 | runs 27 | run-data 28 | 29 | repo 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2021, TeaConMC members and contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the TeaConMC nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL TEACONMC MEMBERS AND CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SlideShow 2 | 3 | … is a Minecraft mod that adds a "Slide Show Projector" block. This block can project any online image into the world. 4 | 5 | ![Preview](./docs/img5.png) 6 | 7 | Simply right-click the block to open its GUI, take the slide item out, 8 | open it and paste the URL to the image, then put it back into the block, and it will work! 9 | There are also several options to adjust; hover your mouse to these icons to get familiar with these options. 10 | 11 | ![GUI](./docs/img1.png) 12 | 13 | ![GUI](./docs/img2.png) 14 | 15 | ![GUI](./docs/img3.png) 16 | 17 | ![GUI](./docs/img4.png) 18 | 19 | By now, only PNG/JPG/GIF format images are verified as supported (including animations); 20 | other image formats may or may not be supported, and further testing are still needed. 21 | 22 | ## Development 23 | 24 | Simply import this project as a Gradle project to your IDE(s), and wait for your IDE(s) to finish their work. 25 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'eclipse' 4 | id 'idea' 5 | id 'maven-publish' 6 | id 'net.neoforged.moddev' version '1.0.15' 7 | } 8 | 9 | // Added by TeaCon 10 | abstract class TeaConDumpPathToGitHub extends DefaultTask { 11 | @Input 12 | abstract Property getPublishName() 13 | @InputFile 14 | abstract RegularFileProperty getTargetFile() 15 | @TaskAction 16 | void dump() { 17 | if (System.env.GITHUB_ACTIONS) { 18 | File theFile = targetFile.getAsFile().get() 19 | 20 | def outputFile = new File(System.env.GITHUB_OUTPUT) 21 | // Use the env-specific line separator for maximally possible compatibility 22 | def newLine = System.getProperty('line.separator') 23 | 24 | // Write out new env variable for later usage 25 | outputFile << newLine << "artifact_name=${theFile.getName()}" 26 | outputFile << newLine << "artifact_publish_name=${publishName.get()}" 27 | outputFile << newLine << "artifact_path=${theFile.absolutePath}" 28 | } 29 | } 30 | } 31 | 32 | tasks.named('wrapper', Wrapper).configure { 33 | // Define wrapper values here so as to not have to always do so when updating gradlew.properties. 34 | // Switching this to Wrapper.DistributionType.ALL will download the full gradle sources that comes with 35 | // documentation attached on cursor hover of gradle classes and methods. However, this comes with increased 36 | // file size for Gradle. If you do switch this to ALL, run the Gradle wrapper task twice afterwards. 37 | // (Verify by checking gradle/wrapper/gradle-wrapper.properties to see if distributionUrl now points to `-all`) 38 | distributionType = Wrapper.DistributionType.BIN 39 | } 40 | 41 | version = mod_version 42 | group = mod_group_id 43 | 44 | repositories { 45 | mavenLocal() 46 | // Added by TeaCon 47 | maven { 48 | name "JitPack" 49 | url 'https://jitpack.io' 50 | } 51 | // Added by TeaCon 52 | maven { 53 | name "Modrinth" 54 | url "https://api.modrinth.com/maven" 55 | } 56 | } 57 | 58 | base { 59 | // Modified by TeaCon 60 | archivesName = "$mod_github_repo-NeoForge-$minecraft_version" 61 | } 62 | 63 | // Mojang ships Java 21 to end users starting in 1.20.5, so mods should target Java 21. 64 | java.toolchain.languageVersion = JavaLanguageVersion.of(21) 65 | 66 | neoForge { 67 | // Specify the version of NeoForge to use. 68 | version = project.neo_version 69 | 70 | parchment { 71 | mappingsVersion = project.parchment_mappings_version 72 | minecraftVersion = project.parchment_minecraft_version 73 | } 74 | 75 | // This line is optional. Access Transformers are automatically detected 76 | // accessTransformers = project.files('src/main/resources/META-INF/accesstransformer.cfg') 77 | 78 | // Default run configurations. 79 | // These can be tweaked, removed, or duplicated as needed. 80 | runs { 81 | client { 82 | client() 83 | 84 | // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. 85 | systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id 86 | } 87 | 88 | server { 89 | server() 90 | programArgument '--nogui' 91 | systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id 92 | } 93 | 94 | // This run config launches GameTestServer and runs all registered gametests, then exits. 95 | // By default, the server will crash when no gametests are provided. 96 | // The gametest system is also enabled by default for other run configs under the /test command. 97 | gameTestServer { 98 | type = "gameTestServer" 99 | systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id 100 | } 101 | 102 | data { 103 | data() 104 | 105 | // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it 106 | // gameDirectory = project.file('run-data') 107 | 108 | // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. 109 | programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() 110 | } 111 | 112 | // applies to all the run configs above 113 | configureEach { 114 | // Recommended logging data for a userdev environment 115 | // The markers can be added/remove as needed separated by commas. 116 | // "SCAN": For mods scan. 117 | // "REGISTRIES": For firing of registry events. 118 | // "REGISTRYDUMP": For getting the contents of all registries. 119 | systemProperty 'forge.logging.markers', 'REGISTRIES' 120 | 121 | // Recommended logging level for the console 122 | // You can set various levels here. 123 | // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels 124 | logLevel = org.slf4j.event.Level.DEBUG 125 | } 126 | } 127 | 128 | mods { 129 | // define mod <-> source bindings 130 | // these are used to tell the game which sources are for which mod 131 | // mostly optional in a single mod project 132 | // but multi mod projects should define one per mod 133 | "${mod_id}" { 134 | sourceSet(sourceSets.main) 135 | } 136 | } 137 | } 138 | 139 | // Include resources generated by data generators. 140 | sourceSets.main.resources { srcDir 'src/generated/resources' } 141 | 142 | // Sets up a dependency configuration called 'localRuntime'. 143 | // This configuration should be used instead of 'runtimeOnly' to declare 144 | // a dependency that will be present for runtime testing but that is 145 | // "optional", meaning it will not be pulled by dependents of this mod. 146 | configurations { 147 | runtimeClasspath.extendsFrom localRuntime 148 | } 149 | 150 | dependencies { 151 | // Modified by TeaCon 152 | jarJar implementation('org.teacon:urlpattern:1.0.1') 153 | jarJar implementation('org.teacon:content-disposition:1.0.0') 154 | jarJar implementation('io.github.darkxanter:webp-imageio:0.3.2') 155 | jarJar implementation('org.apache.httpcomponents:httpclient-cache:4.5.13') 156 | // Modified by TeaCon 157 | additionalRuntimeClasspath 'org.teacon:urlpattern:1.0.1' 158 | additionalRuntimeClasspath 'org.teacon:content-disposition:1.0.0' 159 | additionalRuntimeClasspath 'io.github.darkxanter:webp-imageio:0.3.2' 160 | additionalRuntimeClasspath 'org.apache.httpcomponents:httpclient-cache:4.5.13' 161 | } 162 | 163 | // This block of code expands all declared replace properties in the specified resource targets. 164 | // A missing property will result in an error. Properties are expanded using ${} Groovy notation. 165 | // When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. 166 | // See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html 167 | tasks.withType(ProcessResources).configureEach { 168 | var replaceProperties = [ 169 | minecraft_version : minecraft_version, 170 | minecraft_version_range: minecraft_version_range, 171 | neo_version : neo_version, 172 | neo_version_range : neo_version_range, 173 | loader_version_range : loader_version_range, 174 | mod_id : mod_id, 175 | mod_name : mod_name, 176 | mod_license : mod_license, 177 | mod_version : mod_version, 178 | mod_authors : mod_authors, 179 | mod_description : mod_description 180 | ] 181 | inputs.properties replaceProperties 182 | 183 | filesMatching(['META-INF/neoforge.mods.toml']) { 184 | expand replaceProperties 185 | } 186 | } 187 | 188 | publishing { 189 | publications { 190 | // Modified by TeaCon 191 | register('release', MavenPublication) { 192 | // noinspection GroovyAssignabilityCheck 193 | from components.java 194 | version = mod_version 195 | groupId = mod_group_id 196 | artifactId = "$mod_github_repo-NeoForge-$minecraft_version" 197 | pom { 198 | name = mod_github_repo 199 | url = "https://github.com/$mod_github_owner/$mod_github_repo" 200 | licenses { 201 | license { 202 | name = mod_license 203 | url = "https://github.com/$mod_github_owner/$mod_github_repo/blob/HEAD/LICENSE" 204 | } 205 | } 206 | organization { 207 | name = 'TeaConMC' 208 | url = 'https://github.com/teaconmc' 209 | } 210 | developers { 211 | for (mod_author in "$mod_authors".split(',')) { 212 | developer { id = mod_author.trim(); name = mod_author.trim() } 213 | } 214 | } 215 | issueManagement { 216 | system = 'GitHub Issues' 217 | url = "https://github.com/$mod_github_owner/$mod_github_repo/issues" 218 | } 219 | scm { 220 | url = "https://github.com/$mod_github_owner/$mod_github_repo" 221 | connection = "scm:git:git://github.com/$mod_github_owner/${mod_github_repo}.git" 222 | developerConnection = "scm:git:git@github.com:$mod_github_owner/${mod_github_repo}.git" 223 | } 224 | } 225 | } 226 | } 227 | repositories { 228 | // Modified by TeaCon 229 | maven { 230 | name "teacon" 231 | url "s3://maven/" 232 | credentials(AwsCredentials) { 233 | accessKey = System.env.ARCHIVE_ACCESS_KEY 234 | secretKey = System.env.ARCHIVE_SECRET_KEY 235 | } 236 | } 237 | } 238 | } 239 | 240 | // Added by TeaCon 241 | tasks.withType(PublishToMavenRepository).configureEach { 242 | if (repository && repository.name == "archive") { 243 | it.onlyIf { 244 | System.env.MAVEN_USERNAME && System.env.MAVEN_PASSWORD 245 | } 246 | } 247 | } 248 | 249 | // Added by TeaCon 250 | tasks.register("githubActionOutput", TeaConDumpPathToGitHub) { task -> 251 | task.onlyIf { System.env.GITHUB_ACTIONS } 252 | task.getTargetFile().set(jar.archiveFile) 253 | task.getPublishName().set("${jar.archiveBaseName.get()}-${version}.jar") 254 | } 255 | 256 | tasks.withType(JavaCompile).configureEach { 257 | options.encoding = 'UTF-8' // Use the UTF-8 charset for Java compilation 258 | } 259 | 260 | // IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. 261 | idea { 262 | module { 263 | downloadSources = true 264 | downloadJavadoc = true 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /docs/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/docs/img1.png -------------------------------------------------------------------------------- /docs/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/docs/img2.png -------------------------------------------------------------------------------- /docs/img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/docs/img3.png -------------------------------------------------------------------------------- /docs/img4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/docs/img4.png -------------------------------------------------------------------------------- /docs/img5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/docs/img5.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Sets default memory used for gradle commands. Can be overridden by user or command line properties. 2 | org.gradle.jvmargs=-Xmx1G 3 | org.gradle.daemon=true 4 | org.gradle.parallel=true 5 | org.gradle.caching=true 6 | org.gradle.configuration-cache=true 7 | 8 | #read more on this at https://github.com/neoforged/ModDevGradle?tab=readme-ov-file#better-minecraft-parameter-names--javadoc-parchment 9 | # you can also find the latest versions at: https://parchmentmc.org/docs/getting-started 10 | parchment_minecraft_version=1.21 11 | parchment_mappings_version=2024.07.28 12 | # Environment Properties 13 | # You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge 14 | # The Minecraft version must agree with the Neo version to get a valid artifact 15 | minecraft_version=1.21.1 16 | # The Minecraft version range can use any release version of Minecraft as bounds. 17 | # Snapshots, pre-releases, and release candidates are not guaranteed to sort properly 18 | # as they do not follow standard versioning conventions. 19 | minecraft_version_range=[1.21.1,1.21.2) 20 | # The Neo version must agree with the Minecraft version to get a valid artifact 21 | neo_version=21.1.15 22 | # The Neo version range can use any version of Neo as bounds 23 | neo_version_range=[21.1.1,) 24 | # The loader version range can only use the major version of FML as bounds 25 | loader_version_range=[4,) 26 | 27 | ## Mod Properties 28 | 29 | # The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} 30 | # Must match the String constant located in the main mod class annotated with @Mod. 31 | mod_id=slide_show 32 | # The human-readable display name for the mod. 33 | mod_name=Slide Show 34 | # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. 35 | mod_license=LGPL-3.0-only 36 | # The mod version. See https://semver.org/ 37 | mod_version=0.9.8 38 | # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. 39 | # This should match the base package used for the mod sources. 40 | # See https://maven.apache.org/guides/mini/guide-naming-conventions.html 41 | mod_group_id=org.teacon 42 | # The authors of the mod. This is a simple text string that is used for display purposes in the mod list. 43 | mod_authors=3TUSK, BloCamLimb, ustc-zzzz 44 | # The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. 45 | mod_description=Minecraft mod, adding a projector that can display online images. 46 | # Added by TeaCon: gh owner 47 | mod_github_owner=TeaConMC 48 | # Added by TeaCon: gh repo name 49 | mod_github_repo=SlideShow 50 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/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.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /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 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | gradlePluginPortal() 5 | maven { url = 'https://maven.neoforged.net/releases' } 6 | } 7 | } 8 | 9 | plugins { 10 | id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/ModClientRegistries.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides; 2 | 3 | import com.google.common.base.Functions; 4 | import net.minecraft.FieldsAreNonnullByDefault; 5 | import net.minecraft.MethodsReturnNonnullByDefault; 6 | import net.minecraft.client.renderer.item.ItemProperties; 7 | import net.neoforged.api.distmarker.Dist; 8 | import net.neoforged.bus.api.SubscribeEvent; 9 | import net.neoforged.fml.common.EventBusSubscriber; 10 | import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; 11 | import net.neoforged.neoforge.client.event.EntityRenderersEvent; 12 | import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; 13 | import org.teacon.slides.item.SlideItem; 14 | import org.teacon.slides.renderer.ProjectorRenderer; 15 | import org.teacon.slides.renderer.SlideState; 16 | import org.teacon.slides.screen.ProjectorScreen; 17 | import org.teacon.slides.screen.SlideItemScreen; 18 | import org.teacon.slides.slide.Slide; 19 | 20 | import javax.annotation.ParametersAreNonnullByDefault; 21 | 22 | @FieldsAreNonnullByDefault 23 | @MethodsReturnNonnullByDefault 24 | @ParametersAreNonnullByDefault 25 | @EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) 26 | public final class ModClientRegistries { 27 | public static final boolean IS_OPTIFINE_LOADED = isOptifineLoaded(); 28 | 29 | private static boolean isOptifineLoaded() { 30 | try { 31 | Class.forName("optifine.Installer"); 32 | return true; 33 | } catch (ClassNotFoundException ignored) { 34 | return false; 35 | } 36 | } 37 | 38 | @SubscribeEvent 39 | public static void onRegisterMenuScreen(final RegisterMenuScreensEvent event) { 40 | SlideShow.LOGGER.info("OptiFine loaded: {}", IS_OPTIFINE_LOADED); 41 | event.register(ModRegistries.PROJECTOR_MENU.get(), ProjectorScreen::new); 42 | event.register(ModRegistries.SLIDE_ITEM_MENU.get(), SlideItemScreen::new); 43 | } 44 | 45 | @SubscribeEvent 46 | public static void onClientSetup(final FMLClientSetupEvent event) { 47 | SlideShow.setRequestUrlPrefetch(SlideState::prefetch); 48 | SlideShow.setApplyPrefetch(SlideState::applyPrefetch); 49 | SlideShow.setFetchSlideRecommendedName(Functions.compose(Slide::getRecommendedName, SlideState::getSlide)); 50 | event.enqueueWork(() -> { 51 | var slideItem = ModRegistries.SLIDE_ITEM.get(); 52 | ItemProperties.register(slideItem, SlideShow.id("url_status"), (stack, level, entity, seed) -> { 53 | var uuid = stack.getOrDefault(ModRegistries.SLIDE_ENTRY, SlideItem.ENTRY_DEF).id(); 54 | var status = SlideShow.checkBlock(uuid); 55 | return status.ordinal() / 2F; 56 | }); 57 | }); 58 | } 59 | 60 | @SubscribeEvent 61 | public static void registerRenders(EntityRenderersEvent.RegisterRenderers event) { 62 | event.registerBlockEntityRenderer(ModRegistries.PROJECTOR_BLOCK_ENTITY.get(), ProjectorRenderer::new); 63 | } 64 | 65 | /*@SubscribeEvent 66 | public static void registerShaders(RegisterShadersEvent event) { 67 | try { 68 | event.registerShader(new ShaderInstance(event.getResourceProvider(), 69 | SlideShow.identifier("rendertype_palette_slide"), 70 | DefaultVertexFormat.POSITION_COLOR_TEX_LIGHTMAP), SlideRenderType::setPaletteSlideShader); 71 | } catch (IOException e) { 72 | throw new RuntimeException(e); 73 | } 74 | }*/ 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/ModRegistries.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides; 2 | 3 | import net.minecraft.FieldsAreNonnullByDefault; 4 | import net.minecraft.MethodsReturnNonnullByDefault; 5 | import net.minecraft.core.component.DataComponentType; 6 | import net.minecraft.core.registries.BuiltInRegistries; 7 | import net.minecraft.resources.ResourceLocation; 8 | import net.minecraft.tags.ItemTags; 9 | import net.minecraft.tags.TagKey; 10 | import net.minecraft.world.inventory.MenuType; 11 | import net.minecraft.world.item.CreativeModeTabs; 12 | import net.minecraft.world.item.Item; 13 | import net.minecraft.world.level.block.Block; 14 | import net.minecraft.world.level.block.entity.BlockEntityType; 15 | import net.neoforged.bus.api.SubscribeEvent; 16 | import net.neoforged.fml.common.EventBusSubscriber; 17 | import net.neoforged.neoforge.capabilities.Capabilities; 18 | import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; 19 | import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; 20 | import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; 21 | import net.neoforged.neoforge.registries.DeferredHolder; 22 | import net.neoforged.neoforge.registries.RegisterEvent; 23 | import org.teacon.slides.block.ProjectorBlock; 24 | import org.teacon.slides.block.ProjectorBlockEntity; 25 | import org.teacon.slides.inventory.ProjectorContainerMenu; 26 | import org.teacon.slides.inventory.SlideItemContainerMenu; 27 | import org.teacon.slides.item.ProjectorItem; 28 | import org.teacon.slides.item.SlideItem; 29 | import org.teacon.slides.network.*; 30 | import org.teacon.slides.url.ProjectorURLArgument; 31 | import org.teacon.slides.url.ProjectorURLPatternArgument; 32 | 33 | import javax.annotation.ParametersAreNonnullByDefault; 34 | 35 | @FieldsAreNonnullByDefault 36 | @MethodsReturnNonnullByDefault 37 | @ParametersAreNonnullByDefault 38 | @EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD, modid = SlideShow.ID) 39 | public final class ModRegistries { 40 | /** 41 | * The networking channel version. Since we follow SemVer, this is 42 | * always the same as the MAJOR version of the mod version. 43 | */ 44 | // Remember to update the network version when MAJOR is bumped 45 | // Last Update: Thu, 17 Dec 2020 15:00:00 +0800 (0 => 1) 46 | // Last Update: Tue, 18 Jan 2022 20:00:00 +0800 (1 => 2) 47 | // Last Update: Sun, 26 Mar 2023 22:00:00 +0800 (2 => 3) 48 | // Last Update: Mon, 29 Jul 2024 21:00:00 +0800 (3 => 4) 49 | // Last Update: Thu, 22 Aug 2024 02:00:00 +0800 (4 => 5) 50 | public static final String NETWORK_VERSION = "5"; 51 | 52 | public static final ResourceLocation PROJECTOR_URL_PATTERN_ID = SlideShow.id("projector_url_pattern"); 53 | public static final ResourceLocation PROJECTOR_URL_ID = SlideShow.id("projector_url"); 54 | public static final ResourceLocation PROJECTOR_ID = SlideShow.id("projector"); 55 | public static final ResourceLocation SLIDE_ITEM_ID = SlideShow.id("slide_item"); 56 | public static final ResourceLocation SLIDE_ENTRY_ID = SlideShow.id("slide_entry"); 57 | 58 | public static final TagKey SLIDE_ITEMS = ItemTags.create(SlideShow.id("slide_items")); 59 | 60 | public static final DeferredHolder PROJECTOR_BLOCK; 61 | public static final DeferredHolder SLIDE_ITEM; 62 | public static final DeferredHolder, DataComponentType> SLIDE_ENTRY; 63 | public static final DeferredHolder, BlockEntityType> PROJECTOR_BLOCK_ENTITY; 64 | public static final DeferredHolder, MenuType> PROJECTOR_MENU; 65 | public static final DeferredHolder, MenuType> SLIDE_ITEM_MENU; 66 | 67 | static { 68 | SLIDE_ITEM = DeferredHolder.create(BuiltInRegistries.ITEM.key(), SLIDE_ITEM_ID); 69 | PROJECTOR_BLOCK = DeferredHolder.create(BuiltInRegistries.BLOCK.key(), PROJECTOR_ID); 70 | SLIDE_ENTRY = DeferredHolder.create(BuiltInRegistries.DATA_COMPONENT_TYPE.key(), SLIDE_ENTRY_ID); 71 | PROJECTOR_BLOCK_ENTITY = DeferredHolder.create(BuiltInRegistries.BLOCK_ENTITY_TYPE.key(), PROJECTOR_ID); 72 | PROJECTOR_MENU = DeferredHolder.create(BuiltInRegistries.MENU.key(), PROJECTOR_ID); 73 | SLIDE_ITEM_MENU = DeferredHolder.create(BuiltInRegistries.MENU.key(), SLIDE_ITEM_ID); 74 | } 75 | 76 | @SubscribeEvent 77 | public static void register(final RegisterEvent event) { 78 | event.register(BuiltInRegistries.ITEM.key(), SLIDE_ITEM_ID, SlideItem::new); 79 | event.register(BuiltInRegistries.BLOCK.key(), PROJECTOR_ID, ProjectorBlock::new); 80 | event.register(BuiltInRegistries.ITEM.key(), PROJECTOR_ID, ProjectorItem::new); 81 | event.register(BuiltInRegistries.DATA_COMPONENT_TYPE.key(), SLIDE_ENTRY_ID, SlideItem.Entry::createComponentType); 82 | event.register(BuiltInRegistries.BLOCK_ENTITY_TYPE.key(), PROJECTOR_ID, ProjectorBlockEntity::create); 83 | event.register(BuiltInRegistries.MENU.key(), PROJECTOR_ID, ProjectorContainerMenu::create); 84 | event.register(BuiltInRegistries.MENU.key(), SLIDE_ITEM_ID, SlideItemContainerMenu::create); 85 | event.register(BuiltInRegistries.COMMAND_ARGUMENT_TYPE.key(), PROJECTOR_URL_ID, ProjectorURLArgument::create); 86 | event.register(BuiltInRegistries.COMMAND_ARGUMENT_TYPE.key(), PROJECTOR_URL_PATTERN_ID, ProjectorURLPatternArgument::create); 87 | } 88 | 89 | @SubscribeEvent 90 | public static void onPayloadRegister(final RegisterPayloadHandlersEvent event) { 91 | var pr = event.registrar(NETWORK_VERSION); 92 | pr.playToServer(ProjectorUpdatePacket.TYPE, ProjectorUpdatePacket.CODEC, ProjectorUpdatePacket::handle); 93 | pr.playToServer(SlideItemUpdatePacket.TYPE, SlideItemUpdatePacket.CODEC, SlideItemUpdatePacket::handle); 94 | pr.playToClient(SlideURLPrefetchPacket.TYPE, SlideURLPrefetchPacket.CODEC, SlideURLPrefetchPacket::handle); 95 | pr.playToServer(SlideURLRequestPacket.TYPE, SlideURLRequestPacket.CODEC, SlideURLRequestPacket::handle); 96 | pr.commonToClient(SlideSummaryPacket.TYPE, SlideSummaryPacket.CODEC, SlideSummaryPacket::handle); 97 | SlideShow.LOGGER.info("Registered related network packages (version {})", NETWORK_VERSION); 98 | } 99 | 100 | @SubscribeEvent 101 | public static void onRegisterCapabilities(final RegisterCapabilitiesEvent event) { 102 | var blockItemHandler = Capabilities.ItemHandler.BLOCK; 103 | event.registerBlockEntity(blockItemHandler,PROJECTOR_BLOCK_ENTITY.get(), ProjectorBlockEntity::getCapability); 104 | } 105 | 106 | @SubscribeEvent 107 | public static void onBuildContents(final BuildCreativeModeTabContentsEvent event) { 108 | var tabKey = BuiltInRegistries.CREATIVE_MODE_TAB.getResourceKey(event.getTab()); 109 | if (tabKey.isPresent() && CreativeModeTabs.TOOLS_AND_UTILITIES.equals(tabKey.get())) { 110 | event.accept(SLIDE_ITEM.get()); 111 | event.accept(PROJECTOR_BLOCK.get()); 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/SlideShow.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides; 2 | 3 | import com.mojang.datafixers.util.Either; 4 | import net.minecraft.FieldsAreNonnullByDefault; 5 | import net.minecraft.MethodsReturnNonnullByDefault; 6 | import net.minecraft.resources.ResourceLocation; 7 | import net.neoforged.fml.common.Mod; 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.apache.logging.log4j.LogManager; 10 | import org.apache.logging.log4j.Logger; 11 | import org.teacon.slides.block.ProjectorBlockEntity; 12 | import org.teacon.slides.url.ProjectorURL; 13 | 14 | import javax.annotation.ParametersAreNonnullByDefault; 15 | import java.util.Map; 16 | import java.util.Objects; 17 | import java.util.Set; 18 | import java.util.UUID; 19 | import java.util.function.BiConsumer; 20 | import java.util.function.Consumer; 21 | import java.util.function.Function; 22 | 23 | @Mod(SlideShow.ID) 24 | @FieldsAreNonnullByDefault 25 | @MethodsReturnNonnullByDefault 26 | @ParametersAreNonnullByDefault 27 | public final class SlideShow { 28 | public static final String ID = "slide_show"; // as well as the namespace 29 | public static final Logger LOGGER = LogManager.getLogger("SlideShow"); 30 | 31 | private static volatile Consumer requestUrlPrefetch = Objects::hash; 32 | private static volatile BiConsumer, Map> applyPrefetch = Objects::hash; 33 | private static volatile Function fetchSlideRecommendedName = uuid -> StringUtils.EMPTY; 34 | private static volatile Function, ProjectorURL.Status> checkBlock = url -> ProjectorURL.Status.UNKNOWN; 35 | 36 | public static void setRequestUrlPrefetch(Consumer requestUrlPrefetch) { 37 | SlideShow.requestUrlPrefetch = requestUrlPrefetch; 38 | } 39 | 40 | public static void requestUrlPrefetch(ProjectorBlockEntity projector) { 41 | requestUrlPrefetch.accept(projector); 42 | } 43 | 44 | public static void setApplyPrefetch(BiConsumer, Map> applyPrefetch) { 45 | SlideShow.applyPrefetch = applyPrefetch; 46 | } 47 | 48 | public static void applyPrefetch(Set nonExistent, Map existent) { 49 | applyPrefetch.accept(nonExistent, existent); 50 | } 51 | 52 | public static void setFetchSlideRecommendedName(Function fetchSlideRecommendedName) { 53 | SlideShow.fetchSlideRecommendedName = fetchSlideRecommendedName; 54 | } 55 | 56 | public static String fetchSlideRecommendedName(UUID uuid) { 57 | return fetchSlideRecommendedName.apply(uuid); 58 | } 59 | 60 | public static void setCheckBlock(Function, ProjectorURL.Status> checkBlock) { 61 | SlideShow.checkBlock = checkBlock; 62 | } 63 | 64 | public static ProjectorURL.Status checkBlock(UUID uuid) { 65 | return checkBlock.apply(Either.left(uuid)); 66 | } 67 | 68 | public static ProjectorURL.Status checkBlock(ProjectorURL url) { 69 | return checkBlock.apply(Either.right(url)); 70 | } 71 | 72 | public static ResourceLocation id(String path) { 73 | return ResourceLocation.fromNamespaceAndPath(SlideShow.ID, path); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/admin/SlidePermission.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.admin; 2 | 3 | import net.minecraft.FieldsAreNonnullByDefault; 4 | import net.minecraft.MethodsReturnNonnullByDefault; 5 | import net.minecraft.commands.CommandSource; 6 | import net.minecraft.server.MinecraftServer; 7 | import net.minecraft.server.level.ServerPlayer; 8 | import net.minecraft.server.rcon.RconConsoleSource; 9 | import net.neoforged.bus.api.SubscribeEvent; 10 | import net.neoforged.fml.common.EventBusSubscriber; 11 | import net.neoforged.neoforge.server.permission.PermissionAPI; 12 | import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent; 13 | import net.neoforged.neoforge.server.permission.nodes.PermissionDynamicContext; 14 | import net.neoforged.neoforge.server.permission.nodes.PermissionNode; 15 | import net.neoforged.neoforge.server.permission.nodes.PermissionTypes; 16 | import org.teacon.slides.SlideShow; 17 | 18 | import javax.annotation.Nullable; 19 | import javax.annotation.ParametersAreNonnullByDefault; 20 | import java.util.Objects; 21 | import java.util.UUID; 22 | 23 | @FieldsAreNonnullByDefault 24 | @MethodsReturnNonnullByDefault 25 | @ParametersAreNonnullByDefault 26 | @EventBusSubscriber(bus = EventBusSubscriber.Bus.GAME) 27 | public final class SlidePermission { 28 | private static @Nullable PermissionNode INTERACT_CREATE_PERM; 29 | private static @Nullable PermissionNode INTERACT_EDIT_PERM; 30 | private static @Nullable PermissionNode INTERACT_PERM; 31 | private static @Nullable PermissionNode LIST_PERM; 32 | private static @Nullable PermissionNode BLOCK_PERM; 33 | private static @Nullable PermissionNode UNBLOCK_PERM; 34 | 35 | @SubscribeEvent 36 | public static void gatherPermNodes(PermissionGatherEvent.Nodes event) { 37 | // FIXME: permission resolving 38 | event.addNodes(INTERACT_PERM = new PermissionNode<>(SlideShow.ID, 39 | "interact.projector", PermissionTypes.BOOLEAN, SlidePermission::everyone)); 40 | event.addNodes(INTERACT_CREATE_PERM = new PermissionNode<>(SlideShow.ID, 41 | "interact.projector.create_url", PermissionTypes.BOOLEAN, SlidePermission::everyone)); 42 | event.addNodes(INTERACT_EDIT_PERM = new PermissionNode<>(SlideShow.ID, 43 | "interact.projector.edit_slide", PermissionTypes.BOOLEAN, SlidePermission::everyone)); 44 | event.addNodes(LIST_PERM = new PermissionNode<>(SlideShow.ID, 45 | "interact_url.list", PermissionTypes.BOOLEAN, SlidePermission::operator)); 46 | event.addNodes(BLOCK_PERM = new PermissionNode<>(SlideShow.ID, 47 | "interact_url.block", PermissionTypes.BOOLEAN, SlidePermission::operator)); 48 | event.addNodes(UNBLOCK_PERM = new PermissionNode<>(SlideShow.ID, 49 | "interact_url.unblock", PermissionTypes.BOOLEAN, SlidePermission::operator)); 50 | } 51 | 52 | public static boolean canInteract(@Nullable CommandSource source) { 53 | if (source instanceof ServerPlayer sp) { 54 | return PermissionAPI.getPermission(sp, Objects.requireNonNull(INTERACT_PERM)); 55 | } 56 | return false; 57 | } 58 | 59 | public static boolean canInteractCreateUrl(@Nullable CommandSource source) { 60 | if (source instanceof ServerPlayer serverPlayer) { 61 | return PermissionAPI.getPermission(serverPlayer, Objects.requireNonNull(INTERACT_CREATE_PERM)); 62 | } 63 | return false; 64 | } 65 | 66 | public static boolean canInteractEditSlide(@Nullable CommandSource source) { 67 | if (source instanceof ServerPlayer serverPlayer) { 68 | return PermissionAPI.getPermission(serverPlayer, Objects.requireNonNull(INTERACT_EDIT_PERM)); 69 | } 70 | return false; 71 | } 72 | 73 | public static boolean canListUrl(@Nullable CommandSource source) { 74 | if (source instanceof MinecraftServer || source instanceof RconConsoleSource) { 75 | return true; 76 | } 77 | if (source instanceof ServerPlayer serverPlayer) { 78 | return PermissionAPI.getPermission(serverPlayer, Objects.requireNonNull(LIST_PERM)); 79 | } 80 | return false; 81 | } 82 | 83 | public static boolean canBlockUrl(@Nullable CommandSource source) { 84 | if (source instanceof MinecraftServer || source instanceof RconConsoleSource) { 85 | return true; 86 | } 87 | if (source instanceof ServerPlayer serverPlayer) { 88 | return PermissionAPI.getPermission(serverPlayer, Objects.requireNonNull(BLOCK_PERM)); 89 | } 90 | return false; 91 | } 92 | 93 | public static boolean canUnblockUrl(@Nullable CommandSource source) { 94 | if (source instanceof MinecraftServer || source instanceof RconConsoleSource) { 95 | return true; 96 | } 97 | if (source instanceof ServerPlayer serverPlayer) { 98 | return PermissionAPI.getPermission(serverPlayer, Objects.requireNonNull(UNBLOCK_PERM)); 99 | } 100 | return false; 101 | } 102 | 103 | private static boolean everyone(@Nullable ServerPlayer player, UUID uuid, PermissionDynamicContext... context) { 104 | return true; 105 | } 106 | 107 | private static boolean operator(@Nullable ServerPlayer player, UUID uuid, PermissionDynamicContext... context) { 108 | return player != null && player.hasPermissions(2); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/cache/CacheStorage.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.cache; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.google.common.collect.Sets; 5 | import com.google.common.collect.Streams; 6 | import com.google.gson.Gson; 7 | import com.google.gson.GsonBuilder; 8 | import com.google.gson.JsonArray; 9 | import com.google.gson.JsonObject; 10 | import net.minecraft.FieldsAreNonnullByDefault; 11 | import net.minecraft.MethodsReturnNonnullByDefault; 12 | import net.minecraft.Util; 13 | import org.apache.commons.io.IOUtils; 14 | import org.apache.commons.lang3.tuple.Pair; 15 | import org.apache.http.Header; 16 | import org.apache.http.HttpHeaders; 17 | import org.apache.http.ParseException; 18 | import org.apache.http.client.cache.HttpCacheEntry; 19 | import org.apache.http.client.cache.HttpCacheStorage; 20 | import org.apache.http.client.cache.HttpCacheUpdateCallback; 21 | import org.apache.http.client.utils.DateUtils; 22 | import org.apache.http.entity.ContentType; 23 | import org.apache.http.impl.client.cache.FileResource; 24 | import org.apache.http.message.BasicLineParser; 25 | import org.apache.logging.log4j.LogManager; 26 | import org.apache.logging.log4j.Logger; 27 | import org.apache.logging.log4j.Marker; 28 | import org.apache.logging.log4j.MarkerManager; 29 | 30 | import javax.annotation.Nullable; 31 | import javax.annotation.ParametersAreNonnullByDefault; 32 | import java.io.IOException; 33 | import java.lang.ref.ReferenceQueue; 34 | import java.nio.charset.StandardCharsets; 35 | import java.nio.charset.UnsupportedCharsetException; 36 | import java.nio.file.Files; 37 | import java.nio.file.Path; 38 | import java.nio.file.Paths; 39 | import java.nio.file.StandardCopyOption; 40 | import java.util.LinkedHashMap; 41 | import java.util.Map; 42 | import java.util.Set; 43 | import java.util.concurrent.CompletableFuture; 44 | import java.util.concurrent.TimeUnit; 45 | import java.util.concurrent.atomic.AtomicInteger; 46 | 47 | @FieldsAreNonnullByDefault 48 | @MethodsReturnNonnullByDefault 49 | @ParametersAreNonnullByDefault 50 | final class CacheStorage implements HttpCacheStorage { 51 | 52 | private static final Logger LOGGER = LogManager.getLogger("SlideShow"); 53 | private static final Marker MARKER = MarkerManager.getMarker("Downloader"); 54 | 55 | private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); 56 | 57 | private static final ThreadLocal tempFilePath = ThreadLocal.withInitial(() -> { 58 | var name = Thread.currentThread().getName(); 59 | try { 60 | return Files.createTempFile("slideshow-", "-" + name + ".tmp"); 61 | } catch (IOException e) { 62 | throw new RuntimeException(e); 63 | } 64 | }); 65 | 66 | private final Object keyLock; 67 | private final Path parentPath; 68 | private final Path keyFilePath; 69 | 70 | private final AtomicInteger markedDirty = new AtomicInteger(); 71 | private final Map> entries = new LinkedHashMap<>(); 72 | private final ReferenceQueue referenceQueue; 73 | private final Set resourceReferenceHolder; 74 | 75 | private static Pair normalize(Path parent, HttpCacheEntry ce) throws IOException { 76 | var bytes = IOUtils.toByteArray(ce.getResource().getInputStream()); 77 | var type = (ContentType) null; 78 | try { 79 | var contentTypeHeader = ce.getFirstHeader(HttpHeaders.CONTENT_TYPE); 80 | if (contentTypeHeader != null) { 81 | type = ContentType.parse(contentTypeHeader.getValue()); 82 | } 83 | } catch (ParseException | UnsupportedCharsetException ignored) { 84 | // do nothing 85 | } 86 | var source = tempFilePath.get(); 87 | var targetName = FilenameAllocation.allocateSha1HashName(bytes, type); 88 | try { 89 | var target = Files.move(Files.write(source, bytes), 90 | parent.resolve(targetName), StandardCopyOption.REPLACE_EXISTING); 91 | return Pair.of(target, new HttpCacheEntry(ce.getRequestDate(), ce.getResponseDate(), 92 | ce.getStatusLine(), ce.getAllHeaders(), new FileResource(target.toFile()), ce.getVariantMap())); 93 | } finally { 94 | if (Files.deleteIfExists(source)) { 95 | LOGGER.warn(MARKER, "Failed to move temporary file {} to {}", source.getFileName(), targetName); 96 | } 97 | } 98 | } 99 | 100 | private static void saveJson(Map> entries, JsonObject root) { 101 | for (var entry : entries.entrySet()) { 102 | var filePath = entry.getValue().getKey(); 103 | var cacheEntry = entry.getValue().getValue(); 104 | root.add(entry.getKey(), Util.make(new JsonObject(), child -> { 105 | child.addProperty("request_date", DateUtils.formatDate(cacheEntry.getRequestDate())); 106 | child.addProperty("response_date", DateUtils.formatDate(cacheEntry.getResponseDate())); 107 | child.addProperty("status_line", cacheEntry.getStatusLine().toString()); 108 | child.add("headers", Util.make(new JsonArray(), array -> { 109 | for (var header : cacheEntry.getAllHeaders()) { 110 | array.add(header.toString()); 111 | } 112 | })); 113 | child.addProperty("resource", filePath.toString()); 114 | child.add("variant_map", Util.make(new JsonObject(), object -> { 115 | for (var variantEntry : cacheEntry.getVariantMap().entrySet()) { 116 | object.addProperty(variantEntry.getKey(), variantEntry.getValue()); 117 | } 118 | })); 119 | })); 120 | } 121 | } 122 | 123 | private static void loadJson(Map> entries, JsonObject root) { 124 | for (var entry : root.entrySet()) { 125 | var child = entry.getValue().getAsJsonObject(); 126 | var requestDate = DateUtils.parseDate(child.get("request_date").getAsString()); 127 | var responseDate = DateUtils.parseDate(child.get("response_date").getAsString()); 128 | var statusLine = BasicLineParser.parseStatusLine(child.get("status_line").getAsString(), null); 129 | var filePath = Paths.get(child.get("resource").getAsString()); 130 | var headers = loadHeaders(child); 131 | var variantMap = loadVariantMap(child); 132 | var cacheEntry = new HttpCacheEntry(requestDate, responseDate, 133 | statusLine, headers, new FileResource(filePath.toFile()), variantMap); 134 | entries.put(entry.getKey(), Pair.of(filePath, cacheEntry)); 135 | } 136 | } 137 | 138 | private static Map loadVariantMap(JsonObject child) { 139 | var builder = ImmutableMap.builder(); 140 | var map = child.has("variant_map") ? child.get("variant_map").getAsJsonObject() : new JsonObject(); 141 | for (var entry : map.entrySet()) { 142 | builder.put(entry.getKey(), entry.getValue().getAsString()); 143 | } 144 | return builder.build(); 145 | } 146 | 147 | private static Header[] loadHeaders(JsonObject child) { 148 | var list = child.has("headers") ? child.get("headers").getAsJsonArray() : new JsonArray(); 149 | return Streams.stream(list).map(e -> BasicLineParser.parseHeader(e.getAsString(), null)).toArray(Header[]::new); 150 | } 151 | 152 | private void save() { 153 | var root = new JsonObject(); 154 | synchronized (this.entries) { 155 | saveJson(this.entries, root); 156 | } 157 | synchronized (this.keyLock) { 158 | try (var writer = Files.newBufferedWriter(this.keyFilePath, StandardCharsets.UTF_8)) { 159 | GSON.toJson(root, writer); 160 | } catch (Exception e) { 161 | LOGGER.warn(MARKER, "Failed to save cache storage. ", e); 162 | } 163 | } 164 | } 165 | 166 | private void load() { 167 | var root = new JsonObject(); 168 | synchronized (this.keyLock) { 169 | try (var reader = Files.newBufferedReader(this.keyFilePath, StandardCharsets.UTF_8)) { 170 | root = GSON.fromJson(reader, JsonObject.class); 171 | } catch (Exception e) { 172 | LOGGER.warn(MARKER, "Failed to load cache storage. ", e); 173 | } 174 | } 175 | synchronized (this.entries) { 176 | loadJson(this.entries, root); 177 | } 178 | } 179 | 180 | private void scheduleSave() { 181 | if (this.markedDirty.getAndIncrement() == 0) { 182 | var executor = CompletableFuture.delayedExecutor(5, TimeUnit.SECONDS, Util.backgroundExecutor()); 183 | CompletableFuture.runAsync(this::save, executor).thenRun(() -> { 184 | var changes = this.markedDirty.getAndSet(0); 185 | LOGGER.debug(MARKER, "Attempted to save {} change(s) to cache storage. ", changes); 186 | }); 187 | } 188 | } 189 | 190 | public CacheStorage(Path parentPath) { 191 | this.keyLock = new Object(); 192 | this.parentPath = parentPath; 193 | this.keyFilePath = this.parentPath.resolve("storage-keys.json"); 194 | if (Files.exists(this.keyFilePath)) { 195 | this.load(); 196 | } 197 | this.referenceQueue = new ReferenceQueue<>(); 198 | this.resourceReferenceHolder = Sets.newConcurrentHashSet(); 199 | } 200 | 201 | private void keepResourceReference(final HttpCacheEntry entry) { 202 | var resource = entry.getResource(); 203 | if (resource != null) { 204 | // Must deallocate the resource when the entry is no longer in used 205 | var ref = new ResourceReference(entry, this.referenceQueue); 206 | this.resourceReferenceHolder.add(ref); 207 | } 208 | } 209 | 210 | @Nullable 211 | @Override 212 | public HttpCacheEntry getEntry(String url) { 213 | synchronized (this.entries) { 214 | var pair = this.entries.get(url); 215 | return pair != null ? pair.getValue() : null; 216 | } 217 | } 218 | 219 | @Override 220 | public void putEntry(String url, HttpCacheEntry entry) throws IOException { 221 | synchronized (this.entries) { 222 | var normalizedEntry = normalize(this.parentPath, entry); 223 | this.entries.put(url, normalizedEntry); 224 | this.keepResourceReference(entry); 225 | } 226 | this.scheduleSave(); 227 | } 228 | 229 | @Override 230 | public void removeEntry(String url) { 231 | synchronized (this.entries) { 232 | this.entries.remove(url); 233 | } 234 | this.scheduleSave(); 235 | } 236 | 237 | @Override 238 | public void updateEntry(String url, HttpCacheUpdateCallback cb) throws IOException { 239 | synchronized (this.entries) { 240 | var pair = this.entries.get(url); 241 | this.entries.put(url, normalize(this.parentPath, cb.update(pair != null ? pair.getValue() : null))); 242 | var existing = this.entries.get(url).getValue(); 243 | var updated = cb.update(existing); 244 | if (existing != updated) { 245 | this.keepResourceReference(updated); 246 | } 247 | } 248 | this.scheduleSave(); 249 | } 250 | 251 | public int cleanResources() { 252 | var ref = (ResourceReference) null; 253 | var prevCount = this.resourceReferenceHolder.size(); 254 | while ((ref = (ResourceReference) this.referenceQueue.poll()) != null) { 255 | this.resourceReferenceHolder.remove(ref); 256 | ref.getResource().dispose(); 257 | } 258 | return prevCount - this.resourceReferenceHolder.size(); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/cache/FilenameAllocation.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.cache; 2 | 3 | import com.google.common.hash.Hashing; 4 | import org.apache.http.entity.ContentType; 5 | 6 | import javax.annotation.Nullable; 7 | import javax.imageio.ImageIO; 8 | import javax.imageio.ImageReader; 9 | import java.io.ByteArrayInputStream; 10 | import java.io.IOException; 11 | import java.net.URI; 12 | import java.nio.file.Path; 13 | import java.text.Normalizer; 14 | import java.util.Collections; 15 | import java.util.Locale; 16 | 17 | public final class FilenameAllocation { 18 | private FilenameAllocation() { 19 | throw new UnsupportedOperationException(); 20 | } 21 | 22 | public static String decideImageExtension(String oldName, byte[] bytes, @Nullable ContentType type) { 23 | try (var stream = new ByteArrayInputStream(bytes)) { 24 | try (var imageStream = ImageIO.createImageInputStream(stream)) { 25 | var readers = Collections.emptyIterator(); 26 | if (type != null) { 27 | readers = ImageIO.getImageReadersByMIMEType(type.getMimeType()); 28 | } 29 | if (!readers.hasNext()) { 30 | readers = ImageIO.getImageReaders(imageStream); 31 | } 32 | if (readers.hasNext()) { 33 | var suffixes = readers.next().getOriginatingProvider().getFileSuffixes(); 34 | if (suffixes.length > 0) { 35 | var extension = suffixes[0]; 36 | var oldDotIndex = oldName.lastIndexOf('.'); 37 | var oldCutIndex = oldDotIndex < 0 ? oldName.length() : oldDotIndex; 38 | if (extension.isEmpty()) { 39 | return oldName.substring(0, oldCutIndex); 40 | } 41 | return oldName.substring(0, oldCutIndex) + '.' + extension.toLowerCase(Locale.ROOT); 42 | } 43 | } 44 | } 45 | return oldName; 46 | } catch (IOException e) { 47 | return oldName; 48 | } 49 | } 50 | 51 | public static String allocateHttpRespName(URI location, byte[] bytes, @Nullable ContentType type) { 52 | // TODO: content disposition 53 | var filename = Path.of(location.getPath()).getFileName().toString(); 54 | return decideImageExtension(Normalizer.normalize(filename, Normalizer.Form.NFC), bytes, type); 55 | } 56 | 57 | public static String allocateSha1HashName(byte[] bytes, @Nullable ContentType type) { 58 | @SuppressWarnings("deprecation") var hashFunction = Hashing.sha1(); 59 | var hashString = hashFunction.hashBytes(bytes).toString(); 60 | return decideImageExtension(hashString, bytes, type); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/cache/ImageCache.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.cache; 2 | 3 | import com.google.common.net.HttpHeaders; 4 | import net.minecraft.FieldsAreNonnullByDefault; 5 | import net.minecraft.MethodsReturnNonnullByDefault; 6 | import net.minecraft.Util; 7 | import org.apache.commons.io.IOUtils; 8 | import org.apache.http.Header; 9 | import org.apache.http.client.ClientProtocolException; 10 | import org.apache.http.client.cache.HttpCacheContext; 11 | import org.apache.http.client.methods.CloseableHttpResponse; 12 | import org.apache.http.client.methods.HttpGet; 13 | import org.apache.http.entity.ContentType; 14 | import org.apache.http.impl.client.CloseableHttpClient; 15 | import org.apache.http.impl.client.cache.CacheConfig; 16 | import org.apache.http.impl.client.cache.CachingHttpClients; 17 | import org.apache.logging.log4j.LogManager; 18 | import org.apache.logging.log4j.Logger; 19 | import org.apache.logging.log4j.Marker; 20 | import org.apache.logging.log4j.MarkerManager; 21 | import org.teacon.content_disposition.ContentDisposition; 22 | 23 | import javax.annotation.Nonnull; 24 | import javax.annotation.Nullable; 25 | import javax.annotation.ParametersAreNonnullByDefault; 26 | import javax.imageio.ImageIO; 27 | import java.io.IOException; 28 | import java.net.URI; 29 | import java.nio.file.Files; 30 | import java.nio.file.Path; 31 | import java.nio.file.Paths; 32 | import java.util.Map; 33 | import java.util.Optional; 34 | import java.util.concurrent.CompletableFuture; 35 | import java.util.concurrent.CompletionException; 36 | 37 | @FieldsAreNonnullByDefault 38 | @MethodsReturnNonnullByDefault 39 | @ParametersAreNonnullByDefault 40 | public final class ImageCache { 41 | 42 | private static final Logger LOGGER = LogManager.getLogger("SlideShow"); 43 | private static final Marker MARKER = MarkerManager.getMarker("Cache"); 44 | 45 | private static final Path LOCAL_CACHE_PATH = Paths.get("slideshow"); 46 | 47 | private static volatile @Nullable ImageCache sInstance; 48 | 49 | private static final int MAX_CACHE_OBJECT_SIZE = 1 << 29; // 512 MiB 50 | private static final CacheConfig CONFIG = 51 | CacheConfig.custom().setMaxObjectSize(MAX_CACHE_OBJECT_SIZE).setSharedCache(false).build(); 52 | 53 | private static final String DEFAULT_REFERER = "https://github.com/teaconmc/SlideShow"; 54 | // user agent copied from forge gradle 2.3 (class: net.minecraftforge.gradle.common.Constants) 55 | private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, " + 56 | "like Gecko) Chrome/23.0.1271.95 Safari/537.11"; 57 | 58 | private final CloseableHttpClient mHttpClient; 59 | private final CacheStorage mCacheStorage; 60 | 61 | public static ImageCache getInstance() { 62 | var result = sInstance; 63 | if (result == null) { 64 | synchronized (ImageCache.class) { 65 | result = sInstance; 66 | if (result == null) { 67 | sInstance = (result = new ImageCache(LOCAL_CACHE_PATH)); 68 | } 69 | } 70 | } 71 | return result; 72 | } 73 | 74 | private ImageCache(Path dir) { 75 | try { 76 | Files.createDirectories(dir); 77 | } catch (IOException e) { 78 | throw new RuntimeException("Failed to create cache directory for slide images.", e); 79 | } 80 | mCacheStorage = new CacheStorage(dir); 81 | mHttpClient = CachingHttpClients.custom().setCacheConfig(CONFIG).setHttpCacheStorage(mCacheStorage).build(); 82 | } 83 | 84 | @Nonnull 85 | public CompletableFuture> getResource(@Nonnull URI location, boolean online) { 86 | return CompletableFuture.supplyAsync(() -> { 87 | final HttpCacheContext context = HttpCacheContext.create(); 88 | try (CloseableHttpResponse response = createResponse(location, context, online)) { 89 | try { 90 | Optional dispositionOptional; 91 | try { 92 | dispositionOptional = Optional.ofNullable(response 93 | .getFirstHeader(HttpHeaders.CONTENT_DISPOSITION)) 94 | .map(Header::getValue).map(ContentDisposition::parse); 95 | } catch (IllegalArgumentException e) { 96 | dispositionOptional = Optional.empty(); 97 | } 98 | ContentType type = ContentType.getLenient(response.getEntity()); 99 | byte[] bytes = IOUtils.toByteArray(response.getEntity().getContent()); 100 | return Map.entry(dispositionOptional.flatMap(ContentDisposition::getFilename) 101 | .orElseGet(() -> FilenameAllocation.allocateHttpRespName(location, bytes, type)), bytes); 102 | } catch (IOException e) { 103 | if (online) { 104 | LOGGER.warn(MARKER, "Failed to read bytes from remote source.", e); 105 | } 106 | throw new CompletionException(e); 107 | } 108 | } catch (ClientProtocolException protocolError) { 109 | LOGGER.warn(MARKER, "Detected invalid client protocol.", protocolError); 110 | throw new CompletionException(protocolError); 111 | } catch (IOException connError) { 112 | LOGGER.warn(MARKER, "Failed to establish connection.", connError); 113 | throw new CompletionException(connError); 114 | } 115 | }, Util.nonCriticalIoPool()); 116 | } 117 | 118 | private CloseableHttpResponse createResponse(URI location, HttpCacheContext context, boolean online) throws IOException { 119 | HttpGet request = new HttpGet(location); 120 | 121 | request.addHeader(HttpHeaders.REFERER, DEFAULT_REFERER); 122 | request.addHeader(HttpHeaders.USER_AGENT, DEFAULT_USER_AGENT); 123 | request.addHeader(HttpHeaders.ACCEPT, String.join(", ", ImageIO.getReaderMIMETypes())); 124 | 125 | if (!online) { 126 | request.addHeader(HttpHeaders.CACHE_CONTROL, "max-stale=2147483647"); 127 | request.addHeader(HttpHeaders.CACHE_CONTROL, "only-if-cached"); 128 | } else { 129 | request.addHeader(HttpHeaders.CACHE_CONTROL, "must-revalidate"); 130 | } 131 | 132 | return mHttpClient.execute(request, context); 133 | } 134 | 135 | private void logRequestHeader(@Nonnull HttpCacheContext context) { 136 | LOGGER.debug(MARKER, " >> {}", context.getRequest().getRequestLine()); 137 | for (Header header : context.getRequest().getAllHeaders()) { 138 | LOGGER.debug(MARKER, " >> {}", header); 139 | } 140 | LOGGER.debug(MARKER, " << {}", context.getResponse().getStatusLine()); 141 | for (Header header : context.getResponse().getAllHeaders()) { 142 | LOGGER.debug(MARKER, " << {}", header); 143 | } 144 | LOGGER.debug(MARKER, "Remote server status: {}", context.getCacheResponseStatus()); 145 | } 146 | 147 | public int cleanResources() { 148 | return mCacheStorage.cleanResources(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/cache/ResourceReference.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.cache; 2 | 3 | import net.minecraft.FieldsAreNonnullByDefault; 4 | import net.minecraft.MethodsReturnNonnullByDefault; 5 | import org.apache.http.client.cache.HttpCacheEntry; 6 | import org.apache.http.client.cache.Resource; 7 | import org.apache.http.util.Args; 8 | 9 | import javax.annotation.ParametersAreNonnullByDefault; 10 | import java.lang.ref.PhantomReference; 11 | import java.lang.ref.ReferenceQueue; 12 | 13 | @FieldsAreNonnullByDefault 14 | @MethodsReturnNonnullByDefault 15 | @ParametersAreNonnullByDefault 16 | final class ResourceReference extends PhantomReference { 17 | 18 | private final Resource resource; 19 | 20 | public ResourceReference(final HttpCacheEntry entry, final ReferenceQueue q) { 21 | super(entry, q); 22 | this.resource = Args.notNull(entry.getResource(), "Resource"); 23 | } 24 | 25 | public Resource getResource() { 26 | return this.resource; 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | return this.resource.hashCode(); 32 | } 33 | 34 | @Override 35 | public boolean equals(Object obj) { 36 | return obj == this; // reference equals 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/calc/CalcMicros.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.calc; 2 | 3 | import net.minecraft.FieldsAreNonnullByDefault; 4 | import net.minecraft.MethodsReturnNonnullByDefault; 5 | import org.joml.*; 6 | import org.teacon.slides.block.ProjectorBlock.InternalRotation; 7 | 8 | import javax.annotation.ParametersAreNonnullByDefault; 9 | import java.lang.Math; 10 | import java.text.DecimalFormat; 11 | import java.util.Set; 12 | 13 | import static com.google.common.base.Preconditions.checkArgument; 14 | import static org.joml.RoundingMode.HALF_EVEN; 15 | 16 | @FieldsAreNonnullByDefault 17 | @MethodsReturnNonnullByDefault 18 | @ParametersAreNonnullByDefault 19 | public final class CalcMicros { 20 | private CalcMicros() { 21 | throw new UnsupportedOperationException(); 22 | } 23 | 24 | private static final DecimalFormat 25 | WITHOUT_PLUS_SIGN = new DecimalFormat("0.0###"), 26 | WITH_PLUS_SIGN = new DecimalFormat("+0.0###;-0.0###"); 27 | 28 | public static int fromNumber(float value) { 29 | return (int) Math.rint(value * 1E6); 30 | } 31 | 32 | public static int fromString(String text, int oldMicros) { 33 | var builder = new StringBuilder("calc(").append(text).append(")"); 34 | var calc = new CalcBasic(builder); 35 | checkArgument(builder.isEmpty() && Set.of("").containsAll(calc.getLengthUnits())); 36 | var relative = calc.getPercentage() * oldMicros; 37 | var absolute = calc.getLengthValue("") * 1E6; 38 | return (int) Math.rint(relative + absolute); 39 | } 40 | 41 | public static float toNumber(int micros) { 42 | return (float) (micros / 1E6); 43 | } 44 | 45 | public static String toString(int micros, boolean positiveSigned) { 46 | return micros == 0 ? "0.0" : (positiveSigned ? WITH_PLUS_SIGN : WITHOUT_PLUS_SIGN).format(micros / 1E6); 47 | } 48 | 49 | public static Vector3d fromRelToAbs(Vector3i relOffsetMicros, Vector2i sizeMicros, InternalRotation rotation) { 50 | var center = new Vector4d(sizeMicros.x() / 2D, 0D, sizeMicros.y() / 2D, 1D); 51 | // matrix 7: offset for slide (center[new] = center[old] + offset) 52 | center.mul(new Matrix4d().translate(relOffsetMicros.x(), -relOffsetMicros.z(), relOffsetMicros.y())); 53 | // matrix 6: translation for slide 54 | center.mul(new Matrix4d().translate(-5E5, 0D, 5E5 - sizeMicros.y())); 55 | // matrix 5: internal rotation 56 | rotation.transform(center); 57 | // ok, that's enough 58 | return new Vector3d(center.x() / center.w(), center.y() / center.w(), center.z() / center.w()); 59 | } 60 | 61 | public static Vector3i fromAbsToRel(Vector3d absOffsetMicros, Vector2i sizeMicros, InternalRotation rotation) { 62 | var center = new Vector4d(absOffsetMicros, 1D); 63 | // inverse matrix 5: internal rotation 64 | rotation.invert().transform(center); 65 | // inverse matrix 6: translation for slide 66 | center.mul(new Matrix4d().translate(5E5, 0D, -5E5 + sizeMicros.y())); 67 | // subtract (offset = center[new] - center[old]) 68 | center.mul(new Matrix4d().translate(sizeMicros.x() / -2D, 0D, sizeMicros.y() / -2D)); 69 | // ok, that's enough (remember it is (a, -c, b) => (a, b, c)) 70 | return new Vector3i(center.x() / center.w(), center.z() / center.w(), -center.y() / center.w(), HALF_EVEN); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/inventory/ProjectorContainerMenu.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.inventory; 2 | 3 | import net.minecraft.FieldsAreNonnullByDefault; 4 | import net.minecraft.MethodsReturnNonnullByDefault; 5 | import net.minecraft.Util; 6 | import net.minecraft.core.BlockPos; 7 | import net.minecraft.network.RegistryFriendlyByteBuf; 8 | import net.minecraft.network.codec.ByteBufCodecs; 9 | import net.minecraft.server.level.ServerPlayer; 10 | import net.minecraft.world.entity.player.Inventory; 11 | import net.minecraft.world.entity.player.Player; 12 | import net.minecraft.world.inventory.AbstractContainerMenu; 13 | import net.minecraft.world.inventory.MenuType; 14 | import net.minecraft.world.inventory.Slot; 15 | import net.minecraft.world.item.ItemStack; 16 | import net.neoforged.neoforge.common.extensions.IMenuTypeExtension; 17 | import net.neoforged.neoforge.items.SlotItemHandler; 18 | import org.apache.commons.lang3.tuple.MutablePair; 19 | import org.joml.Vector2i; 20 | import org.joml.Vector3i; 21 | import org.teacon.slides.ModRegistries; 22 | import org.teacon.slides.admin.SlidePermission; 23 | import org.teacon.slides.block.ProjectorBlock; 24 | import org.teacon.slides.block.ProjectorBlockEntity; 25 | import org.teacon.slides.block.ProjectorBlockEntity.ColorTransform; 26 | import org.teacon.slides.block.ProjectorBlockEntity.SlideItemStackHandler; 27 | import org.teacon.slides.item.SlideItem; 28 | 29 | import javax.annotation.ParametersAreNonnullByDefault; 30 | import java.util.Optional; 31 | 32 | @FieldsAreNonnullByDefault 33 | @MethodsReturnNonnullByDefault 34 | @ParametersAreNonnullByDefault 35 | public final class ProjectorContainerMenu extends AbstractContainerMenu { 36 | public final BlockPos tilePos; 37 | public final Vector2i tileSizeMicros; 38 | public final Vector3i tileOffsetMicros; 39 | public final ColorTransform tileColorTransform; 40 | public final MutablePair, Optional> tileNextCurrent; 41 | public final ProjectorBlock.InternalRotation tileInitialRotation; 42 | 43 | public ProjectorContainerMenu(int containerId, Inventory playerInventory, ProjectorBlockEntity projector) { 44 | super(ModRegistries.PROJECTOR_MENU.get(), containerId); 45 | 46 | this.tilePos = projector.getBlockPos(); 47 | this.tileSizeMicros = projector.getSizeMicros(); 48 | this.tileOffsetMicros = projector.getOffsetMicros(); 49 | this.tileColorTransform = projector.getColorTransform(); 50 | this.tileNextCurrent = projector.getNextCurrentEntries(); 51 | this.tileInitialRotation = projector.getBlockState().getValue(ProjectorBlock.ROTATION); 52 | 53 | var itemsToDisplay = projector.getItemsToDisplay(); 54 | var itemsDisplayed = projector.getItemsDisplayed(); 55 | 56 | for (var i = 0; i < ProjectorBlock.SLIDE_ITEM_HANDLER_CAPACITY; ++i) { 57 | this.addSlot(new SlotItemHandler(itemsToDisplay, i, 7 + (i % 12) * 18, 7 + (i / 12) * 18)); 58 | } 59 | 60 | for (var i = 0; i < ProjectorBlock.SLIDE_ITEM_HANDLER_CAPACITY; ++i) { 61 | this.addSlot(new SlotItemHandler(itemsDisplayed, i, 7 + (i % 12) * 18, 156 + (i / 12) * 18)); 62 | } 63 | 64 | for (var i = 9; i < 36; ++i) { 65 | this.addSlot(new Slot(playerInventory, i, 235 + (i % 9) * 18, 170 + (i / 9) * 18)); 66 | } 67 | 68 | for (var i = 0; i < 9; ++i) { 69 | this.addSlot(new Slot(playerInventory, i, 235 + (i % 9) * 18, 246)); 70 | } 71 | } 72 | 73 | public ProjectorContainerMenu(int containerId, Inventory playerInventory, RegistryFriendlyByteBuf buf) { 74 | super(ModRegistries.PROJECTOR_MENU.get(), containerId); 75 | 76 | this.tilePos = buf.readBlockPos(); 77 | this.tileSizeMicros = new Vector2i(buf.readVarInt(), buf.readVarInt()); 78 | this.tileOffsetMicros = new Vector3i(buf.readVarInt(), buf.readVarInt(), buf.readVarInt()); 79 | this.tileColorTransform = Util.make(new ColorTransform(), colorTransform -> { 80 | colorTransform.color = buf.readInt(); 81 | colorTransform.doubleSided = buf.readBoolean(); 82 | colorTransform.hideEmptySlideIcon = buf.readBoolean(); 83 | colorTransform.hideFailedSlideIcon = buf.readBoolean(); 84 | colorTransform.hideBlockedSlideIcon = buf.readBoolean(); 85 | colorTransform.hideLoadingSlideIcon = buf.readBoolean(); 86 | }); 87 | this.tileNextCurrent = MutablePair.of( 88 | ByteBufCodecs.optional(SlideItem.Entry.STREAM_CODEC).decode(buf), 89 | ByteBufCodecs.optional(SlideItem.Entry.STREAM_CODEC).decode(buf)); 90 | this.tileInitialRotation = buf.readById(ProjectorBlock.InternalRotation.BY_ID); 91 | 92 | var itemsToDisplay = new SlideItemStackHandler( 93 | () -> tileNextCurrent.setLeft(Optional.empty()), 94 | (f, l) -> tileNextCurrent.setLeft(Optional.of(f))); 95 | var itemsDisplayed = new SlideItemStackHandler( 96 | () -> tileNextCurrent.setRight(Optional.empty()), 97 | (f, l) -> tileNextCurrent.setRight(Optional.of(l))); 98 | 99 | for (var i = 0; i < ProjectorBlock.SLIDE_ITEM_HANDLER_CAPACITY; ++i) { 100 | this.addSlot(new SlotItemHandler(itemsToDisplay, i, 8 + (i % 12) * 18, 8 + (i / 12) * 18)); 101 | } 102 | 103 | for (var i = 0; i < ProjectorBlock.SLIDE_ITEM_HANDLER_CAPACITY; ++i) { 104 | this.addSlot(new SlotItemHandler(itemsDisplayed, i, 8 + (i % 12) * 18, 157 + (i / 12) * 18)); 105 | } 106 | 107 | for (var i = 9; i < 36; ++i) { 108 | this.addSlot(new Slot(playerInventory, i, 236 + (i % 9) * 18, 171 + (i / 9) * 18)); 109 | } 110 | 111 | for (var i = 0; i < 9; ++i) { 112 | this.addSlot(new Slot(playerInventory, i, 236 + (i % 9) * 18, 247)); 113 | } 114 | } 115 | 116 | public static void openGui(Player currentPlayer, ProjectorBlockEntity tile) { 117 | if (currentPlayer instanceof ServerPlayer player && SlidePermission.canInteract(player)) { 118 | currentPlayer.openMenu(tile, buf -> { 119 | buf.writeBlockPos(tile.getBlockPos()); 120 | var tileSizeMicros = tile.getSizeMicros(); 121 | buf.writeVarInt(tileSizeMicros.x).writeVarInt(tileSizeMicros.y); 122 | var tileOffsetMicros = tile.getOffsetMicros(); 123 | buf.writeVarInt(tileOffsetMicros.x).writeVarInt(tileOffsetMicros.y).writeVarInt(tileOffsetMicros.z); 124 | var tileColorTransform = tile.getColorTransform(); 125 | buf.writeInt(tileColorTransform.color); 126 | buf.writeBoolean(tileColorTransform.doubleSided); 127 | buf.writeBoolean(tileColorTransform.hideEmptySlideIcon); 128 | buf.writeBoolean(tileColorTransform.hideFailedSlideIcon); 129 | buf.writeBoolean(tileColorTransform.hideBlockedSlideIcon); 130 | buf.writeBoolean(tileColorTransform.hideLoadingSlideIcon); 131 | var tileNextCurrent = tile.getNextCurrentEntries(); 132 | ByteBufCodecs.optional(SlideItem.Entry.STREAM_CODEC).encode(buf, tileNextCurrent.left); 133 | ByteBufCodecs.optional(SlideItem.Entry.STREAM_CODEC).encode(buf, tileNextCurrent.right); 134 | var tileInternalRotation = tile.getBlockState().getValue(ProjectorBlock.ROTATION); 135 | buf.writeById(ProjectorBlock.InternalRotation::ordinal, tileInternalRotation); 136 | }); 137 | } 138 | } 139 | 140 | public static MenuType create() { 141 | return IMenuTypeExtension.create(ProjectorContainerMenu::new); 142 | } 143 | 144 | @Override 145 | public ItemStack quickMoveStack(Player player, int index) { 146 | var projectHalfCapacity = ProjectorBlock.SLIDE_ITEM_HANDLER_CAPACITY; 147 | var resultStack = ItemStack.EMPTY; 148 | var slot = this.slots.get(index); 149 | // noinspection ConstantValue 150 | if (slot != null && slot.hasItem()) { 151 | var slotStack = slot.getItem(); 152 | resultStack = slotStack.copy(); 153 | if (index < projectHalfCapacity * 2) { 154 | if (!this.moveItemStackTo(slotStack, projectHalfCapacity * 2, this.slots.size(), true)) { 155 | return ItemStack.EMPTY; 156 | } 157 | } else if (!this.moveItemStackTo(slotStack, 0, projectHalfCapacity, false) && 158 | !this.moveItemStackTo(slotStack, projectHalfCapacity, projectHalfCapacity * 2, true)) { 159 | return ItemStack.EMPTY; 160 | } 161 | if (slotStack.isEmpty()) { 162 | slot.setByPlayer(ItemStack.EMPTY); 163 | } else { 164 | slot.setChanged(); 165 | } 166 | } 167 | return resultStack; 168 | } 169 | 170 | @Override 171 | public boolean stillValid(Player player) { 172 | // noinspection resource 173 | var level = player.level(); 174 | if (!level.isLoaded(this.tilePos)) { 175 | return false; 176 | } 177 | if (!(level.getBlockEntity(this.tilePos) instanceof ProjectorBlockEntity)) { 178 | return false; 179 | } 180 | return SlidePermission.canInteract(player); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/inventory/SlideItemContainerMenu.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.inventory; 2 | 3 | import net.minecraft.FieldsAreNonnullByDefault; 4 | import net.minecraft.MethodsReturnNonnullByDefault; 5 | import net.minecraft.network.RegistryFriendlyByteBuf; 6 | import net.minecraft.world.entity.player.Inventory; 7 | import net.minecraft.world.entity.player.Player; 8 | import net.minecraft.world.inventory.AbstractContainerMenu; 9 | import net.minecraft.world.inventory.MenuType; 10 | import net.minecraft.world.item.ItemStack; 11 | import net.neoforged.neoforge.common.extensions.IMenuTypeExtension; 12 | import org.teacon.slides.ModRegistries; 13 | import org.teacon.slides.network.SlideItemUpdatePacket; 14 | 15 | import javax.annotation.ParametersAreNonnullByDefault; 16 | 17 | @FieldsAreNonnullByDefault 18 | @MethodsReturnNonnullByDefault 19 | @ParametersAreNonnullByDefault 20 | public final class SlideItemContainerMenu extends AbstractContainerMenu { 21 | public final SlideItemUpdatePacket packet; 22 | 23 | public SlideItemContainerMenu(int containerId, SlideItemUpdatePacket packet) { 24 | super(ModRegistries.SLIDE_ITEM_MENU.get(), containerId); 25 | this.packet = packet; 26 | } 27 | 28 | public SlideItemContainerMenu(int containerId, Inventory inventory, RegistryFriendlyByteBuf buf) { 29 | super(ModRegistries.SLIDE_ITEM_MENU.get(), containerId); 30 | this.packet = SlideItemUpdatePacket.CODEC.decode(buf); 31 | } 32 | 33 | public static MenuType create() { 34 | return IMenuTypeExtension.create(SlideItemContainerMenu::new); 35 | } 36 | 37 | @Override 38 | public ItemStack quickMoveStack(Player player, int index) { 39 | // TODO 40 | return ItemStack.EMPTY; 41 | } 42 | 43 | @Override 44 | public boolean stillValid(Player player) { 45 | return player.getInventory().getItem(this.packet.slotId()).is(ModRegistries.SLIDE_ITEM); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/item/ProjectorItem.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.item; 2 | 3 | import net.minecraft.FieldsAreNonnullByDefault; 4 | import net.minecraft.MethodsReturnNonnullByDefault; 5 | import net.minecraft.Util; 6 | import net.minecraft.core.BlockPos; 7 | import net.minecraft.core.NonNullList; 8 | import net.minecraft.core.component.DataComponents; 9 | import net.minecraft.world.entity.player.Player; 10 | import net.minecraft.world.item.BlockItem; 11 | import net.minecraft.world.item.Item; 12 | import net.minecraft.world.item.ItemStack; 13 | import net.minecraft.world.item.Rarity; 14 | import net.minecraft.world.item.component.ItemContainerContents; 15 | import net.minecraft.world.level.Level; 16 | import net.minecraft.world.level.block.state.BlockState; 17 | import org.teacon.slides.ModRegistries; 18 | import org.teacon.slides.block.ProjectorBlock; 19 | import org.teacon.slides.block.ProjectorBlockEntity; 20 | import org.teacon.slides.inventory.ProjectorContainerMenu; 21 | 22 | import javax.annotation.Nullable; 23 | import javax.annotation.ParametersAreNonnullByDefault; 24 | 25 | @FieldsAreNonnullByDefault 26 | @MethodsReturnNonnullByDefault 27 | @ParametersAreNonnullByDefault 28 | public final class ProjectorItem extends BlockItem { 29 | 30 | public ProjectorItem() { 31 | super(ModRegistries.PROJECTOR_BLOCK.get(), new Item.Properties().rarity(Rarity.RARE) 32 | .component(DataComponents.CONTAINER, Util.make(() -> { 33 | var list = NonNullList.withSize(ProjectorBlock.SLIDE_ITEM_HANDLER_CAPACITY * 2, ItemStack.EMPTY); 34 | list.set(0, ModRegistries.SLIDE_ITEM.get().getDefaultInstance()); 35 | return ItemContainerContents.fromItems(list); 36 | }))); 37 | } 38 | 39 | @Override 40 | protected boolean updateCustomBlockEntityTag(BlockPos pos, Level level, @Nullable Player player, ItemStack stack, 41 | BlockState state) { 42 | final boolean superResult = super.updateCustomBlockEntityTag(pos, level, player, stack, state); 43 | if (!superResult && !level.isClientSide && player != null) { 44 | if (level.getBlockEntity(pos) instanceof ProjectorBlockEntity tile) { 45 | ProjectorContainerMenu.openGui(player, tile); 46 | } 47 | } 48 | return superResult; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/item/SlideItem.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.item; 2 | 3 | import com.mojang.datafixers.util.Either; 4 | import com.mojang.serialization.Codec; 5 | import com.mojang.serialization.codecs.RecordCodecBuilder; 6 | import io.netty.buffer.ByteBuf; 7 | import net.minecraft.ChatFormatting; 8 | import net.minecraft.FieldsAreNonnullByDefault; 9 | import net.minecraft.MethodsReturnNonnullByDefault; 10 | import net.minecraft.core.UUIDUtil; 11 | import net.minecraft.core.component.DataComponentType; 12 | import net.minecraft.network.chat.Component; 13 | import net.minecraft.network.codec.ByteBufCodecs; 14 | import net.minecraft.network.codec.StreamCodec; 15 | import net.minecraft.server.level.ServerPlayer; 16 | import net.minecraft.stats.Stats; 17 | import net.minecraft.world.InteractionHand; 18 | import net.minecraft.world.InteractionResultHolder; 19 | import net.minecraft.world.MenuProvider; 20 | import net.minecraft.world.SimpleMenuProvider; 21 | import net.minecraft.world.entity.player.Inventory; 22 | import net.minecraft.world.entity.player.Player; 23 | import net.minecraft.world.item.Item; 24 | import net.minecraft.world.item.ItemStack; 25 | import net.minecraft.world.item.Rarity; 26 | import net.minecraft.world.item.TooltipFlag; 27 | import net.minecraft.world.level.Level; 28 | import org.apache.commons.lang3.StringUtils; 29 | import org.teacon.slides.ModRegistries; 30 | import org.teacon.slides.SlideShow; 31 | import org.teacon.slides.calc.CalcBasic; 32 | import org.teacon.slides.inventory.SlideItemContainerMenu; 33 | import org.teacon.slides.network.SlideItemUpdatePacket; 34 | import org.teacon.slides.url.ProjectorURLSavedData; 35 | import org.teacon.slides.url.ProjectorURLSavedData.LogType; 36 | 37 | import javax.annotation.ParametersAreNonnullByDefault; 38 | import java.util.*; 39 | import java.util.function.Function; 40 | import java.util.regex.Pattern; 41 | 42 | import static com.google.common.base.Preconditions.checkArgument; 43 | import static com.google.common.base.Predicates.alwaysFalse; 44 | import static org.apache.commons.lang3.StringUtils.abbreviateMiddle; 45 | 46 | @FieldsAreNonnullByDefault 47 | @MethodsReturnNonnullByDefault 48 | @ParametersAreNonnullByDefault 49 | public final class SlideItem extends Item { 50 | public static final Entry ENTRY_DEF = new Entry(new UUID(0L, 0L), Size.DEFAULT); 51 | 52 | public SlideItem() { 53 | super(new Properties().stacksTo(1).rarity(Rarity.RARE).component(ModRegistries.SLIDE_ENTRY, ENTRY_DEF)); 54 | } 55 | 56 | @Override 57 | public void appendHoverText(ItemStack stack, TooltipContext context, List tooltips, TooltipFlag flag) { 58 | tooltips.add(Component.translatable("item.slide_show.slide_item.hint").withStyle(ChatFormatting.GRAY)); 59 | } 60 | 61 | @Override 62 | public Component getName(ItemStack stack) { 63 | var uuid = stack.getOrDefault(ModRegistries.SLIDE_ENTRY, SlideItem.ENTRY_DEF).id(); 64 | var name = abbreviateMiddle("<" + SlideShow.fetchSlideRecommendedName(uuid) + ">", "...", 45); 65 | return "<>".equals(name) ? Component.translatable(this.getDescriptionId(stack)) : Component.literal(name); 66 | } 67 | 68 | @Override 69 | public InteractionResultHolder use(Level level, Player player, InteractionHand hand) { 70 | var item = player.getItemInHand(hand); 71 | if (player instanceof ServerPlayer serverPlayer) { 72 | var slotId = hand == InteractionHand.MAIN_HAND ? player.getInventory().selected : Inventory.SLOT_OFFHAND; 73 | var entry = item.getOrDefault(ModRegistries.SLIDE_ENTRY, ENTRY_DEF); 74 | var data = ProjectorURLSavedData.get(serverPlayer.getServer()); 75 | var log = Optional.empty(); 76 | var imgUrl = data.getUrlById(entry.id()); 77 | if (imgUrl.isPresent()) { 78 | log = data.getLatestLog(imgUrl.get(), alwaysFalse(), Set.of(LogType.BLOCK, LogType.UNBLOCK)); 79 | if (log.isEmpty()) { 80 | log = data.getLatestLog(imgUrl.get(), alwaysFalse(), Set.of(LogType.values())); 81 | } 82 | } 83 | var perm = new SlideItemUpdatePacket.Perm(player); 84 | var packet = new SlideItemUpdatePacket(slotId, perm, entry.id(), log, imgUrl, entry.size()); 85 | player.openMenu(this.getMenuProvider(item, packet), buf -> SlideItemUpdatePacket.CODEC.encode(buf, packet)); 86 | } 87 | player.awardStat(Stats.ITEM_USED.get(this)); 88 | return InteractionResultHolder.sidedSuccess(item, level.isClientSide()); 89 | } 90 | 91 | private MenuProvider getMenuProvider(ItemStack item, SlideItemUpdatePacket packet) { 92 | return new SimpleMenuProvider((c, i, p) -> new SlideItemContainerMenu(c, packet), item.getDisplayName()); 93 | } 94 | 95 | public record Entry(UUID id, Size size) { 96 | public static final Codec CODEC; 97 | public static final StreamCodec STREAM_CODEC; 98 | 99 | static { 100 | CODEC = RecordCodecBuilder.create(builder -> builder.group( 101 | UUIDUtil.CODEC.fieldOf("id").forGetter(Entry::id), 102 | Size.CODEC.fieldOf("size").forGetter(Entry::size)).apply(builder, Entry::new)); 103 | STREAM_CODEC = StreamCodec.composite( 104 | UUIDUtil.STREAM_CODEC, Entry::id, 105 | Size.STREAM_CODEC, Entry::size, Entry::new); 106 | } 107 | 108 | public static DataComponentType createComponentType() { 109 | var builder = DataComponentType.builder(); 110 | return builder.persistent(CODEC).networkSynchronized(STREAM_CODEC).cacheEncoding().build(); 111 | } 112 | } 113 | 114 | public sealed interface Size permits KeywordSize, ValueSize, AutoValueSize, ValueAutoSize, ValueValueSize { 115 | Codec CODEC = Codec.STRING.xmap(Size::parse, Size::toString); 116 | StreamCodec STREAM_CODEC = ByteBufCodecs.STRING_UTF8.map(Size::parse, Size::toString); 117 | ValueValueSize DEFAULT = new ValueValueSize(new CalcBasic(100D), new CalcBasic(100D)); 118 | 119 | static Size parse(String input) { 120 | var builder = new StringBuilder(input); 121 | // check first keyword or calc 122 | var first = parseKeywordOrCalc(builder); 123 | if (builder.isEmpty()) { 124 | return first.map(Function.identity(), ValueSize::new); 125 | } 126 | first.ifLeft(k1 -> checkArgument(k1 == KeywordSize.AUTO, "only keyword auto allowed in two arguments")); 127 | // skip internal spaces 128 | var secondStart = 0; 129 | while (secondStart < builder.length()) { 130 | if (Character.isWhitespace(builder.charAt(secondStart))) { 131 | secondStart += 1; 132 | continue; 133 | } 134 | break; 135 | } 136 | builder.delete(0, secondStart); 137 | // check second keyword or calc 138 | var second = parseKeywordOrCalc(builder); 139 | checkArgument(builder.isEmpty(), "only two arguments allowed"); 140 | second.ifLeft(k2 -> checkArgument(k2 == KeywordSize.AUTO, "only keyword auto allowed in two arguments")); 141 | return second.map(k2 -> first.map(k1 -> KeywordSize.AUTO_AUTO, ValueAutoSize::new), 142 | c2 -> first.map(k1 -> new AutoValueSize(c2), c1 -> new ValueValueSize(c1, c2))); 143 | } 144 | 145 | private static Either parseKeywordOrCalc(StringBuilder builder) { 146 | var matcher = KeywordSize.PATTERN.matcher(builder); 147 | if (!matcher.find()) { 148 | var calc = new CalcBasic(builder); 149 | checkArgument(calc.getLengthUnits().isEmpty(), "only percentage allowed"); 150 | return calc.hasPercentage() ? Either.right(calc) : Either.right(new CalcBasic(0D)); 151 | } 152 | var keyword = switch (StringUtils.toRootLowerCase(matcher.group("k"))) { 153 | case "auto" -> KeywordSize.AUTO; 154 | case "cover" -> KeywordSize.COVER; 155 | case "contain" -> KeywordSize.CONTAIN; 156 | case null, default -> throw new IllegalStateException("Unexpected value: " + matcher.group("k")); 157 | }; 158 | builder.delete(0, matcher.end()); 159 | return Either.left(keyword); 160 | } 161 | } 162 | 163 | public enum KeywordSize implements Size { 164 | COVER, CONTAIN, AUTO, AUTO_AUTO; 165 | 166 | private static final Pattern PATTERN; 167 | 168 | static { 169 | PATTERN = Pattern.compile("\\G(?cover|contain|auto)", Pattern.CASE_INSENSITIVE); 170 | } 171 | 172 | @Override 173 | public String toString() { 174 | return this.name().toLowerCase(Locale.ROOT).replace('_', ' '); 175 | } 176 | } 177 | 178 | public record ValueSize(CalcBasic width) implements Size { 179 | @Override 180 | public String toString() { 181 | return this.width.toString(); 182 | } 183 | } 184 | 185 | public record AutoValueSize(CalcBasic height) implements Size { 186 | @Override 187 | public String toString() { 188 | return "auto " + this.height; 189 | } 190 | } 191 | 192 | public record ValueAutoSize(CalcBasic width) implements Size { 193 | @Override 194 | public String toString() { 195 | return this.width + " auto"; 196 | } 197 | } 198 | 199 | public record ValueValueSize(CalcBasic width, CalcBasic height) implements Size { 200 | @Override 201 | public String toString() { 202 | return this.width + " " + this.height; 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/network/ProjectorUpdatePacket.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.network; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import net.minecraft.FieldsAreNonnullByDefault; 5 | import net.minecraft.MethodsReturnNonnullByDefault; 6 | import net.minecraft.core.BlockPos; 7 | import net.minecraft.core.GlobalPos; 8 | import net.minecraft.network.RegistryFriendlyByteBuf; 9 | import net.minecraft.network.codec.ByteBufCodecs; 10 | import net.minecraft.network.codec.StreamCodec; 11 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 12 | import net.minecraft.server.level.ServerLevel; 13 | import net.minecraft.util.ByIdMap; 14 | import net.minecraft.world.level.block.Block; 15 | import net.neoforged.neoforge.network.handling.IPayloadContext; 16 | import org.apache.logging.log4j.Marker; 17 | import org.apache.logging.log4j.MarkerManager; 18 | import org.teacon.slides.SlideShow; 19 | import org.teacon.slides.admin.SlidePermission; 20 | import org.teacon.slides.block.ProjectorBlock; 21 | import org.teacon.slides.block.ProjectorBlockEntity; 22 | 23 | import javax.annotation.ParametersAreNonnullByDefault; 24 | import java.util.function.IntFunction; 25 | 26 | @FieldsAreNonnullByDefault 27 | @MethodsReturnNonnullByDefault 28 | @ParametersAreNonnullByDefault 29 | public record ProjectorUpdatePacket(Category category, BlockPos pos, int value) implements CustomPacketPayload { 30 | private static final Marker MARKER = MarkerManager.getMarker("Network"); 31 | 32 | public static final Type TYPE; 33 | public static final StreamCodec CODEC; 34 | 35 | static { 36 | TYPE = new Type<>(SlideShow.id("block_update")); 37 | CODEC = StreamCodec.composite( 38 | Category.STREAM_CODEC, ProjectorUpdatePacket::category, 39 | BlockPos.STREAM_CODEC, ProjectorUpdatePacket::pos, 40 | ByteBufCodecs.INT, ProjectorUpdatePacket::value, 41 | ProjectorUpdatePacket::new); 42 | } 43 | 44 | public void handle(IPayloadContext context) { 45 | context.enqueueWork(() -> { 46 | var player = context.player(); 47 | // noinspection resource 48 | if (SlidePermission.canInteract(player) && player.level() instanceof ServerLevel level) { 49 | // prevent remote chunk loading 50 | if (level.isLoaded(this.pos) && level.getBlockEntity(this.pos) instanceof ProjectorBlockEntity tile) { 51 | var state = tile.getBlockState(); 52 | switch (this.category) { 53 | case MOVE_SLIDE_ITEMS -> tile.moveSlideItems(this.value); 54 | case SET_WIDTH_MICROS -> tile.getSizeMicros().x = this.value; 55 | case SET_HEIGHT_MICROS -> tile.getSizeMicros().y = this.value; 56 | case SET_OFFSET_X_MICROS -> tile.getOffsetMicros().x = this.value; 57 | case SET_OFFSET_Y_MICROS -> tile.getOffsetMicros().y = this.value; 58 | case SET_OFFSET_Z_MICROS -> tile.getOffsetMicros().z = this.value; 59 | case SET_ADDITIONAL_COLOR -> tile.getColorTransform().color = this.value; 60 | case SET_DOUBLE_SIDED -> tile.getColorTransform().doubleSided = this.value != 0; 61 | case SET_INTERNAL_ROTATION -> { 62 | var rotation = ProjectorBlock.InternalRotation.BY_ID.apply(this.value); 63 | state = state.setValue(ProjectorBlock.ROTATION, rotation); 64 | } 65 | } 66 | // update states 67 | if (!level.setBlock(this.pos, state, Block.UPDATE_ALL)) { 68 | // state is unchanged, but re-render it 69 | level.sendBlockUpdated(this.pos, state, state, Block.UPDATE_CLIENTS); 70 | } 71 | // mark chunk unsaved 72 | tile.setChanged(); 73 | return; 74 | } 75 | var profile = player.getGameProfile(); 76 | var globalPos = GlobalPos.of(level.dimension(), this.pos); 77 | SlideShow.LOGGER.debug(MARKER, "Received illegal packet: player = {}, pos = {}", profile, globalPos); 78 | } 79 | }); 80 | } 81 | 82 | @Override 83 | public Type type() { 84 | return TYPE; 85 | } 86 | 87 | public enum Category { 88 | MOVE_SLIDE_ITEMS, SET_WIDTH_MICROS, SET_HEIGHT_MICROS, 89 | SET_OFFSET_X_MICROS, SET_OFFSET_Y_MICROS, SET_OFFSET_Z_MICROS, 90 | SET_ADDITIONAL_COLOR, SET_DOUBLE_SIDED, SET_INTERNAL_ROTATION; 91 | 92 | private static final IntFunction BY_ID; 93 | private static final StreamCodec STREAM_CODEC; 94 | 95 | static { 96 | BY_ID = ByIdMap.continuous(Category::ordinal, values(), ByIdMap.OutOfBoundsStrategy.ZERO); 97 | STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, Category::ordinal); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/network/SlideItemUpdatePacket.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.network; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import net.minecraft.FieldsAreNonnullByDefault; 5 | import net.minecraft.MethodsReturnNonnullByDefault; 6 | import net.minecraft.commands.CommandSource; 7 | import net.minecraft.core.UUIDUtil; 8 | import net.minecraft.network.RegistryFriendlyByteBuf; 9 | import net.minecraft.network.codec.ByteBufCodecs; 10 | import net.minecraft.network.codec.StreamCodec; 11 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 12 | import net.minecraft.server.level.ServerPlayer; 13 | import net.minecraft.world.entity.player.Inventory; 14 | import net.neoforged.neoforge.network.handling.IPayloadContext; 15 | import org.teacon.slides.ModRegistries; 16 | import org.teacon.slides.SlideShow; 17 | import org.teacon.slides.admin.SlidePermission; 18 | import org.teacon.slides.item.SlideItem; 19 | import org.teacon.slides.url.ProjectorURL; 20 | import org.teacon.slides.url.ProjectorURLSavedData; 21 | import org.teacon.slides.url.ProjectorURLSavedData.Log; 22 | 23 | import javax.annotation.ParametersAreNonnullByDefault; 24 | import java.util.Optional; 25 | import java.util.UUID; 26 | 27 | @FieldsAreNonnullByDefault 28 | @MethodsReturnNonnullByDefault 29 | @ParametersAreNonnullByDefault 30 | public record SlideItemUpdatePacket(int slotId, Perm permissions, 31 | UUID imgUniqueId, Optional oldLastLog, 32 | Optional url, SlideItem.Size size) implements CustomPacketPayload { 33 | public static final CustomPacketPayload.Type TYPE; 34 | public static final StreamCodec CODEC; 35 | 36 | static { 37 | TYPE = new CustomPacketPayload.Type<>(SlideShow.id("item_update")); 38 | CODEC = StreamCodec.composite( 39 | ByteBufCodecs.VAR_INT, SlideItemUpdatePacket::slotId, 40 | Perm.STREAM_CODEC, SlideItemUpdatePacket::permissions, 41 | UUIDUtil.STREAM_CODEC, SlideItemUpdatePacket::imgUniqueId, 42 | Log.OPTIONAL_STREAM_CODEC, SlideItemUpdatePacket::oldLastLog, 43 | ProjectorURL.OPTIONAL_STREAM_CODEC, SlideItemUpdatePacket::url, 44 | SlideItem.Size.STREAM_CODEC, SlideItemUpdatePacket::size, 45 | SlideItemUpdatePacket::new); 46 | } 47 | 48 | public void handle(IPayloadContext context) { 49 | if (Inventory.isHotbarSlot(this.slotId) || this.slotId == Inventory.SLOT_OFFHAND) { 50 | if (context.player() instanceof ServerPlayer player && SlidePermission.canInteractEditSlide(player)) { 51 | var data = ProjectorURLSavedData.get(player.server); 52 | var item = player.getInventory().getItem(this.slotId); 53 | if (item.is(ModRegistries.SLIDE_ITEM)) { 54 | var newEntry = new SlideItem.Entry(this.imgUniqueId, this.size); 55 | var oldEntry = item.getOrDefault(ModRegistries.SLIDE_ENTRY, SlideItem.ENTRY_DEF); 56 | if (data.getUrlById(newEntry.id()).isEmpty() && this.url.isPresent()) { 57 | if (SlidePermission.canInteractCreateUrl(player)) { 58 | newEntry = new SlideItem.Entry(data.getOrCreateIdByItem(this.url.get(), player), this.size); 59 | } else { 60 | var imgId = data.getIdByUrl(this.url.get()); 61 | newEntry = new SlideItem.Entry(imgId.orElseGet(oldEntry::id), this.size); 62 | } 63 | } 64 | if (!newEntry.equals(oldEntry)) { 65 | item.set(ModRegistries.SLIDE_ENTRY, newEntry); 66 | data.applyIdChangeByItem(oldEntry, newEntry, player); 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | @Override 74 | public Type type() { 75 | return TYPE; 76 | } 77 | 78 | public record Perm(boolean create, boolean edit) { 79 | public static final StreamCodec STREAM_CODEC; 80 | 81 | public Perm(CommandSource source) { 82 | this(SlidePermission.canInteractCreateUrl(source), SlidePermission.canInteractEditSlide(source)); 83 | } 84 | 85 | static { 86 | STREAM_CODEC = StreamCodec.of((buffer, value) -> { 87 | var flags = (value.create ? 0b10 : 0b00) + (value.edit ? 0b01 : 0b00); 88 | buffer.writeByte(flags); 89 | }, buffer -> { 90 | var flags = buffer.readByte(); 91 | return new Perm((flags & 0b10) != 0, (flags & 0b01) != 0); 92 | }); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/network/SlideSummaryPacket.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.network; 2 | 3 | import com.google.common.collect.BiMap; 4 | import com.google.common.hash.HashFunction; 5 | import com.google.common.hash.Hashing; 6 | import com.mojang.datafixers.util.Either; 7 | import it.unimi.dsi.fastutil.objects.Object2BooleanMap; 8 | import it.unimi.dsi.fastutil.objects.Object2BooleanMaps; 9 | import it.unimi.dsi.fastutil.objects.Object2BooleanRBTreeMap; 10 | import net.minecraft.FieldsAreNonnullByDefault; 11 | import net.minecraft.MethodsReturnNonnullByDefault; 12 | import net.minecraft.core.UUIDUtil; 13 | import net.minecraft.network.FriendlyByteBuf; 14 | import net.minecraft.network.codec.StreamCodec; 15 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 16 | import net.minecraft.util.Crypt; 17 | import net.neoforged.neoforge.network.handling.IPayloadContext; 18 | import org.teacon.slides.SlideShow; 19 | import org.teacon.slides.url.ProjectorURL; 20 | import org.teacon.slides.url.ProjectorURL.Status; 21 | 22 | import javax.annotation.ParametersAreNonnullByDefault; 23 | import java.nio.ByteBuffer; 24 | import java.nio.ByteOrder; 25 | import java.nio.charset.StandardCharsets; 26 | import java.util.Set; 27 | import java.util.UUID; 28 | import java.util.function.Function; 29 | 30 | import static com.google.common.base.Preconditions.checkArgument; 31 | 32 | @FieldsAreNonnullByDefault 33 | @MethodsReturnNonnullByDefault 34 | @ParametersAreNonnullByDefault 35 | public final class SlideSummaryPacket implements Function, Status>, CustomPacketPayload { 36 | public static final CustomPacketPayload.Type TYPE; 37 | public static final StreamCodec CODEC; 38 | 39 | static { 40 | TYPE = new CustomPacketPayload.Type<>(SlideShow.id("url_summary")); 41 | CODEC = StreamCodec.ofMember(SlideSummaryPacket::write, SlideSummaryPacket::new); 42 | } 43 | 44 | private final Bits256 hmacNonce; 45 | private final HashFunction hmacNonceFunction; 46 | private final Object2BooleanMap hmacToBlockStatus; 47 | 48 | public SlideSummaryPacket(BiMap idToUrl, Set blockedIdSet) { 49 | var hmacNonce = Bits256.random(); 50 | var hmacNonceFunction = Hashing.hmacSha256(hmacNonce.toBytes()); 51 | var hmacUrlToBlockStatus = new Object2BooleanRBTreeMap(); 52 | for (var entry : idToUrl.entrySet()) { 53 | var hmacKey = hmacNonceFunction.hashBytes(UUIDUtil.uuidToByteArray(entry.getKey())); 54 | var hmacValue = hmacNonceFunction.hashString(entry.getValue().toString(), StandardCharsets.US_ASCII); 55 | hmacUrlToBlockStatus.put(Bits256.fromBytes(hmacKey.asBytes()), blockedIdSet.contains(entry.getKey())); 56 | hmacUrlToBlockStatus.put(Bits256.fromBytes(hmacValue.asBytes()), blockedIdSet.contains(entry.getKey())); 57 | } 58 | this.hmacNonce = hmacNonce; 59 | this.hmacNonceFunction = hmacNonceFunction; 60 | this.hmacToBlockStatus = Object2BooleanMaps.unmodifiable(hmacUrlToBlockStatus); 61 | } 62 | 63 | public SlideSummaryPacket(FriendlyByteBuf buf) { 64 | var nonce = Bits256.read(buf); 65 | var hmacToBlockStatus = new Object2BooleanRBTreeMap(); 66 | while (true) { 67 | switch (buf.readEnum(Status.class)) { 68 | case UNKNOWN -> { 69 | this.hmacNonce = nonce; 70 | this.hmacNonceFunction = Hashing.hmacSha256(nonce.toBytes()); 71 | this.hmacToBlockStatus = Object2BooleanMaps.unmodifiable(hmacToBlockStatus); 72 | return; 73 | } 74 | case BLOCKED -> hmacToBlockStatus.put(Bits256.read(buf), true); 75 | case ALLOWED -> hmacToBlockStatus.put(Bits256.read(buf), false); 76 | } 77 | } 78 | } 79 | 80 | public void write(FriendlyByteBuf buf) { 81 | this.hmacNonce.write(buf); 82 | for (var entry : this.hmacToBlockStatus.object2BooleanEntrySet()) { 83 | buf.writeEnum(entry.getBooleanValue() ? Status.BLOCKED : Status.ALLOWED); 84 | entry.getKey().write(buf); 85 | } 86 | buf.writeEnum(Status.UNKNOWN); 87 | } 88 | 89 | @Override 90 | public Status apply(Either either) { 91 | var hmac = Bits256.fromBytes(either.map( 92 | uuid -> this.hmacNonceFunction.hashBytes(UUIDUtil.uuidToByteArray(uuid)), 93 | url -> this.hmacNonceFunction.hashString(url.toString(), StandardCharsets.US_ASCII)).asBytes()); 94 | if (this.hmacToBlockStatus.containsKey(hmac)) { 95 | return this.hmacToBlockStatus.getBoolean(hmac) ? Status.BLOCKED : Status.ALLOWED; 96 | } 97 | return Status.UNKNOWN; 98 | } 99 | 100 | public void handle(IPayloadContext ignored) { 101 | // thread safe 102 | SlideShow.setCheckBlock(this); 103 | } 104 | 105 | @Override 106 | public CustomPacketPayload.Type type() { 107 | return TYPE; 108 | } 109 | 110 | public record Bits256(long bytesLE1, long bytesLE2, 111 | long bytesLE3, long bytesLE4) implements Comparable { 112 | public void write(FriendlyByteBuf buf) { 113 | buf.writeLongLE(this.bytesLE1).writeLongLE(this.bytesLE2); 114 | buf.writeLongLE(this.bytesLE3).writeLongLE(this.bytesLE4); 115 | } 116 | 117 | public static Bits256 read(FriendlyByteBuf buf) { 118 | return new Bits256(buf.readLongLE(), buf.readLongLE(), buf.readLongLE(), buf.readLongLE()); 119 | } 120 | 121 | public byte[] toBytes() { 122 | var buffer = ByteBuffer.wrap(new byte[256 / Byte.SIZE]).order(ByteOrder.LITTLE_ENDIAN); 123 | buffer.putLong(this.bytesLE1).putLong(this.bytesLE2); 124 | buffer.putLong(this.bytesLE3).putLong(this.bytesLE4); 125 | return buffer.array(); 126 | } 127 | 128 | public static Bits256 fromBytes(byte[] bytes) { 129 | checkArgument(bytes.length == 256 / Byte.SIZE); 130 | var buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); 131 | return new Bits256(buffer.getLong(), buffer.getLong(), buffer.getLong(), buffer.getLong()); 132 | } 133 | 134 | public static Bits256 random() { 135 | return new Bits256( 136 | Crypt.SaltSupplier.getLong(), Crypt.SaltSupplier.getLong(), 137 | Crypt.SaltSupplier.getLong(), Crypt.SaltSupplier.getLong()); 138 | } 139 | 140 | @Override 141 | public int compareTo(Bits256 that) { 142 | // compare from the last byte to the first byte 143 | var cmp1 = Long.compareUnsigned(this.bytesLE1, that.bytesLE1); 144 | var cmp2 = Long.compareUnsigned(this.bytesLE2, that.bytesLE2); 145 | var cmp3 = Long.compareUnsigned(this.bytesLE3, that.bytesLE3); 146 | var cmp4 = Long.compareUnsigned(this.bytesLE4, that.bytesLE4); 147 | return cmp4 == 0 ? cmp3 == 0 ? cmp2 == 0 ? cmp1 : cmp2 : cmp3 : cmp4; 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/network/SlideURLPrefetchPacket.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.network; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.google.common.collect.ImmutableSet; 5 | import net.minecraft.FieldsAreNonnullByDefault; 6 | import net.minecraft.MethodsReturnNonnullByDefault; 7 | import net.minecraft.network.RegistryFriendlyByteBuf; 8 | import net.minecraft.network.codec.StreamCodec; 9 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 10 | import net.neoforged.neoforge.network.handling.IPayloadContext; 11 | import org.teacon.slides.SlideShow; 12 | import org.teacon.slides.url.ProjectorURL; 13 | import org.teacon.slides.url.ProjectorURLSavedData; 14 | 15 | import javax.annotation.ParametersAreNonnullByDefault; 16 | import java.util.Set; 17 | import java.util.UUID; 18 | 19 | @FieldsAreNonnullByDefault 20 | @MethodsReturnNonnullByDefault 21 | @ParametersAreNonnullByDefault 22 | public final class SlideURLPrefetchPacket implements CustomPacketPayload { 23 | public static final CustomPacketPayload.Type TYPE; 24 | public static final StreamCodec CODEC; 25 | 26 | static { 27 | TYPE = new CustomPacketPayload.Type<>(SlideShow.id("url_prefetch")); 28 | CODEC = StreamCodec.ofMember(SlideURLPrefetchPacket::write, SlideURLPrefetchPacket::new); 29 | } 30 | 31 | private final ImmutableSet nonExistentIdSet; 32 | 33 | private final ImmutableMap existentIdMap; 34 | 35 | private enum Status { 36 | END, EXISTENT, NON_EXISTENT 37 | } 38 | 39 | public SlideURLPrefetchPacket(Set idSet, ProjectorURLSavedData data) { 40 | var nonExistentBuilder = ImmutableSet.builder(); 41 | var existentBuilder = ImmutableMap.builder(); 42 | for (var id : idSet) { 43 | data.getUrlById(id).ifPresentOrElse(u -> existentBuilder.put(id, u), () -> nonExistentBuilder.add(id)); 44 | } 45 | this.nonExistentIdSet = nonExistentBuilder.build(); 46 | this.existentIdMap = existentBuilder.build(); 47 | } 48 | 49 | public SlideURLPrefetchPacket(RegistryFriendlyByteBuf buf) { 50 | var nonExistentBuilder = ImmutableSet.builder(); 51 | var existentBuilder = ImmutableMap.builder(); 52 | while (true) { 53 | switch (buf.readEnum(Status.class)) { 54 | case END -> { 55 | this.nonExistentIdSet = nonExistentBuilder.build(); 56 | this.existentIdMap = existentBuilder.build(); 57 | return; 58 | } 59 | case NON_EXISTENT -> nonExistentBuilder.add(buf.readUUID()); 60 | case EXISTENT -> existentBuilder.put(buf.readUUID(), new ProjectorURL(buf.readUtf())); 61 | } 62 | } 63 | } 64 | 65 | public void write(RegistryFriendlyByteBuf buf) { 66 | this.existentIdMap.forEach((uuid, url) -> buf.writeEnum(Status.EXISTENT).writeUUID(uuid).writeUtf(url.toUrl().toString())); 67 | this.nonExistentIdSet.forEach(uuid -> buf.writeEnum(Status.NON_EXISTENT).writeUUID(uuid)); 68 | buf.writeEnum(Status.END); 69 | } 70 | 71 | public void handle(IPayloadContext context) { 72 | context.enqueueWork(() -> SlideShow.applyPrefetch(this.nonExistentIdSet, this.existentIdMap)); 73 | } 74 | 75 | @Override 76 | public Type type() { 77 | return TYPE; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/network/SlideURLRequestPacket.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.network; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import it.unimi.dsi.fastutil.ints.IntArrayList; 5 | import it.unimi.dsi.fastutil.ints.IntList; 6 | import net.minecraft.FieldsAreNonnullByDefault; 7 | import net.minecraft.MethodsReturnNonnullByDefault; 8 | import net.minecraft.core.BlockPos; 9 | import net.minecraft.network.RegistryFriendlyByteBuf; 10 | import net.minecraft.network.codec.StreamCodec; 11 | import net.minecraft.network.protocol.common.custom.CustomPacketPayload; 12 | import net.minecraft.server.level.ServerPlayer; 13 | import net.minecraft.world.item.ItemStack; 14 | import net.neoforged.neoforge.network.PacketDistributor; 15 | import net.neoforged.neoforge.network.handling.IPayloadContext; 16 | import org.teacon.slides.ModRegistries; 17 | import org.teacon.slides.SlideShow; 18 | import org.teacon.slides.block.ProjectorBlockEntity; 19 | import org.teacon.slides.url.ProjectorURLSavedData; 20 | 21 | import javax.annotation.ParametersAreNonnullByDefault; 22 | import java.util.LinkedHashSet; 23 | import java.util.UUID; 24 | 25 | @FieldsAreNonnullByDefault 26 | @MethodsReturnNonnullByDefault 27 | @ParametersAreNonnullByDefault 28 | public final class SlideURLRequestPacket implements CustomPacketPayload { 29 | public static final CustomPacketPayload.Type TYPE; 30 | public static final StreamCodec CODEC; 31 | 32 | static { 33 | TYPE = new CustomPacketPayload.Type<>(SlideShow.id("url_request")); 34 | CODEC = StreamCodec.ofMember(SlideURLRequestPacket::write, SlideURLRequestPacket::new); 35 | } 36 | 37 | private final ImmutableSet requestedPosSet; 38 | private final IntList requestedSlotIdList; 39 | 40 | public SlideURLRequestPacket(Iterable blockPosRequested, IntList slotIdRequested) { 41 | this.requestedPosSet = ImmutableSet.copyOf(blockPosRequested); 42 | this.requestedSlotIdList = new IntArrayList(slotIdRequested); 43 | } 44 | 45 | public SlideURLRequestPacket(RegistryFriendlyByteBuf buf) { 46 | var posCount = buf.readVarInt(); 47 | var posSetBuilder = ImmutableSet.builder(); 48 | for (var i = 0; i < posCount; ++i) { 49 | posSetBuilder.add(new BlockPos(buf.readVarInt(), buf.readVarInt(), buf.readVarInt())); 50 | } 51 | this.requestedPosSet = posSetBuilder.build(); 52 | this.requestedSlotIdList = buf.readIntIdList(); 53 | } 54 | 55 | public void write(RegistryFriendlyByteBuf buf) { 56 | buf.writeVarInt(this.requestedPosSet.size()); 57 | for (var pos : this.requestedPosSet) { 58 | buf.writeVarInt(pos.getX()); 59 | buf.writeVarInt(pos.getY()); 60 | buf.writeVarInt(pos.getZ()); 61 | } 62 | buf.writeIntIdList(this.requestedSlotIdList); 63 | } 64 | 65 | public void handle(IPayloadContext context) { 66 | context.enqueueWork(() -> { 67 | var player = context.player(); 68 | if (player instanceof ServerPlayer serverPlayer) { 69 | // noinspection resource 70 | var level = serverPlayer.serverLevel(); 71 | var imageLocations = new LinkedHashSet(this.requestedPosSet.size()); 72 | for (var pos : this.requestedPosSet) { 73 | // prevent remote chunk loading 74 | if (level.isLoaded(pos) && level.getBlockEntity(pos) instanceof ProjectorBlockEntity tile) { 75 | tile.getNextCurrentEntries().getLeft().ifPresent(entry -> imageLocations.add(entry.id())); 76 | tile.getNextCurrentEntries().getRight().ifPresent(entry -> imageLocations.add(entry.id())); 77 | } 78 | } 79 | for (var slotId: this.requestedSlotIdList) { 80 | var item = ItemStack.EMPTY; 81 | if (slotId == -1) { 82 | item = serverPlayer.containerMenu.getCarried(); 83 | } 84 | if (slotId >= 0 && slotId < serverPlayer.containerMenu.slots.size()) { 85 | item = serverPlayer.containerMenu.slots.get(slotId).getItem(); 86 | } 87 | var entry = item.get(ModRegistries.SLIDE_ENTRY); 88 | if (entry != null) { 89 | imageLocations.add(entry.id()); 90 | } 91 | } 92 | var data = ProjectorURLSavedData.get(serverPlayer.getServer()); 93 | PacketDistributor.sendToPlayer(serverPlayer, new SlideURLPrefetchPacket(imageLocations, data)); 94 | } 95 | }); 96 | } 97 | 98 | @Override 99 | public CustomPacketPayload.Type type() { 100 | return TYPE; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/renderer/ProjectorRenderer.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.renderer; 2 | 3 | import com.mojang.blaze3d.vertex.PoseStack; 4 | import net.minecraft.FieldsAreNonnullByDefault; 5 | import net.minecraft.MethodsReturnNonnullByDefault; 6 | import net.minecraft.client.Minecraft; 7 | import net.minecraft.client.renderer.LightTexture; 8 | import net.minecraft.client.renderer.MultiBufferSource; 9 | import net.minecraft.client.renderer.RenderType; 10 | import net.minecraft.client.renderer.block.BlockRenderDispatcher; 11 | import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; 12 | import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; 13 | import net.minecraft.client.renderer.texture.OverlayTexture; 14 | import net.minecraft.world.inventory.InventoryMenu; 15 | import net.minecraft.world.item.Items; 16 | import net.minecraft.world.phys.AABB; 17 | import net.neoforged.neoforge.client.model.data.ModelData; 18 | import org.joml.Vector2d; 19 | import org.teacon.slides.ModRegistries; 20 | import org.teacon.slides.block.ProjectorBlock; 21 | import org.teacon.slides.block.ProjectorBlockEntity; 22 | import org.teacon.slides.item.SlideItem; 23 | import org.teacon.slides.slide.IconSlide; 24 | 25 | import javax.annotation.ParametersAreNonnullByDefault; 26 | import java.util.List; 27 | 28 | @FieldsAreNonnullByDefault 29 | @MethodsReturnNonnullByDefault 30 | @ParametersAreNonnullByDefault 31 | public final class ProjectorRenderer implements BlockEntityRenderer { 32 | private final BlockRenderDispatcher blockRenderDispatcher; 33 | 34 | public ProjectorRenderer(BlockEntityRendererProvider.Context context) { 35 | this.blockRenderDispatcher = context.getBlockRenderDispatcher(); 36 | } 37 | 38 | @Override 39 | public void render(ProjectorBlockEntity tile, float partialTick, PoseStack pStack, 40 | MultiBufferSource src, int packedLight, int packedOverlay) { 41 | var tileState = tile.getBlockState(); 42 | // always update slide state of current and next slide 43 | var nextCurrentEntries = tile.getNextCurrentEntries(); 44 | var nextEntry = nextCurrentEntries.left; 45 | if (nextEntry.isPresent()) { 46 | var tileNextEntryUUID = nextEntry.get().id(); 47 | SlideState.getSlide(tileNextEntryUUID); 48 | } 49 | var currentEntry = nextCurrentEntries.right; 50 | if (currentEntry.isPresent()) { 51 | pStack.pushPose(); 52 | var tileColorTransform = tile.getColorTransform(); 53 | var tileCurrentEntryUUID = currentEntry.get().id(); 54 | var tileCurrentSlide = SlideState.getSlide(tileCurrentEntryUUID); 55 | var tileIconHidden = tileCurrentSlide instanceof IconSlide iconSlide && switch (iconSlide) { 56 | case DEFAULT_EMPTY -> tileColorTransform.hideEmptySlideIcon; 57 | case DEFAULT_FAILED -> tileColorTransform.hideFailedSlideIcon; 58 | case DEFAULT_BLOCKED -> tileColorTransform.hideBlockedSlideIcon; 59 | case DEFAULT_LOADING -> tileColorTransform.hideLoadingSlideIcon; 60 | }; 61 | var tileColorTransparent = (tileColorTransform.color & 0xFF000000) == 0; 62 | if (!tileColorTransparent && !tileIconHidden) { 63 | var last = pStack.last(); 64 | tile.transformToSlideSpaceMicros(last.pose(), last.normal()); 65 | var flipped = tileState.getValue(ProjectorBlock.ROTATION).isFlipped(); 66 | var sizeMicros = tile.getSizeMicros(); 67 | var scaleSizeMicros = new Vector2d(sizeMicros); 68 | switch (currentEntry.get().size()) { 69 | case SlideItem.KeywordSize.COVER -> tileCurrentSlide.getDimension().ifPresent(dim -> { 70 | var scale = Math.max((double) sizeMicros.x / dim.x, (double) sizeMicros.y / dim.y); 71 | scaleSizeMicros.set(scale * dim.x, scale * dim.y); 72 | }); 73 | case SlideItem.KeywordSize.CONTAIN, 74 | SlideItem.KeywordSize.AUTO, 75 | SlideItem.KeywordSize.AUTO_AUTO -> tileCurrentSlide.getDimension().ifPresent(dim -> { 76 | var scale = Math.min((double) sizeMicros.x / dim.x, (double) sizeMicros.y / dim.y); 77 | scaleSizeMicros.set(scale * dim.x, scale * dim.y); 78 | }); 79 | case SlideItem.AutoValueSize(var value) -> { 80 | var scale = value.getPercentage() / 100D; 81 | scaleSizeMicros.y = scale * sizeMicros.y; 82 | var tileCurrentSlideDim = tileCurrentSlide.getDimension(); 83 | tileCurrentSlideDim.ifPresent(dim -> scaleSizeMicros.x = scale * sizeMicros.y * dim.x / dim.y); 84 | } 85 | case SlideItem.ValueAutoSize(var value) -> { 86 | var scale = value.getPercentage() / 100D; 87 | scaleSizeMicros.x = scale * sizeMicros.x; 88 | var tileCurrentSlideDim = tileCurrentSlide.getDimension(); 89 | tileCurrentSlideDim.ifPresent(dim -> scaleSizeMicros.y = scale * sizeMicros.x * dim.y / dim.x); 90 | } 91 | case SlideItem.ValueSize(var value) -> { 92 | var scale = value.getPercentage() / 100D; 93 | scaleSizeMicros.x = scale * sizeMicros.x; 94 | var tileCurrentSlideDim = tileCurrentSlide.getDimension(); 95 | tileCurrentSlideDim.ifPresent(dim -> scaleSizeMicros.y = scale * sizeMicros.x * dim.y / dim.x); 96 | } 97 | case SlideItem.ValueValueSize(var first, var second) -> { 98 | scaleSizeMicros.x = first.getPercentage() / 100D * sizeMicros.x; 99 | scaleSizeMicros.y = second.getPercentage() / 100D * sizeMicros.y; 100 | } 101 | } 102 | tileCurrentSlide.render(src, last, 103 | sizeMicros.x, sizeMicros.y, scaleSizeMicros.x, scaleSizeMicros.y, 104 | tileColorTransform.color, LightTexture.FULL_BRIGHT, OverlayTexture.NO_OVERLAY, 105 | flipped || tileColorTransform.doubleSided, !flipped || tileColorTransform.doubleSided, 106 | SlideState.getAnimationTick(), partialTick); 107 | } 108 | pStack.popPose(); 109 | } 110 | if (tile.hasLevel()) { 111 | pStack.pushPose(); 112 | var mc = Minecraft.getInstance(); 113 | var handItems = mc.player == null ? List.of(Items.AIR, Items.AIR) : 114 | List.of(mc.player.getMainHandItem().getItem(), mc.player.getOffhandItem().getItem()); 115 | if (handItems.contains(ModRegistries.PROJECTOR_BLOCK.get().asItem())) { 116 | var outline = RenderType.outline(InventoryMenu.BLOCK_ATLAS); 117 | var outlineSource = mc.renderBuffers().outlineBufferSource(); 118 | var blockModel = this.blockRenderDispatcher.getBlockModel(tileState); 119 | this.blockRenderDispatcher.getModelRenderer().renderModel( 120 | pStack.last(), outlineSource.getBuffer(outline), tileState, blockModel, 121 | 0.0F, 0.0F, 0.0F, packedLight, packedOverlay, ModelData.EMPTY, outline); 122 | } 123 | pStack.popPose(); 124 | } 125 | } 126 | 127 | @Override 128 | public AABB getRenderBoundingBox(ProjectorBlockEntity blockEntity) { 129 | return blockEntity.getRenderBoundingBox(); 130 | } 131 | 132 | @Override 133 | public boolean shouldRenderOffScreen(ProjectorBlockEntity tile) { 134 | // global rendering 135 | return true; 136 | } 137 | 138 | @Override 139 | public int getViewDistance() { 140 | return 256; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/renderer/SlideRenderType.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.renderer; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.mojang.blaze3d.systems.RenderSystem; 5 | import com.mojang.blaze3d.vertex.DefaultVertexFormat; 6 | import com.mojang.blaze3d.vertex.VertexFormat; 7 | import net.minecraft.client.renderer.RenderStateShard; 8 | import net.minecraft.client.renderer.RenderType; 9 | import net.minecraft.client.renderer.ShaderInstance; 10 | import net.minecraft.resources.ResourceLocation; 11 | import org.teacon.slides.SlideShow; 12 | 13 | /** 14 | * @author BloCamLimb 15 | */ 16 | public final class SlideRenderType extends RenderType.CompositeRenderType { 17 | 18 | private static ShaderInstance sPaletteSlideShader; 19 | private static final ShaderStateShard 20 | RENDERTYPE_PALETTE_SLIDE = new ShaderStateShard(SlideRenderType::getPaletteSlideShader); 21 | 22 | private static final ImmutableList GENERAL_STATES; 23 | private static final ImmutableList PALETTE_STATES; 24 | 25 | static { 26 | GENERAL_STATES = ImmutableList.of( 27 | RENDERTYPE_TEXT_SEE_THROUGH_SHADER, 28 | TRANSLUCENT_TRANSPARENCY, 29 | LEQUAL_DEPTH_TEST, 30 | CULL, 31 | LIGHTMAP, 32 | NO_OVERLAY, 33 | NO_LAYERING, 34 | MAIN_TARGET, 35 | DEFAULT_TEXTURING, 36 | COLOR_DEPTH_WRITE, 37 | DEFAULT_LINE 38 | ); 39 | PALETTE_STATES = ImmutableList.of( 40 | RENDERTYPE_PALETTE_SLIDE, 41 | TRANSLUCENT_TRANSPARENCY, 42 | LEQUAL_DEPTH_TEST, 43 | CULL, 44 | LIGHTMAP, 45 | NO_OVERLAY, 46 | NO_LAYERING, 47 | MAIN_TARGET, 48 | DEFAULT_TEXTURING, 49 | COLOR_DEPTH_WRITE, 50 | DEFAULT_LINE 51 | ); 52 | } 53 | 54 | private final Runnable additionalSetupState; 55 | 56 | public SlideRenderType(int texture) { 57 | super(SlideShow.ID, DefaultVertexFormat.BLOCK, 58 | VertexFormat.Mode.QUADS, 256, false, true, 59 | CompositeState.builder() 60 | .setShaderState(RENDERTYPE_TEXT_SEE_THROUGH_SHADER) 61 | .setTransparencyState(TRANSLUCENT_TRANSPARENCY) 62 | .setDepthTestState(LEQUAL_DEPTH_TEST) 63 | .setCullState(CULL) 64 | .setLightmapState(LIGHTMAP) 65 | .setOverlayState(NO_OVERLAY) 66 | .setLayeringState(NO_LAYERING) 67 | .setOutputState(MAIN_TARGET) 68 | .setTexturingState(DEFAULT_TEXTURING) 69 | .setWriteMaskState(COLOR_DEPTH_WRITE) 70 | .setLineState(DEFAULT_LINE) 71 | .createCompositeState(true) 72 | ); 73 | this.additionalSetupState = () -> RenderSystem.setShaderTexture(0, texture); 74 | } 75 | 76 | public SlideRenderType(int imageTexture, int paletteTexture) { 77 | super(SlideShow.ID + "_palette", DefaultVertexFormat.BLOCK, 78 | VertexFormat.Mode.QUADS, 256, false, true, 79 | CompositeState.builder() 80 | .setShaderState(RENDERTYPE_PALETTE_SLIDE) 81 | .setTransparencyState(TRANSLUCENT_TRANSPARENCY) 82 | .setDepthTestState(LEQUAL_DEPTH_TEST) 83 | .setCullState(CULL) 84 | .setLightmapState(LIGHTMAP) 85 | .setOverlayState(NO_OVERLAY) 86 | .setLayeringState(NO_LAYERING) 87 | .setOutputState(MAIN_TARGET) 88 | .setTexturingState(DEFAULT_TEXTURING) 89 | .setWriteMaskState(COLOR_DEPTH_WRITE) 90 | .setLineState(DEFAULT_LINE) 91 | .createCompositeState(true)); 92 | var baseSetup = this.setupState; 93 | this.additionalSetupState = () -> { 94 | RenderSystem.setShaderTexture(0, imageTexture); 95 | RenderSystem.setShaderTexture(3, paletteTexture); 96 | }; 97 | } 98 | 99 | public SlideRenderType(ResourceLocation texture) { 100 | super(SlideShow.ID + "_icon", DefaultVertexFormat.BLOCK, 101 | VertexFormat.Mode.QUADS, 256, false, true, 102 | CompositeState.builder() 103 | .setShaderState(RENDERTYPE_TEXT_SEE_THROUGH_SHADER) 104 | .setTransparencyState(TRANSLUCENT_TRANSPARENCY) 105 | .setDepthTestState(LEQUAL_DEPTH_TEST) 106 | .setCullState(CULL) 107 | .setLightmapState(LIGHTMAP) 108 | .setOverlayState(NO_OVERLAY) 109 | .setLayeringState(NO_LAYERING) 110 | .setOutputState(MAIN_TARGET) 111 | .setTexturingState(DEFAULT_TEXTURING) 112 | .setWriteMaskState(COLOR_DEPTH_WRITE) 113 | .setLineState(DEFAULT_LINE) 114 | .createCompositeState(true)); 115 | var baseSetup = this.setupState; 116 | this.additionalSetupState = () -> RenderSystem.setShaderTexture(0, texture); 117 | } 118 | 119 | @Override 120 | public void setupRenderState() { 121 | super.setupRenderState(); 122 | this.additionalSetupState.run(); 123 | } 124 | 125 | @Override 126 | public boolean equals(Object o) { 127 | return this == o; 128 | } 129 | 130 | public static ShaderInstance getPaletteSlideShader() { 131 | return sPaletteSlideShader; 132 | } 133 | 134 | public static void setPaletteSlideShader(ShaderInstance paletteSlideShader) { 135 | sPaletteSlideShader = paletteSlideShader; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/screen/LazyWidget.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.screen; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import net.minecraft.FieldsAreNonnullByDefault; 5 | import net.minecraft.MethodsReturnNonnullByDefault; 6 | import net.minecraft.client.gui.components.events.GuiEventListener; 7 | import net.minecraft.client.gui.narration.NarratableEntry; 8 | 9 | import javax.annotation.Nullable; 10 | import javax.annotation.ParametersAreNonnullByDefault; 11 | import java.util.function.Function; 12 | import java.util.function.Supplier; 13 | 14 | @FieldsAreNonnullByDefault 15 | @MethodsReturnNonnullByDefault 16 | @ParametersAreNonnullByDefault 17 | public final class LazyWidget implements Supplier { 18 | private @Nullable T cached; 19 | private final Supplier initializer; 20 | private final Function refresher; 21 | 22 | private LazyWidget(U init, Function refresher, Function supplier) { 23 | this.refresher = old -> supplier.apply(refresher.apply(old)); 24 | this.initializer = () -> supplier.apply(init); 25 | this.cached = null; 26 | } 27 | 28 | public static LazyWidget of( 29 | U init, Function refresher, Function supplier) { 30 | return new LazyWidget<>(init, refresher, supplier); 31 | } 32 | 33 | public T refresh() { 34 | RenderSystem.assertOnRenderThread(); 35 | var obj = this.cached; 36 | if (obj == null) { 37 | obj = this.initializer.get(); 38 | } else { 39 | obj = this.refresher.apply(obj); 40 | } 41 | this.cached = obj; 42 | return obj; 43 | } 44 | 45 | @Override 46 | public T get() { 47 | RenderSystem.assertOnRenderThread(); 48 | var obj = this.cached; 49 | if (obj == null) { 50 | obj = this.initializer.get(); 51 | this.cached = obj; 52 | } 53 | return obj; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/screen/SlideItemScreen.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.screen; 2 | 3 | import com.mojang.blaze3d.systems.RenderSystem; 4 | import net.minecraft.ChatFormatting; 5 | import net.minecraft.FieldsAreNonnullByDefault; 6 | import net.minecraft.MethodsReturnNonnullByDefault; 7 | import net.minecraft.client.gui.GuiGraphics; 8 | import net.minecraft.client.gui.components.EditBox; 9 | import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; 10 | import net.minecraft.network.chat.Component; 11 | import net.minecraft.resources.ResourceLocation; 12 | import net.minecraft.world.entity.player.Inventory; 13 | import net.neoforged.neoforge.network.PacketDistributor; 14 | import org.apache.commons.lang3.StringUtils; 15 | import org.lwjgl.glfw.GLFW; 16 | import org.teacon.slides.SlideShow; 17 | import org.teacon.slides.inventory.SlideItemContainerMenu; 18 | import org.teacon.slides.item.SlideItem; 19 | import org.teacon.slides.network.SlideItemUpdatePacket; 20 | import org.teacon.slides.renderer.SlideState; 21 | import org.teacon.slides.url.ProjectorURL; 22 | 23 | import javax.annotation.Nullable; 24 | import javax.annotation.ParametersAreNonnullByDefault; 25 | import java.util.*; 26 | 27 | @FieldsAreNonnullByDefault 28 | @MethodsReturnNonnullByDefault 29 | @ParametersAreNonnullByDefault 30 | public final class SlideItemScreen extends AbstractContainerScreen { 31 | private static final ResourceLocation 32 | GUI_TEXTURE = SlideShow.id("textures/gui/projector_gui.png"); 33 | 34 | private static final int 35 | GUI_WIDTH = 512, 36 | GUI_HEIGHT = 384; 37 | 38 | private static final Component 39 | IMAGE_TEXT = Component.translatable("gui.slide_show.section.image"), 40 | URL_TEXT = Component.translatable("gui.slide_show.url"), 41 | SIZE_TEXT = Component.translatable("gui.slide_show.size"), 42 | SIZE_HINT_1 = Component.translatable("gui.slide_show.size_hint.two", 43 | Component.literal("contain").withStyle(ChatFormatting.AQUA), 44 | Component.literal("cover").withStyle(ChatFormatting.AQUA)) 45 | .withStyle(ChatFormatting.GRAY), 46 | SIZE_HINT_2 = Component.translatable("gui.slide_show.size_hint.contain_or_cover") 47 | .withStyle(ChatFormatting.GRAY), 48 | SIZE_HINT_3 = Component.translatable("gui.slide_show.size_hint.two", 49 | Component.literal("% auto").withStyle(ChatFormatting.AQUA), 50 | Component.literal("%").withStyle(ChatFormatting.AQUA)) 51 | .withStyle(ChatFormatting.GRAY), 52 | SIZE_HINT_4 = Component.translatable("gui.slide_show.size_hint.height_auto") 53 | .withStyle(ChatFormatting.GRAY), 54 | SIZE_HINT_5 = Component.translatable("gui.slide_show.size_hint.one", 55 | Component.literal("auto %").withStyle(ChatFormatting.AQUA)) 56 | .withStyle(ChatFormatting.GRAY), 57 | SIZE_HINT_6 = Component.translatable("gui.slide_show.size_hint.width_auto") 58 | .withStyle(ChatFormatting.GRAY), 59 | SIZE_HINT_7 = Component.translatable("gui.slide_show.size_hint.one", 60 | Component.literal("% %").withStyle(ChatFormatting.AQUA)) 61 | .withStyle(ChatFormatting.GRAY), 62 | SIZE_HINT_8 = Component.translatable("gui.slide_show.size_hint.both") 63 | .withStyle(ChatFormatting.GRAY); 64 | 65 | private static final int 66 | URL_MAX_LENGTH = 1 << 9, 67 | SIZE_MAX_LENGTH = 1 << 9; 68 | 69 | private final LazyWidget mURLInput; 70 | private final LazyWidget mSizeInput; 71 | 72 | private final SlideItemUpdatePacket mInitPacket; 73 | 74 | private SlideItem.Size mSlideSize; 75 | private @Nullable ProjectorURL mImgUrl; 76 | 77 | // refreshed after initialization 78 | 79 | private boolean mInvalidSize = true; 80 | private UrlStatus mUrlStatus = UrlStatus.NO_CONTENT; 81 | 82 | public SlideItemScreen(SlideItemContainerMenu menu, Inventory inventory, Component title) { 83 | super(menu, inventory, title); 84 | imageWidth = 230; 85 | imageHeight = 82; 86 | // initialize variables 87 | mInitPacket = menu.packet; 88 | mSlideSize = menu.packet.size(); 89 | mImgUrl = menu.packet.url().orElse(null); 90 | // url input 91 | mURLInput = LazyWidget.of(mInitPacket.url().map(u -> u.toUrl().toString()).orElse(""), EditBox::getValue, v -> { 92 | var input = new EditBox(font, leftPos + 27, topPos + 37, 197, 16, URL_TEXT); 93 | input.setEditable(mInitPacket.permissions().edit()); 94 | input.setMaxLength(URL_MAX_LENGTH); 95 | input.setResponder(text -> { 96 | try { 97 | mImgUrl = new ProjectorURL(text); 98 | if (mInitPacket.permissions().create()) { 99 | var blocked = SlideState.getImgBlocked(mImgUrl); 100 | mUrlStatus = blocked ? UrlStatus.BLOCKED : UrlStatus.NORMAL; 101 | } else { 102 | var allowed = SlideState.getImgAllowed(mImgUrl); 103 | mUrlStatus = allowed ? UrlStatus.NORMAL : UrlStatus.INVALID; 104 | } 105 | } catch (IllegalArgumentException e) { 106 | mImgUrl = null; 107 | mUrlStatus = StringUtils.isNotBlank(text) ? UrlStatus.INVALID : UrlStatus.NO_CONTENT; 108 | } 109 | input.setTextColor(switch (mUrlStatus) { 110 | case NORMAL, NO_CONTENT -> 0xE0E0E0; 111 | case BLOCKED -> 0xE0E04B; 112 | case INVALID -> 0xE04B4B; 113 | }); 114 | }); 115 | input.setValue(v); 116 | return input; 117 | }); 118 | // size input 119 | mSizeInput = LazyWidget.of(mInitPacket.size().toString(), EditBox::getValue, v -> { 120 | var input = new EditBox(font, leftPos + 27, topPos + 59, 197, 16, SIZE_TEXT); 121 | input.setEditable(mInitPacket.permissions().edit()); 122 | input.setMaxLength(SIZE_MAX_LENGTH); 123 | input.setResponder(text -> { 124 | try { 125 | mSlideSize = SlideItem.Size.parse(text); 126 | mInvalidSize = false; 127 | } catch (IllegalArgumentException e) { 128 | mInvalidSize = true; 129 | } 130 | input.setTextColor(mInvalidSize ? 0xE04B4B : 0xE0E0E0); 131 | }); 132 | input.setValue(v); 133 | return input; 134 | }); 135 | } 136 | 137 | @Override 138 | protected void init() { 139 | super.init(); 140 | 141 | addRenderableWidget(mURLInput.refresh()); 142 | addRenderableWidget(mSizeInput.refresh()); 143 | 144 | setInitialFocus(mURLInput.get()); 145 | } 146 | 147 | @Override 148 | public void removed() { 149 | super.removed(); 150 | var imgId = mInitPacket.imgUniqueId(); 151 | var urlFallback = (ProjectorURL) null; 152 | var urlRemoved = mUrlStatus == UrlStatus.NO_CONTENT && mInitPacket.url().isPresent(); 153 | var urlChanged = mUrlStatus == UrlStatus.NORMAL && !Objects.equals(mImgUrl, mInitPacket.url().orElse(null)); 154 | if (urlRemoved || urlChanged) { 155 | urlFallback = urlRemoved ? null : mImgUrl; 156 | // use default uuid to trigger update 157 | imgId = new UUID(0L, 0L); 158 | } 159 | PacketDistributor.sendToServer(new SlideItemUpdatePacket( 160 | mInitPacket.slotId(), mInitPacket.permissions(), imgId, 161 | Optional.empty(), Optional.ofNullable(urlFallback), mSlideSize)); 162 | } 163 | 164 | @Override 165 | public boolean keyPressed(int keyCode, int scanCode, int modifier) { 166 | var isEscape = false; 167 | 168 | if (keyCode == GLFW.GLFW_KEY_ESCAPE) { 169 | Objects.requireNonNull(Objects.requireNonNull(minecraft).player).closeContainer(); 170 | isEscape = true; 171 | } 172 | 173 | return isEscape 174 | || mURLInput.get().keyPressed(keyCode, scanCode, modifier) || mURLInput.get().canConsumeInput() 175 | || mSizeInput.get().keyPressed(keyCode, scanCode, modifier) || mSizeInput.get().canConsumeInput() 176 | || super.keyPressed(keyCode, scanCode, modifier); 177 | } 178 | 179 | @Override 180 | protected void renderBg(GuiGraphics gui, float partialTicks, int mouseX, int mouseY) { 181 | RenderSystem.setShaderColor(1F, 1F, 1F, 1F); 182 | RenderSystem.setShaderTexture(0, GUI_TEXTURE); 183 | gui.blit(GUI_TEXTURE, leftPos, topPos, 0F, 302F, imageWidth, imageHeight, GUI_WIDTH, GUI_HEIGHT); 184 | } 185 | 186 | @Override 187 | protected void renderLabels(GuiGraphics gui, int mouseX, int mouseY) { 188 | RenderSystem.enableBlend(); 189 | RenderSystem.defaultBlendFunc(); 190 | RenderSystem.setShaderTexture(0, GUI_TEXTURE); 191 | 192 | if (mUrlStatus == UrlStatus.INVALID || mUrlStatus == UrlStatus.BLOCKED) { 193 | gui.blit(GUI_TEXTURE, 7, 35, 7F, 277F, 18, 19, GUI_WIDTH, GUI_HEIGHT); 194 | } 195 | 196 | gui.drawString(font, IMAGE_TEXT.getVisualOrderText(), 116 - font.width(IMAGE_TEXT) / 2F, 15, 0x404040, false); 197 | } 198 | 199 | @Override 200 | protected void renderTooltip(GuiGraphics gui, int mouseX, int mouseY) { 201 | super.renderTooltip(gui, mouseX, mouseY); 202 | int offsetX = mouseX - leftPos, offsetY = mouseY - topPos; 203 | if (offsetX >= 7 && offsetY >= 35 && offsetX < 25 && offsetY < 54) { 204 | gui.renderComponentTooltip(font, this.getUrlTexts(), mouseX, mouseY); 205 | } else if (offsetX >= 7 && offsetY >= 57 && offsetX < 25 && offsetY < 76) { 206 | gui.renderComponentTooltip(font, List.of(SIZE_TEXT, 207 | Component.literal(""), SIZE_HINT_1, SIZE_HINT_2, 208 | Component.literal(""), SIZE_HINT_3, SIZE_HINT_4, 209 | Component.literal(""), SIZE_HINT_5, SIZE_HINT_6, 210 | Component.literal(""), SIZE_HINT_7, SIZE_HINT_8), mouseX, mouseY); 211 | } 212 | } 213 | 214 | @Override 215 | public void render(GuiGraphics gui, int mouseX, int mouseY, float partialTick) { 216 | super.render(gui, mouseX, mouseY, partialTick); 217 | this.renderTooltip(gui, mouseX, mouseY); 218 | } 219 | 220 | private List getUrlTexts() { 221 | var lastLogOptional = mInitPacket.oldLastLog(); 222 | var components = new ArrayList(); 223 | components.add(URL_TEXT); 224 | if (lastLogOptional.isPresent()) { 225 | var lastLog = lastLogOptional.get(); 226 | var mc = Objects.requireNonNull(this.minecraft); 227 | lastLog.addToTooltip(mc.level == null ? null : mc.level.dimension(), components); 228 | } 229 | return components; 230 | } 231 | 232 | private enum UrlStatus { 233 | NORMAL, BLOCKED, INVALID, NO_CONTENT 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/slide/ImageSlide.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.slide; 2 | 3 | import com.mojang.blaze3d.vertex.PoseStack; 4 | import net.minecraft.FieldsAreNonnullByDefault; 5 | import net.minecraft.MethodsReturnNonnullByDefault; 6 | import net.minecraft.client.renderer.MultiBufferSource; 7 | import net.minecraft.util.Mth; 8 | import org.joml.Vector2i; 9 | import org.teacon.slides.texture.TextureProvider; 10 | 11 | import javax.annotation.ParametersAreNonnullByDefault; 12 | import java.util.Optional; 13 | 14 | @FieldsAreNonnullByDefault 15 | @MethodsReturnNonnullByDefault 16 | @ParametersAreNonnullByDefault 17 | public final class ImageSlide implements Slide { 18 | 19 | private final TextureProvider mTexture; 20 | 21 | ImageSlide(TextureProvider texture) { 22 | mTexture = texture; 23 | } 24 | 25 | @Override 26 | public void render(MultiBufferSource source, PoseStack.Pose pose, 27 | int widthMicros, int heightMicros, double scaleWidthMicros, double scaleHeightMicros, 28 | int color, int light, int overlay, boolean front, boolean back, long tick, float partialTick) { 29 | // extract colors 30 | var red = (color >> 16) & 255; 31 | var green = (color >> 8) & 255; 32 | var blue = color & 255; 33 | var alpha = color >>> 24; 34 | // get vertex consumer 35 | var consumer = source.getBuffer(mTexture.updateAndGet(tick, partialTick)); 36 | // calculate image boundaries without clipping 37 | var left = Double.isNaN(scaleWidthMicros) ? 1D / 2D : (widthMicros - scaleWidthMicros) / 2D; 38 | var top = Double.isNaN(scaleHeightMicros) ? 1D / 2D : (heightMicros - scaleHeightMicros) / 2D; 39 | var right = Double.isNaN(scaleWidthMicros) ? 1D / 2D : (widthMicros + scaleWidthMicros) / 2D; 40 | var bottom = Double.isNaN(scaleHeightMicros) ? 1D / 2D : (heightMicros + scaleHeightMicros) / 2D; 41 | // clip image boundaries 42 | var x0 = (float) Math.clamp(left, 0D, widthMicros); 43 | var y0 = (float) Math.clamp(top, 0D, heightMicros); 44 | var x1 = (float) Math.clamp(right, 0D, widthMicros); 45 | var y1 = (float) Math.clamp(bottom, 0D, heightMicros); 46 | // calculate uv for rendering 47 | var u0 = left == right ? 0F : (float) Mth.clamp(Mth.inverseLerp(0D, left, right), 0D, 1D); 48 | var v0 = top == bottom ? 0F : (float) Mth.clamp(Mth.inverseLerp(0D, top, bottom), 0D, 1D); 49 | var u1 = left == right ? 1F : (float) Mth.clamp(Mth.inverseLerp(widthMicros, left, right), 0D, 1D); 50 | var v1 = top == bottom ? 1F : (float) Mth.clamp(Mth.inverseLerp(heightMicros, top, bottom), 0D, 1D); 51 | if (front) { 52 | consumer.addVertex(pose, x0, 4096F, y1) 53 | .setColor(red, green, blue, alpha) 54 | .setUv(u0, v1).setLight(light) 55 | .setNormal(pose, 0, 1, 0); 56 | consumer.addVertex(pose, x1, 4096F, y1) 57 | .setColor(red, green, blue, alpha) 58 | .setUv(u1, v1).setLight(light) 59 | .setNormal(pose, 0, 1, 0); 60 | consumer.addVertex(pose, x1, 4096F, y0) 61 | .setColor(red, green, blue, alpha) 62 | .setUv(u1, v0).setLight(light) 63 | .setNormal(pose, 0, 1, 0); 64 | consumer.addVertex(pose, x0, 4096F, y0) 65 | .setColor(red, green, blue, alpha) 66 | .setUv(u0, v0).setLight(light) 67 | .setNormal(pose, 0, 1, 0); 68 | } 69 | if (back) { 70 | consumer.addVertex(pose, x0, -4096F, y0) 71 | .setColor(red, green, blue, alpha) 72 | .setUv(u0, v0).setLight(light) 73 | .setNormal(pose, 0, -1, 0); 74 | consumer.addVertex(pose, x1, -4096F, y0) 75 | .setColor(red, green, blue, alpha) 76 | .setUv(u1, v0).setLight(light) 77 | .setNormal(pose, 0, -1, 0); 78 | consumer.addVertex(pose, x1, -4096F, y1) 79 | .setColor(red, green, blue, alpha) 80 | .setUv(u1, v1).setLight(light) 81 | .setNormal(pose, 0, -1, 0); 82 | consumer.addVertex(pose, x0, -4096F, y1) 83 | .setColor(red, green, blue, alpha) 84 | .setUv(u0, v1).setLight(light) 85 | .setNormal(pose, 0, -1, 0); 86 | } 87 | } 88 | 89 | @Override 90 | public void close() { 91 | mTexture.close(); 92 | } 93 | 94 | @Override 95 | public Optional getDimension() { 96 | return Optional.of(new Vector2i(mTexture.getWidth(), mTexture.getHeight())); 97 | } 98 | 99 | @Override 100 | public String getRecommendedName() { 101 | return mTexture.getRecommendedName(); 102 | } 103 | 104 | @Override 105 | public int getCPUMemorySize() { 106 | return mTexture.getCPUMemorySize(); 107 | } 108 | 109 | @Override 110 | public int getGPUMemorySize() { 111 | return mTexture.getGPUMemorySize(); 112 | } 113 | 114 | @Override 115 | public String toString() { 116 | return "ImageSlide{texture=" + mTexture + "}"; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/slide/Slide.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.slide; 2 | 3 | import com.mojang.blaze3d.vertex.PoseStack; 4 | import net.minecraft.FieldsAreNonnullByDefault; 5 | import net.minecraft.MethodsReturnNonnullByDefault; 6 | import net.minecraft.client.renderer.MultiBufferSource; 7 | import org.joml.Vector2i; 8 | import org.teacon.slides.renderer.SlideState; 9 | import org.teacon.slides.texture.TextureProvider; 10 | 11 | import javax.annotation.ParametersAreNonnullByDefault; 12 | import java.util.Optional; 13 | 14 | /** 15 | * Represents a slide drawable, with immutable storage. 16 | * 17 | * @see SlideState 18 | */ 19 | @FieldsAreNonnullByDefault 20 | @MethodsReturnNonnullByDefault 21 | @ParametersAreNonnullByDefault 22 | public sealed interface Slide extends AutoCloseable permits IconSlide, ImageSlide { 23 | void render(MultiBufferSource source, PoseStack.Pose pose, 24 | int widthMicros, int heightMicros, double scaleWidthMicros, double scaleHeightMicros, 25 | int color, int light, int overlay, boolean front, boolean back, long tick, float partialTick); 26 | 27 | @Override 28 | void close(); 29 | 30 | default Optional getDimension() { 31 | return Optional.empty(); 32 | } 33 | 34 | default String getRecommendedName() { 35 | return ""; 36 | } 37 | 38 | default int getCPUMemorySize() { 39 | return 0; 40 | } 41 | 42 | default int getGPUMemorySize() { 43 | return 0; 44 | } 45 | 46 | static Slide make(TextureProvider texture) { 47 | return new ImageSlide(texture); 48 | } 49 | 50 | static Slide empty() { 51 | return IconSlide.DEFAULT_EMPTY; 52 | } 53 | 54 | static Slide failed() { 55 | return IconSlide.DEFAULT_FAILED; 56 | } 57 | 58 | static Slide blocked() { 59 | return IconSlide.DEFAULT_BLOCKED; 60 | } 61 | 62 | static Slide loading() { 63 | return IconSlide.DEFAULT_LOADING; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/texture/AnimatedTextureProvider.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.texture; 2 | 3 | import com.mojang.blaze3d.platform.GlStateManager; 4 | import net.minecraft.FieldsAreNonnullByDefault; 5 | import net.minecraft.MethodsReturnNonnullByDefault; 6 | import org.lwjgl.system.MemoryUtil; 7 | import org.teacon.slides.renderer.SlideRenderType; 8 | 9 | import javax.annotation.Nonnull; 10 | import javax.annotation.Nullable; 11 | import javax.annotation.ParametersAreNonnullByDefault; 12 | import java.io.IOException; 13 | import java.nio.ByteBuffer; 14 | 15 | import static org.lwjgl.opengl.GL11C.*; 16 | import static org.lwjgl.opengl.GL12C.GL_CLAMP_TO_EDGE; 17 | 18 | @FieldsAreNonnullByDefault 19 | @MethodsReturnNonnullByDefault 20 | @ParametersAreNonnullByDefault 21 | public final class AnimatedTextureProvider implements TextureProvider { 22 | 23 | private static final LZWDecoder gRenderThreadDecoder = new LZWDecoder(); 24 | 25 | private final GIFDecoder mDecoder; 26 | 27 | private int mTexture; 28 | private final SlideRenderType mRenderType; 29 | 30 | private long mFrameStartTime; 31 | private long mFrameDelayTime; 32 | 33 | @Nullable 34 | private ByteBuffer mFrame; 35 | private final String mRecommendedName; 36 | 37 | private final int mCPUMemorySize; 38 | 39 | public AnimatedTextureProvider(String name, byte[] data) throws IOException { 40 | try { 41 | mDecoder = new GIFDecoder(ByteBuffer.wrap(data), gRenderThreadDecoder); 42 | final int width = mDecoder.getScreenWidth(); 43 | final int height = mDecoder.getScreenHeight(); 44 | if (width > MAX_TEXTURE_SIZE || height > MAX_TEXTURE_SIZE) { 45 | throw new IOException("Image is too big: " + width + "x" + height); 46 | } 47 | 48 | // COMPRESSED + HEAP (4) + NATIVE (4) + INDEX (1) 49 | mCPUMemorySize = data.length + (width * height * (4 + 4 + 1)); 50 | 51 | mFrame = MemoryUtil.memAlloc(width * height * 4); 52 | mFrameDelayTime = mDecoder.decodeNextFrame(mFrame); 53 | // we successfully decoded the first frame, then create a texture 54 | 55 | mTexture = GlStateManager._genTexture(); 56 | GlStateManager._bindTexture(mTexture); 57 | 58 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 59 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 60 | 61 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 62 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 63 | 64 | glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); 65 | glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); 66 | glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); 67 | glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 68 | 69 | // no mipmap generation 70 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, mFrame.rewind()); 71 | mRenderType = new SlideRenderType(mTexture); 72 | mRecommendedName = name; 73 | } catch (IOException e) { 74 | this.close(); 75 | throw e; 76 | } 77 | } 78 | 79 | @Nonnull 80 | @Override 81 | public SlideRenderType updateAndGet(long tick, float partialTick) { 82 | long timeMillis = (long) ((tick + partialTick) * 50); 83 | if (mFrameStartTime == 0) { 84 | mFrameStartTime = timeMillis; 85 | } else if (mFrameStartTime + mFrameDelayTime <= timeMillis) { 86 | try { 87 | final int width = getWidth(); 88 | final int height = getHeight(); 89 | assert mFrame != null; 90 | mFrameDelayTime = mDecoder.decodeNextFrame(mFrame); 91 | GlStateManager._bindTexture(mTexture); 92 | glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); 93 | glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); 94 | glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); 95 | glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 96 | glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, mFrame.rewind()); 97 | } catch (Exception e) { 98 | // If an exception occurs, keep the texture image as the last frame and no longer update 99 | // Don't use Long.MAX_VALUE in case of overflow 100 | mFrameDelayTime = Integer.MAX_VALUE; 101 | } 102 | // Don't skip frames if FPS is low 103 | mFrameStartTime = timeMillis; 104 | } 105 | return mRenderType; 106 | } 107 | 108 | @Override 109 | public int getWidth() { 110 | return mDecoder.getScreenWidth(); 111 | } 112 | 113 | @Override 114 | public int getHeight() { 115 | return mDecoder.getScreenHeight(); 116 | } 117 | 118 | @Override 119 | public int getCPUMemorySize() { 120 | return mCPUMemorySize; 121 | } 122 | 123 | @Override 124 | public int getGPUMemorySize() { 125 | return getWidth() * getHeight() * 4; 126 | } 127 | 128 | @Override 129 | public String getRecommendedName() { 130 | return mRecommendedName; 131 | } 132 | 133 | @Override 134 | public void close() { 135 | if (mTexture != 0) { 136 | GlStateManager._deleteTexture(mTexture); 137 | } 138 | mTexture = 0; 139 | MemoryUtil.memFree(mFrame); 140 | mFrame = null; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/texture/LZWDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Modern UI. 3 | * Copyright (C) 2019-2023 BloCamLimb. All rights reserved. 4 | * 5 | * Modern UI is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * Modern UI is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with Modern UI. If not, see . 17 | */ 18 | 19 | package org.teacon.slides.texture; 20 | 21 | import java.nio.BufferUnderflowException; 22 | import java.nio.ByteBuffer; 23 | 24 | /** 25 | * The GIF format uses LSB first. The initial code size can vary between 2 and 8 bits, inclusive. 26 | */ 27 | public final class LZWDecoder { 28 | 29 | private static final int MAX_TABLE_SIZE = 1 << 12; 30 | 31 | // input data buffer 32 | private ByteBuffer mData; 33 | 34 | private int mInitCodeSize; 35 | // ClearCode = (1 << L) + 0; 36 | // EndOfInfo = (1 << L) + 1; 37 | // NewCodeIndex = (1 << L) + 2; 38 | private int mClearCode; 39 | private int mEndOfInfo; 40 | 41 | private int mCodeSize; 42 | private int mCodeMask; 43 | 44 | private int mTableIndex; 45 | private int mPrevCode; 46 | 47 | private int mBlockPos; 48 | private int mBlockLength; 49 | private final byte[] mBlock = new byte[255]; 50 | private int mInData; 51 | private int mInBits; 52 | 53 | // table 54 | private final int[] mPrefix = new int[MAX_TABLE_SIZE]; 55 | private final byte[] mSuffix = new byte[MAX_TABLE_SIZE]; 56 | private final byte[] mInitial = new byte[MAX_TABLE_SIZE]; 57 | private final int[] mLength = new int[MAX_TABLE_SIZE]; 58 | private final byte[] mString = new byte[MAX_TABLE_SIZE]; 59 | 60 | public LZWDecoder() { 61 | } 62 | 63 | /** 64 | * Reset the decoder with the given input data buffer. 65 | * 66 | * @param data the compressed data 67 | * @return the string table 68 | */ 69 | public byte[] setData(ByteBuffer data, int initCodeSize) { 70 | mData = data; 71 | mBlockPos = 0; 72 | mBlockLength = 0; 73 | mInData = 0; 74 | mInBits = 0; 75 | mInitCodeSize = initCodeSize; 76 | mClearCode = 1 << mInitCodeSize; 77 | mEndOfInfo = mClearCode + 1; 78 | initTable(); 79 | return mString; 80 | } 81 | 82 | /** 83 | * Decode next string of data, which can be accessed by {@link #setData(ByteBuffer, int)} method. 84 | * 85 | * @return the length of string, or -1 on EOF 86 | */ 87 | public int readString() { 88 | int code = getNextCode(); 89 | if (code == mEndOfInfo) { 90 | return -1; 91 | } else if (code == mClearCode) { 92 | initTable(); 93 | code = getNextCode(); 94 | if (code == mEndOfInfo) { 95 | return -1; 96 | } 97 | } else { 98 | final int newSuffixIndex; 99 | if (code < mTableIndex) { 100 | newSuffixIndex = code; 101 | } else { 102 | newSuffixIndex = mPrevCode; 103 | if (code != mTableIndex) { 104 | return -1; 105 | } 106 | } 107 | 108 | if (mTableIndex < MAX_TABLE_SIZE) { 109 | int tableIndex = mTableIndex; 110 | int prevCode = mPrevCode; 111 | 112 | mPrefix[tableIndex] = prevCode; 113 | mSuffix[tableIndex] = mInitial[newSuffixIndex]; 114 | mInitial[tableIndex] = mInitial[prevCode]; 115 | mLength[tableIndex] = mLength[prevCode] + 1; 116 | 117 | ++mTableIndex; 118 | if ((mTableIndex == (1 << mCodeSize)) && (mTableIndex < MAX_TABLE_SIZE)) { 119 | ++mCodeSize; 120 | mCodeMask = (1 << mCodeSize) - 1; 121 | } 122 | } 123 | } 124 | // reverse 125 | int c = code; 126 | int len = mLength[c]; 127 | for (int i = len - 1; i >= 0; i--) { 128 | mString[i] = mSuffix[c]; 129 | c = mPrefix[c]; 130 | } 131 | 132 | mPrevCode = code; 133 | return len; 134 | } 135 | 136 | private void initTable() { 137 | int size = 1 << mInitCodeSize; 138 | for (int i = 0; i < size; i++) { 139 | mPrefix[i] = -1; 140 | mSuffix[i] = (byte) i; 141 | mInitial[i] = (byte) i; 142 | mLength[i] = 1; 143 | } 144 | 145 | for (int i = size; i < MAX_TABLE_SIZE; i++) { 146 | mPrefix[i] = -1; 147 | mSuffix[i] = 0; 148 | mInitial[i] = 0; 149 | mLength[i] = 1; 150 | } 151 | 152 | mCodeSize = mInitCodeSize + 1; 153 | mCodeMask = (1 << mCodeSize) - 1; 154 | mTableIndex = size + 2; 155 | mPrevCode = 0; 156 | } 157 | 158 | private int getNextCode() { 159 | while (mInBits < mCodeSize) { 160 | if (mBlockPos == mBlockLength) { 161 | mBlockPos = 0; 162 | try { 163 | if ((mBlockLength = mData.get() & 0xFF) > 0) { 164 | mData.get(mBlock, 0, mBlockLength); 165 | } else { 166 | return mEndOfInfo; 167 | } 168 | } catch (BufferUnderflowException e) { 169 | return mEndOfInfo; 170 | } 171 | } 172 | mInData |= (mBlock[mBlockPos++] & 0xFF) << mInBits; 173 | mInBits += 8; 174 | } 175 | int code = mInData & mCodeMask; 176 | mInBits -= mCodeSize; 177 | mInData >>>= mCodeSize; 178 | return code; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/texture/StaticTextureProvider.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.texture; 2 | 3 | import com.mojang.blaze3d.platform.GlStateManager; 4 | import com.mojang.blaze3d.platform.NativeImage; 5 | import net.minecraft.FieldsAreNonnullByDefault; 6 | import net.minecraft.MethodsReturnNonnullByDefault; 7 | import org.teacon.slides.renderer.SlideRenderType; 8 | 9 | import javax.annotation.Nonnull; 10 | import javax.annotation.Nullable; 11 | import javax.annotation.ParametersAreNonnullByDefault; 12 | import java.io.IOException; 13 | import java.nio.IntBuffer; 14 | 15 | import static org.lwjgl.opengl.GL11C.*; 16 | import static org.lwjgl.opengl.GL12C.*; 17 | import static org.lwjgl.opengl.GL14C.GL_TEXTURE_LOD_BIAS; 18 | import static org.lwjgl.opengl.GL30C.glGenerateMipmap; 19 | import static org.lwjgl.opengl.GL33C.GL_TEXTURE_SWIZZLE_RGBA; 20 | 21 | @FieldsAreNonnullByDefault 22 | @MethodsReturnNonnullByDefault 23 | @ParametersAreNonnullByDefault 24 | public final class StaticTextureProvider implements TextureProvider { 25 | 26 | private int mTexture; 27 | private final SlideRenderType mRenderType; 28 | private final String mRecommendedName; 29 | private final int mWidth, mHeight; 30 | 31 | public StaticTextureProvider(String name, NativeImage image, @Nullable int[] rgbaSwizzle) throws IOException { 32 | try { 33 | mWidth = image.getWidth(); 34 | mHeight = image.getHeight(); 35 | if (mWidth > MAX_TEXTURE_SIZE || mHeight > MAX_TEXTURE_SIZE) { 36 | throw new IOException("Image is too big: " + mWidth + "x" + mHeight); 37 | } 38 | final int maxLevel = Math.min(31 - Integer.numberOfLeadingZeros(Math.max(mWidth, mHeight)), 4); 39 | 40 | mTexture = GlStateManager._genTexture(); 41 | GlStateManager._bindTexture(mTexture); 42 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_LOD, 0); 43 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LOD, maxLevel); 44 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); 45 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, maxLevel); 46 | glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, 0.0F); 47 | 48 | for (int level = 0; level <= maxLevel; ++level) { 49 | glTexImage2D(GL_TEXTURE_2D, level, 50 | GL_RGBA8, mWidth >> level, mHeight >> level, 51 | 0, GL_RED, GL_UNSIGNED_BYTE, (IntBuffer) null); 52 | } 53 | 54 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 55 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); 56 | 57 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 58 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 59 | 60 | // row pixels 0 means width 61 | glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); 62 | glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0); 63 | glPixelStorei(GL_UNPACK_SKIP_ROWS, 0); 64 | glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 65 | 66 | try (image) { 67 | glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mWidth, mHeight, GL_RGBA, GL_UNSIGNED_BYTE, image.pixels); 68 | if (rgbaSwizzle != null) { 69 | // rearrange argb / 0rgb to rgba 70 | glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, rgbaSwizzle); 71 | } 72 | } 73 | 74 | // auto generate mipmap 75 | glGenerateMipmap(GL_TEXTURE_2D); 76 | mRenderType = new SlideRenderType(mTexture); 77 | mRecommendedName = name; 78 | } catch (IOException e) { 79 | this.close(); 80 | throw e; 81 | } 82 | } 83 | 84 | @Nonnull 85 | @Override 86 | public SlideRenderType updateAndGet(long tick, float partialTick) { 87 | return mRenderType; 88 | } 89 | 90 | @Override 91 | public int getWidth() { 92 | return mWidth; 93 | } 94 | 95 | @Override 96 | public int getHeight() { 97 | return mHeight; 98 | } 99 | 100 | @Override 101 | public int getCPUMemorySize() { 102 | return 0; 103 | } 104 | 105 | @Override 106 | public int getGPUMemorySize() { 107 | return mWidth * mHeight * 4 * 4 / 3; 108 | } 109 | 110 | @Override 111 | public String getRecommendedName() { 112 | return mRecommendedName; 113 | } 114 | 115 | @Override 116 | public void close() { 117 | if (mTexture != 0) { 118 | GlStateManager._deleteTexture(mTexture); 119 | } 120 | mTexture = 0; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/texture/TextureProvider.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.texture; 2 | 3 | import net.minecraft.FieldsAreNonnullByDefault; 4 | import net.minecraft.MethodsReturnNonnullByDefault; 5 | import org.teacon.slides.renderer.SlideRenderType; 6 | 7 | import javax.annotation.Nonnull; 8 | import javax.annotation.ParametersAreNonnullByDefault; 9 | 10 | @FieldsAreNonnullByDefault 11 | @MethodsReturnNonnullByDefault 12 | @ParametersAreNonnullByDefault 13 | public interface TextureProvider extends AutoCloseable { 14 | 15 | int MAX_TEXTURE_SIZE = 4096; 16 | 17 | @Nonnull 18 | SlideRenderType updateAndGet(long tick, float partialTick); 19 | 20 | int getWidth(); 21 | 22 | int getHeight(); 23 | 24 | int getCPUMemorySize(); 25 | 26 | int getGPUMemorySize(); 27 | 28 | String getRecommendedName(); 29 | 30 | @Override 31 | void close(); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/texture/WebPDecoder.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.texture; 2 | 3 | import com.luciad.imageio.webp.WebPReadParam; 4 | import com.mojang.blaze3d.platform.NativeImage; 5 | import org.apache.commons.lang3.ArrayUtils; 6 | import org.lwjgl.system.MemoryUtil; 7 | 8 | import javax.annotation.Nonnull; 9 | import javax.imageio.ImageIO; 10 | import java.awt.image.DataBufferInt; 11 | import java.awt.image.DirectColorModel; 12 | import java.io.ByteArrayInputStream; 13 | import java.io.IOException; 14 | import java.nio.ByteBuffer; 15 | import java.nio.ByteOrder; 16 | import java.util.Objects; 17 | 18 | import static org.lwjgl.opengl.GL11C.*; 19 | 20 | public final class WebPDecoder { 21 | public static boolean checkMagic(@Nonnull byte[] buf) { 22 | if (buf.length >= 12) { 23 | var wr = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN); 24 | var riff = wr.getInt() == 0x46464952; // RIFF in LITTLE ENDIAN 25 | var size = wr.getInt() == buf.length - 8; // SIZE - 8 of image 26 | var webp = wr.getInt() == 0x50424557; // WEBP in LITTLE ENDIAN 27 | var vp8_ = ArrayUtils.contains(new int[]{0x58385056, 0x4C385056, 0x20385056}, wr.getInt()); // VP8[XL\x20] in LITTLE ENDIAN; 28 | return riff && size && webp && vp8_; 29 | } 30 | return false; 31 | } 32 | 33 | public static NativeImage toNativeImage(@Nonnull byte[] buf, int[] rgbaSwizzle) throws IOException { 34 | try (var stream = new ByteArrayInputStream(buf)) { 35 | try (var imageStream = ImageIO.createImageInputStream(stream)) { 36 | var readParam = new WebPReadParam(); 37 | readParam.setBypassFiltering(true); 38 | var reader = ImageIO.getImageReadersByMIMEType("image/webp").next(); 39 | reader.setInput(imageStream); 40 | var image = reader.read(0, readParam); 41 | if (!(image.getColorModel() instanceof DirectColorModel imageColorModel)) { 42 | var colorModelType = image.getColorModel().getClass(); 43 | throw new IOException("unrecognized color model type: " + colorModelType.getName()); 44 | } 45 | var bigEndian = ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN; 46 | for (var i = 0; i < rgbaSwizzle.length; ++i) { 47 | var mask = switch (i) { 48 | case 0 -> imageColorModel.getRedMask(); 49 | case 1 -> imageColorModel.getGreenMask(); 50 | case 2 -> imageColorModel.getBlueMask(); 51 | case 3 -> imageColorModel.getAlphaMask(); 52 | default -> 0x00000000; 53 | }; 54 | rgbaSwizzle[i] = switch (mask) { 55 | case 0x00000000 -> GL_ZERO; 56 | case 0xFF000000 -> bigEndian ? GL_RED : GL_ALPHA; 57 | case 0x00FF0000 -> bigEndian ? GL_GREEN : GL_BLUE; 58 | case 0x0000FF00 -> bigEndian ? GL_BLUE : GL_GREEN; 59 | case 0x000000FF -> bigEndian ? GL_ALPHA : GL_RED; 60 | default -> throw new IOException("unrecognized rgba mask[%d]: 0x%08X".formatted(i, mask)); 61 | }; 62 | } 63 | if (!(image.getData().getDataBuffer() instanceof DataBufferInt imageDataBuffer)) { 64 | var bufferType = image.getData().getDataBuffer().getClass(); 65 | throw new IOException("unrecognized data buffer type: " + bufferType.getName()); 66 | } 67 | var nativeImage = new NativeImage(image.getWidth(), image.getHeight(), false); 68 | var nativeBuffer = MemoryUtil.memByteBuffer(nativeImage.pixels, Math.toIntExact(nativeImage.size)); 69 | Objects.requireNonNull(nativeBuffer).asIntBuffer().put(imageDataBuffer.getData()); 70 | return nativeImage; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/url/ProjectorURL.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.url; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import io.netty.buffer.ByteBuf; 5 | import net.minecraft.FieldsAreNonnullByDefault; 6 | import net.minecraft.MethodsReturnNonnullByDefault; 7 | import net.minecraft.network.codec.ByteBufCodecs; 8 | import net.minecraft.network.codec.StreamCodec; 9 | 10 | import javax.annotation.ParametersAreNonnullByDefault; 11 | import java.net.URI; 12 | import java.util.Optional; 13 | 14 | import static com.google.common.base.Preconditions.checkArgument; 15 | 16 | @FieldsAreNonnullByDefault 17 | @MethodsReturnNonnullByDefault 18 | @ParametersAreNonnullByDefault 19 | public final class ProjectorURL { 20 | public static final StreamCodec> OPTIONAL_STREAM_CODEC; 21 | 22 | private static final ImmutableSet ALLOWED_SCHEMES = ImmutableSet.of("http", "https"); 23 | private static final String NOT_ALLOWED_SCHEME = "the url scheme is neither http nor https"; 24 | 25 | static { 26 | OPTIONAL_STREAM_CODEC = ByteBufCodecs.STRING_UTF8.map(str -> { 27 | if (str.isEmpty()) { 28 | return Optional.empty(); 29 | } 30 | return Optional.of(new ProjectorURL(str)); 31 | }, opt -> { 32 | if (opt.isPresent()) { 33 | return opt.get().toUrl().toString(); 34 | } 35 | return ""; 36 | }); 37 | } 38 | 39 | private final String urlString; 40 | private final URI urlObject; 41 | 42 | public ProjectorURL(String urlString) { 43 | this.urlObject = URI.create(urlString); 44 | this.urlString = this.urlObject.normalize().toASCIIString(); 45 | checkArgument(ALLOWED_SCHEMES.contains(this.urlObject.getScheme()), NOT_ALLOWED_SCHEME); 46 | } 47 | 48 | public URI toUrl() { 49 | return this.urlObject; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return this.urlString; 55 | } 56 | 57 | @Override 58 | public int hashCode() { 59 | return this.urlString.hashCode(); 60 | } 61 | 62 | @Override 63 | public boolean equals(Object o) { 64 | return this == o || o instanceof ProjectorURL that && this.urlString.equals(that.urlString); 65 | } 66 | 67 | public enum Status { 68 | UNKNOWN, BLOCKED, ALLOWED; 69 | 70 | public boolean isBlocked() { 71 | return this == BLOCKED; 72 | } 73 | 74 | public boolean isAllowed() { 75 | return this == ALLOWED; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/url/ProjectorURLArgument.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.url; 2 | 3 | import com.mojang.brigadier.StringReader; 4 | import com.mojang.brigadier.arguments.ArgumentType; 5 | import com.mojang.brigadier.context.CommandContext; 6 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 7 | import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; 8 | import com.mojang.brigadier.suggestion.Suggestions; 9 | import com.mojang.brigadier.suggestion.SuggestionsBuilder; 10 | import com.mojang.datafixers.util.Either; 11 | import net.minecraft.FieldsAreNonnullByDefault; 12 | import net.minecraft.MethodsReturnNonnullByDefault; 13 | import net.minecraft.commands.CommandSourceStack; 14 | import net.minecraft.commands.synchronization.SingletonArgumentInfo; 15 | import net.minecraft.network.chat.Component; 16 | 17 | import javax.annotation.ParametersAreNonnullByDefault; 18 | import java.util.UUID; 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | import static net.minecraft.commands.synchronization.ArgumentTypeInfos.registerByClass; 22 | 23 | @FieldsAreNonnullByDefault 24 | @MethodsReturnNonnullByDefault 25 | @ParametersAreNonnullByDefault 26 | public final class ProjectorURLArgument implements ArgumentType> { 27 | private static final DynamicCommandExceptionType INVALID_URL = new DynamicCommandExceptionType(v -> Component.translatable("argument.slide_show.projector_url.error", v)); 28 | 29 | public static SingletonArgumentInfo create() { 30 | return registerByClass(ProjectorURLArgument.class, SingletonArgumentInfo.contextFree(ProjectorURLArgument::new)); 31 | } 32 | 33 | @SuppressWarnings("unchecked") 34 | public static Either getUrl(CommandContext context, String argument) { 35 | return context.getArgument(argument, Either.class); 36 | } 37 | 38 | @Override 39 | public Either parse(StringReader reader) throws CommandSyntaxException { 40 | var stringBuilder = new StringBuilder(reader.getRemainingLength()); 41 | while (reader.canRead() && reader.peek() != ' ') { 42 | stringBuilder.append(reader.read()); 43 | } 44 | var string = stringBuilder.toString(); 45 | try { 46 | return Either.left(UUID.fromString(string)); 47 | } catch (IllegalArgumentException e1) { 48 | try { 49 | return Either.right(new ProjectorURL(string)); 50 | } catch (IllegalArgumentException e2) { 51 | throw INVALID_URL.create(string); 52 | } 53 | } 54 | } 55 | 56 | @Override 57 | public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { 58 | // TODO: fetch urls 59 | return ArgumentType.super.listSuggestions(context, builder); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/teacon/slides/url/ProjectorURLPatternArgument.java: -------------------------------------------------------------------------------- 1 | package org.teacon.slides.url; 2 | 3 | import com.mojang.brigadier.StringReader; 4 | import com.mojang.brigadier.arguments.ArgumentType; 5 | import com.mojang.brigadier.context.CommandContext; 6 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 7 | import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; 8 | import com.mojang.brigadier.suggestion.Suggestions; 9 | import com.mojang.brigadier.suggestion.SuggestionsBuilder; 10 | import net.minecraft.FieldsAreNonnullByDefault; 11 | import net.minecraft.MethodsReturnNonnullByDefault; 12 | import net.minecraft.commands.CommandSourceStack; 13 | import net.minecraft.commands.synchronization.SingletonArgumentInfo; 14 | import net.minecraft.network.chat.Component; 15 | import org.teacon.urlpattern.URLPattern; 16 | 17 | import javax.annotation.ParametersAreNonnullByDefault; 18 | import java.util.concurrent.CompletableFuture; 19 | 20 | import static net.minecraft.commands.synchronization.ArgumentTypeInfos.registerByClass; 21 | 22 | @FieldsAreNonnullByDefault 23 | @MethodsReturnNonnullByDefault 24 | @ParametersAreNonnullByDefault 25 | public final class ProjectorURLPatternArgument implements ArgumentType { 26 | private static final DynamicCommandExceptionType INVALID_URL_PATTERN = new DynamicCommandExceptionType(v -> Component.translatable("argument.slide_show.projector_url_pattern.error", v)); 27 | 28 | public static SingletonArgumentInfo create() { 29 | return registerByClass(ProjectorURLPatternArgument.class, SingletonArgumentInfo.contextFree(ProjectorURLPatternArgument::new)); 30 | } 31 | 32 | public static URLPattern getUrl(CommandContext context, String argument) { 33 | return context.getArgument(argument, URLPattern.class); 34 | } 35 | 36 | @Override 37 | public URLPattern parse(StringReader reader) throws CommandSyntaxException { 38 | var stringBuilder = new StringBuilder(reader.getRemainingLength()); 39 | while (reader.canRead() && reader.peek() != ' ') { 40 | stringBuilder.append(reader.read()); 41 | } 42 | var string = stringBuilder.toString(); 43 | try { 44 | return new URLPattern(string); 45 | } catch (IllegalArgumentException e) { 46 | throw INVALID_URL_PATTERN.create(string); 47 | } 48 | } 49 | 50 | @Override 51 | public CompletableFuture listSuggestions(CommandContext context, SuggestionsBuilder builder) { 52 | // TODO: fetch urls 53 | return ArgumentType.super.listSuggestions(context, builder); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/accesstransformer.cfg: -------------------------------------------------------------------------------- 1 | public-f net.minecraft.client.renderer.RenderType$CompositeRenderType 2 | 3 | public-f com.mojang.blaze3d.platform.NativeImage size 4 | public-f com.mojang.blaze3d.platform.NativeImage pixels 5 | public com.mojang.blaze3d.platform.NativeImage (Lcom/mojang/blaze3d/platform/NativeImage$Format;IIZJ)V 6 | protected net.minecraft.client.renderer.RenderType$CompositeRenderType (Ljava/lang/String;Lcom/mojang/blaze3d/vertex/VertexFormat;Lcom/mojang/blaze3d/vertex/VertexFormat$Mode;IZZLnet/minecraft/client/renderer/RenderType$CompositeState;)V 7 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/neoforge.mods.toml: -------------------------------------------------------------------------------- 1 | # This is an example neoforge.mods.toml file. It contains the data relating to the loading mods. 2 | # There are several mandatory fields (#mandatory), and many more that are optional (#optional). 3 | # The overall format is standard TOML format, v0.5.0. 4 | # Note that there are a couple of TOML lists in this file. 5 | # Find more information on toml format here: https://github.com/toml-lang/toml 6 | # The name of the mod loader type to load - for regular FML @Mod mods it should be javafml 7 | modLoader="javafml" #mandatory 8 | 9 | # A version range to match for said mod loader - for regular FML @Mod it will be the FML version. This is currently 2. 10 | loaderVersion="${loader_version_range}" #mandatory 11 | 12 | # The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. 13 | # Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. 14 | license="${mod_license}" 15 | 16 | # A URL to refer people to when problems occur with this mod 17 | #issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional 18 | 19 | # A list of mods - how many allowed here is determined by the individual mod loader 20 | [[mods]] #mandatory 21 | 22 | # The modid of the mod 23 | modId="${mod_id}" #mandatory 24 | 25 | # The version number of the mod 26 | version="${mod_version}" #mandatory 27 | 28 | # A display name for the mod 29 | displayName="${mod_name}" #mandatory 30 | 31 | # A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/ 32 | #updateJSONURL="https://change.me.example.invalid/updates.json" #optional 33 | 34 | # A URL for the "homepage" for this mod, displayed in the mod UI 35 | #displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional 36 | 37 | # A file name (in the root of the mod JAR) containing a logo for display 38 | #logoFile="examplemod.png" #optional 39 | 40 | # A text field displayed in the mod UI 41 | #credits="" #optional 42 | 43 | # A text field displayed in the mod UI 44 | authors="${mod_authors}" #optional 45 | 46 | # The description text for the mod (multi line!) (#mandatory) 47 | description='''${mod_description}''' 48 | 49 | # The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. 50 | #[[mixins]] 51 | #config="${mod_id}.mixins.json" 52 | 53 | # The [[accessTransformers]] block allows you to declare where your AT file is. 54 | # If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg 55 | #[[accessTransformers]] 56 | #file="META-INF/accesstransformer.cfg" 57 | 58 | # The coremods config file path is not configurable and is always loaded from META-INF/coremods.json 59 | 60 | # A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. 61 | [[dependencies.${mod_id}]] #optional 62 | # the modid of the dependency 63 | modId="neoforge" #mandatory 64 | # The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). 65 | # 'required' requires the mod to exist, 'optional' does not 66 | # 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning 67 | type="required" #mandatory 68 | # Optional field describing why the dependency is required or why it is incompatible 69 | # reason="..." 70 | # The version range of the dependency 71 | versionRange="${neo_version_range}" #mandatory 72 | # An ordering relationship for the dependency. 73 | # BEFORE - This mod is loaded BEFORE the dependency 74 | # AFTER - This mod is loaded AFTER the dependency 75 | ordering="NONE" 76 | # Side this dependency is applied on - BOTH, CLIENT, or SERVER 77 | side="BOTH" 78 | 79 | # Here's another dependency 80 | [[dependencies.${mod_id}]] 81 | modId="minecraft" 82 | type="required" 83 | # This version range declares a minimum of the current minecraft version up to but not including the next major version 84 | versionRange="${minecraft_version_range}" 85 | ordering="NONE" 86 | side="BOTH" 87 | 88 | # Features are specific properties of the game environment, that you may want to declare you require. This example declares 89 | # that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't 90 | # stop your mod loading on the server for example. 91 | #[features.${mod_id}] 92 | #openGLVersion="[3.2,)" 93 | -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/blockstates/projector.json: -------------------------------------------------------------------------------- 1 | { 2 | "variants": { 3 | "base=up,facing=north": { "model": "slide_show:block/projector_inverted", "x": 180 }, 4 | "base=down,facing=north": { "model": "slide_show:block/projector" }, 5 | "base=up,facing=east": { "model": "slide_show:block/projector_inverted", "x": 180, "y": 90 }, 6 | "base=down,facing=east": { "model": "slide_show:block/projector", "y": 90 }, 7 | "base=up,facing=south": { "model": "slide_show:block/projector_inverted", "x": 180, "y": 180 }, 8 | "base=down,facing=south": { "model": "slide_show:block/projector", "y": 180 }, 9 | "base=up,facing=west": { "model": "slide_show:block/projector_inverted", "x": 180, "y": 270 }, 10 | "base=down,facing=west": { "model": "slide_show:block/projector", "y": 270 }, 11 | "base=up,facing=up": { "model": "slide_show:block/projector_down", "x": 180, "y": 180 }, 12 | "base=down,facing=up": { "model": "slide_show:block/projector_up", "y": 180 }, 13 | "base=up,facing=down": { "model": "slide_show:block/projector_up", "x": 180, "y": 180 }, 14 | "base=down,facing=down": { "model": "slide_show:block/projector_down", "y": 180 } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "argument.slide_show.projector_url.error": "Input %s is not a valid url", 3 | "argument.slide_show.projector_url_pattern.error": "Input %s is not a valid url pattern", 4 | 5 | "item.slide_show.slide_item": "Slide Item", 6 | "block.slide_show.projector": "Slide Show Projector", 7 | "item.slide_show.slide_item.hint": "Hold it on your hand and right click to edit the URL and related information of this slide", 8 | 9 | "command.slide_show.scroll_up.success": "Successfully scrolled up %s slide item(s)", 10 | "command.slide_show.scroll_up.not_enough": "There are no more slide items to scroll up", 11 | "command.slide_show.scroll_down.success": "Successfully scrolled down %s slide item(s)", 12 | "command.slide_show.scroll_down.not_enough": "There are no more slide items to scroll down", 13 | "command.slide_show.scroll_current.success": "The current slide number is %s", 14 | "command.slide_show.scroll_amount.success": "The total amount of slide item(s) is %s", 15 | "command.slide_show.prefetch_projector_url.success": "Successfully prefetched the requested url (%s)", 16 | "command.slide_show.list_projector_url.success": "Successfully found %s url(s): %s", 17 | "command.slide_show.block_projector_url.success": "The projector url (%s) is successfully blocked", 18 | "command.slide_show.unblock_projector_url.success": "The projector url (%s) is successfully unblocked", 19 | 20 | "command.slide_show.failed.url_not_exist": "The input url or uuid (%s) cannot be used for further operations", 21 | "command.slide_show.failed.perm_not_exist": "You do not have related permission for executing the command", 22 | 23 | "gui.slide_show.url": "Image Link", 24 | "gui.slide_show.size": "Image Size Related to Projector", 25 | "gui.slide_show.color": "Projection Color", 26 | "gui.slide_show.width": "Projection Width", 27 | "gui.slide_show.height": "Projection Height", 28 | "gui.slide_show.offset_x": "Move Left/Right", 29 | "gui.slide_show.offset_y": "Move Up/Down", 30 | "gui.slide_show.offset_z": "Move Backward/Forward", 31 | "gui.slide_show.move_to_begin": "Start at the Beginning", 32 | "gui.slide_show.move_upward": "Switch to Previous Slide", 33 | "gui.slide_show.move_downward": "Switch to Next Slide", 34 | "gui.slide_show.move_to_end": "Switch to the Last Slide", 35 | "gui.slide_show.flip": "Flip Horizontally", 36 | "gui.slide_show.rotate": "Rotate Clockwise", 37 | "gui.slide_show.single_double_sided": "Switch Single-Sided / Double-Sided", 38 | 39 | "gui.slide_show.size_hint.one": "%s", 40 | "gui.slide_show.size_hint.two": "%s or %s", 41 | "gui.slide_show.size_hint.contain_or_cover": "While preserving its aspect ratio, the image is rendered at the largest size contained within, or covering, the projection area defined by the projector", 42 | "gui.slide_show.size_hint.height_auto": "The width will be stretched to the specified percentage while the height is computed using the image's corresponding aspect ratio", 43 | "gui.slide_show.size_hint.width_auto": "The height will be stretched to the specified percentage while the width is computed using the image's corresponding aspect ratio", 44 | "gui.slide_show.size_hint.both": "The width and the height will be stretched to the specified percentage of the projection area defined by the projector", 45 | 46 | "gui.slide_show.log_comment": "at %s by %s", 47 | "gui.slide_show.log_comment_nobody": "at %s", 48 | "gui.slide_show.log_message.slide_show.create_url": "The image was created in the server", 49 | "gui.slide_show.log_message.slide_show.block_url": "The image was blocked in the server", 50 | "gui.slide_show.log_message.slide_show.erase_url": "The image was erased in the server", 51 | "gui.slide_show.log_message.slide_show.unblock_url": "The image was unblocked in the server", 52 | "gui.slide_show.log_message.slide_show.attach_url_to_projector": "The image was attached to a projector", 53 | "gui.slide_show.log_message.slide_show.attach_url_to_item": "The image was attached to a slide item", 54 | "gui.slide_show.log_message.slide_show.detach_url_from_projector": "The image was detached from a projector", 55 | "gui.slide_show.log_message.slide_show.detach_url_from_item": "The image was detached from a slide item", 56 | "gui.slide_show.log_message.slide_show.create_url.in_another_level": "The image was created in the server", 57 | "gui.slide_show.log_message.slide_show.block_url.in_another_level": "The image was blocked in the server", 58 | "gui.slide_show.log_message.slide_show.erase_url.in_another_level": "The image was erased in the server", 59 | "gui.slide_show.log_message.slide_show.unblock_url.in_another_level": "The image was unblocked in the server", 60 | "gui.slide_show.log_message.slide_show.attach_url_to_projector.in_another_level": "The image was attached to a projector in another dimension", 61 | "gui.slide_show.log_message.slide_show.attach_url_to_item.in_another_level": "The image was attached to a slide item in another dimension", 62 | "gui.slide_show.log_message.slide_show.detach_url_from_projector.in_another_level": "The image was detached from a projector in another dimension", 63 | "gui.slide_show.log_message.slide_show.detach_url_from_item.in_another_level": "The image was detached from a slide item in another dimension", 64 | "gui.slide_show.log_message.slide_show.create_url.in_current_level": "The image was created in the server", 65 | "gui.slide_show.log_message.slide_show.block_url.in_current_level": "The image was blocked in the server", 66 | "gui.slide_show.log_message.slide_show.erase_url.in_current_level": "The image was erased in the server", 67 | "gui.slide_show.log_message.slide_show.unblock_url.in_current_level": "The image was unblocked in the server", 68 | "gui.slide_show.log_message.slide_show.attach_url_to_projector.in_current_level": "The image was attached to a projector located at %s", 69 | "gui.slide_show.log_message.slide_show.attach_url_to_item.in_current_level": "The image was attached to a slide item located at %s", 70 | "gui.slide_show.log_message.slide_show.detach_url_from_projector.in_current_level": "The image was detached from a projector located at %s", 71 | "gui.slide_show.log_message.slide_show.detach_url_from_item.in_current_level": "The image was detached from a slide item located at %s", 72 | 73 | "gui.slide_show.section.image": "Image Properties", 74 | "gui.slide_show.section.size": "Width & Height", 75 | "gui.slide_show.section.offset": "Offset", 76 | "gui.slide_show.section.others.first": "Colors &", 77 | "gui.slide_show.section.others.second": "Transforms", 78 | "gui.slide_show.section.container_hint": "Slides for Projection", 79 | "gui.slide_show.section.container_hint_1": "Above are the slides waiting for projection, sorted by natural order", 80 | "gui.slide_show.section.container_hint_2": "Below are the slides already been projected, sorted by natural order", 81 | "gui.slide_show.section.container_hint_3": "Redstone signals can be used for controlling" 82 | } 83 | -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/lang/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "argument.slide_show.projector_url.error": "输入 %s 不是一个合法的 URL", 3 | "argument.slide_show.projector_url_pattern.error": "输入 %s 不是一个合法的 URL Pattern", 4 | 5 | "item.slide_show.slide_item": "幻灯片", 6 | "block.slide_show.projector": "幻灯片投影仪", 7 | "item.slide_show.slide_item.hint": "放在手上并使用右键可编辑该幻灯片的 URL 等相关信息", 8 | 9 | "command.slide_show.scroll_up.success": "已成功向上滚动 %s 个幻灯片", 10 | "command.slide_show.scroll_up.not_enough": "没有可供向上滚动的幻灯片了", 11 | "command.slide_show.scroll_down.success": "已成功向下滚动 %s 个幻灯片", 12 | "command.slide_show.scroll_down.not_enough": "没有可供向下滚动的幻灯片了", 13 | "command.slide_show.scroll_current.success": "当前的幻灯片序号为 %s", 14 | "command.slide_show.scroll_amount.success": "当前的幻灯片总数为 %s", 15 | "command.slide_show.prefetch_projector_url.success": "已成功预加载请求的 URL(%s)", 16 | "command.slide_show.list_projector_url.success": "已成功找到 %s 个 URL:%s", 17 | "command.slide_show.block_projector_url.success": "已成功屏蔽幻灯片 URL(%s)", 18 | "command.slide_show.unblock_projector_url.success": "已成功为幻灯片 URL(%s)解除屏蔽", 19 | 20 | "command.slide_show.failed.url_not_exist": "输入 URL 或 UUID(%s)无法用于进一步操作", 21 | "command.slide_show.failed.perm_not_exist": "你没有执行命令的相关权限", 22 | 23 | "gui.slide_show.url": "图片链接", 24 | "gui.slide_show.size": "幻灯片相对投影的大小", 25 | "gui.slide_show.color": "投影颜色", 26 | "gui.slide_show.width": "投影宽度", 27 | "gui.slide_show.height": "投影高度", 28 | "gui.slide_show.offset_x": "左右平移", 29 | "gui.slide_show.offset_y": "上下平移", 30 | "gui.slide_show.offset_z": "前后平移", 31 | "gui.slide_show.move_to_begin": "从头开始投影", 32 | "gui.slide_show.move_upward": "切换到上一张幻灯片", 33 | "gui.slide_show.move_downward": "切换到下一张幻灯片", 34 | "gui.slide_show.move_to_end": "切换到最后一张幻灯片", 35 | "gui.slide_show.flip": "水平翻转", 36 | "gui.slide_show.rotate": "顺时针旋转", 37 | "gui.slide_show.single_double_sided": "切换单面/双面", 38 | 39 | "gui.slide_show.size_hint.one": "%s", 40 | "gui.slide_show.size_hint.two": "%s 或 %s", 41 | "gui.slide_show.size_hint.contain_or_cover": "缩放图片以完全覆盖(cover)或最大包含(contain)投影仪定义的投影区域", 42 | "gui.slide_show.size_hint.height_auto": "宽度使用指定百分比,未指定的高度依图片固有长宽比例计算", 43 | "gui.slide_show.size_hint.width_auto": "高度使用指定百分比,未指定的宽度依图片固有长宽比例计算", 44 | "gui.slide_show.size_hint.both": "宽度和高度将拉伸至投影仪定义的投影区域的指定百分比", 45 | 46 | "gui.slide_show.log_comment": "%s(操作者:%s)", 47 | "gui.slide_show.log_comment_nobody": "%s", 48 | "gui.slide_show.log_message.slide_show.create_url": "该图片在服务器的创建时间:", 49 | "gui.slide_show.log_message.slide_show.block_url": "该图片在服务器的屏蔽时间:", 50 | "gui.slide_show.log_message.slide_show.erase_url": "该图片在服务器的抹除时间:", 51 | "gui.slide_show.log_message.slide_show.unblock_url": "该图片在服务器的解除屏蔽时间:", 52 | "gui.slide_show.log_message.slide_show.attach_url_to_projector": "该图片上次绑定到投影仪上的时间:", 53 | "gui.slide_show.log_message.slide_show.attach_url_to_item": "该图片上次绑定到幻灯片上的时间:", 54 | "gui.slide_show.log_message.slide_show.detach_url_from_projector": "该图片上次从投影仪上撤下的时间:", 55 | "gui.slide_show.log_message.slide_show.detach_url_from_item": "该图片上次从幻灯片上撤下的时间:", 56 | "gui.slide_show.log_message.slide_show.create_url.in_another_level": "该图片在服务器的创建时间:", 57 | "gui.slide_show.log_message.slide_show.block_url.in_another_level": "该图片在服务器的屏蔽时间:", 58 | "gui.slide_show.log_message.slide_show.erase_url.in_another_level": "该图片在服务器的抹除时间:", 59 | "gui.slide_show.log_message.slide_show.unblock_url.in_another_level": "该图片在服务器的解除屏蔽时间:", 60 | "gui.slide_show.log_message.slide_show.attach_url_to_projector.in_another_level": "该图片上次绑定到其他维度投影仪上的时间:", 61 | "gui.slide_show.log_message.slide_show.attach_url_to_item.in_another_level": "该图片上次绑定到其他维度幻灯片上的时间:", 62 | "gui.slide_show.log_message.slide_show.detach_url_from_projector.in_another_level": "该图片上次从其他维度的投影仪上撤下的时间:", 63 | "gui.slide_show.log_message.slide_show.detach_url_from_item.in_another_level": "该图片上次从其他维度的幻灯片上撤下的时间:", 64 | "gui.slide_show.log_message.slide_show.create_url.in_current_level": "该图片在服务器的创建时间:", 65 | "gui.slide_show.log_message.slide_show.block_url.in_current_level": "该图片在服务器的屏蔽时间:", 66 | "gui.slide_show.log_message.slide_show.erase_url.in_current_level": "该图片在服务器的抹除时间:", 67 | "gui.slide_show.log_message.slide_show.unblock_url.in_current_level": "该图片在服务器的解除屏蔽时间:", 68 | "gui.slide_show.log_message.slide_show.attach_url_to_projector.in_current_level": "该图片上次绑定到当前维度 %s 处的时间:", 69 | "gui.slide_show.log_message.slide_show.attach_url_to_item.in_current_level": "该图片上次绑定到当前维度 %s 处的时间:", 70 | "gui.slide_show.log_message.slide_show.detach_url_from_projector.in_current_level": "该图片上次从当前维度 %s 处撤下的时间:", 71 | "gui.slide_show.log_message.slide_show.detach_url_from_item.in_current_level": "该图片上次从当前维度 %s 处撤下的时间:", 72 | 73 | "gui.slide_show.section.image": "图片属性", 74 | "gui.slide_show.section.size": "投影长宽", 75 | "gui.slide_show.section.offset": "投影平移量", 76 | "gui.slide_show.section.others.first": "投影颜色", 77 | "gui.slide_show.section.others.second": "及投影变换", 78 | "gui.slide_show.section.container_hint": "用于播放的幻灯片", 79 | "gui.slide_show.section.container_hint_1": "上方为等待播放的幻灯片,从前到后自然排序", 80 | "gui.slide_show.section.container_hint_2": "下方为已经播放的幻灯片,从前到后自然排序", 81 | "gui.slide_show.section.container_hint_3": "可使用红石信号控制播放" 82 | } 83 | -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/lang/zh_tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "argument.slide_show.projector_url.error": "輸入 %s 不是一個合法的 URL", 3 | "argument.slide_show.projector_url_pattern.error": "輸入 %s 不是一個合法的 URL Pattern", 4 | 5 | "item.slide_show.slide_item": "幻燈片", 6 | "block.slide_show.projector": "幻燈片投影機", 7 | 8 | "command.slide_show.prefetch_projector_url.success": "已成功預載入請求的 URL(%s)", 9 | "command.slide_show.list_projector_url.success": "已成功找到 %s 個 URL:%s", 10 | "command.slide_show.block_projector_url.success": "已成功隱藏幻燈片 URL(%s)", 11 | "command.slide_show.unblock_projector_url.success": "已成功為幻燈片 URL(%s)解除隱藏", 12 | 13 | "command.slide_show.failed.url_not_exist": "輸入 URL 或 UUID(%s)無法用於進一步操作", 14 | 15 | "gui.slide_show.url": "圖片鏈接", 16 | "gui.slide_show.color": "投影顏色", 17 | "gui.slide_show.width": "投影寬度", 18 | "gui.slide_show.height": "投影高度", 19 | "gui.slide_show.offset_x": "左右平移", 20 | "gui.slide_show.offset_y": "上下平移", 21 | "gui.slide_show.offset_z": "前後平移", 22 | "gui.slide_show.flip": "水平翻轉", 23 | "gui.slide_show.rotate": "順時針旋轉", 24 | "gui.slide_show.single_double_sided": "切換單面/雙面", 25 | 26 | "gui.slide_show.log_comment": "%s(操作者:%s)", 27 | "gui.slide_show.log_comment_nobody": "%s", 28 | "gui.slide_show.log_message.slide_show.create_url": "該圖片在伺服器的創建時間:", 29 | "gui.slide_show.log_message.slide_show.block_url": "該圖片在伺服器的隱藏時間:", 30 | "gui.slide_show.log_message.slide_show.erase_url": "該圖片在伺服器的抹除時間:", 31 | "gui.slide_show.log_message.slide_show.unblock_url": "該圖片在伺服器的解除隱藏時間:", 32 | "gui.slide_show.log_message.slide_show.attach_url_to_projector": "該圖片上次綁定到投影機上的時間:", 33 | "gui.slide_show.log_message.slide_show.detach_url_from_projector": "該圖片上次從投影機上撤下的時間:", 34 | "gui.slide_show.log_message.slide_show.create_url.in_another_level": "該圖片在伺服器的創建時間:", 35 | "gui.slide_show.log_message.slide_show.block_url.in_another_level": "該圖片在伺服器的隱藏時間:", 36 | "gui.slide_show.log_message.slide_show.erase_url.in_another_level": "該圖片在伺服器的抹除時間:", 37 | "gui.slide_show.log_message.slide_show.unblock_url.in_another_level": "該圖片在伺服器的解除隱藏時間:", 38 | "gui.slide_show.log_message.slide_show.attach_url_to_projector.in_another_level": "該圖片上次綁定到其他維度投影機上的時間:", 39 | "gui.slide_show.log_message.slide_show.detach_url_from_projector.in_another_level": "該圖片上次從其他維度的投影機上撤下的時間:", 40 | "gui.slide_show.log_message.slide_show.create_url.in_current_level": "該圖片在伺服器的創建時間:", 41 | "gui.slide_show.log_message.slide_show.block_url.in_current_level": "該圖片在伺服器的隱藏時間:", 42 | "gui.slide_show.log_message.slide_show.erase_url.in_current_level": "該圖片在伺服器的抹除時間:", 43 | "gui.slide_show.log_message.slide_show.unblock_url.in_current_level": "該圖片在伺服器的解除隱藏時間:", 44 | "gui.slide_show.log_message.slide_show.attach_url_to_projector.in_current_level": "該圖片上次綁定到當前維度 %s 處的時間:", 45 | "gui.slide_show.log_message.slide_show.detach_url_from_projector.in_current_level": "該圖片上次從當前維度 %s 處撤下的時間:", 46 | 47 | "gui.slide_show.section.image": "圖片屬性", 48 | "gui.slide_show.section.offset": "幻燈片平移", 49 | "gui.slide_show.section.others": "其他" 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/models/item/projector.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "slide_show:block/projector" 3 | } -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/models/item/slide_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:item/generated", 3 | "textures": { 4 | "layer0": "slide_show:item/slide_item" 5 | }, 6 | "overrides": [{ 7 | "predicate": { "slide_show:url_status": 0.5 }, 8 | "model": "slide_show:item/slide_item_blocked" 9 | }, { 10 | "predicate": { "slide_show:url_status": 1.0 }, 11 | "model": "slide_show:item/slide_item_allowed" 12 | }] 13 | } -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/models/item/slide_item_allowed.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:item/generated", 3 | "textures": { 4 | "layer0": "slide_show:item/slide_item_allowed" 5 | } 6 | } -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/models/item/slide_item_blocked.json: -------------------------------------------------------------------------------- 1 | { 2 | "parent": "minecraft:item/generated", 3 | "textures": { 4 | "layer0": "slide_show:item/slide_item_blocked" 5 | } 6 | } -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/shaders/core/rendertype_palette_slide.fsh: -------------------------------------------------------------------------------- 1 | #version 150 2 | 3 | uniform sampler2D Sampler0; 4 | uniform sampler2D Sampler3; 5 | 6 | uniform vec4 ColorModulator; 7 | 8 | in vec4 vertexColor; 9 | in vec2 texCoord0; 10 | 11 | out vec4 fragColor; 12 | 13 | void main() { 14 | float index = texture(Sampler0, texCoord0).r * 255.0; 15 | vec4 color = texture(Sampler3, vec2((index + 0.5) / 256.0, 0.5)) * vertexColor; 16 | if (color.a < 0.1) { 17 | discard; 18 | } 19 | fragColor = color * ColorModulator; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/shaders/core/rendertype_palette_slide.json: -------------------------------------------------------------------------------- 1 | { 2 | "blend": { 3 | "func": "add", 4 | "srcrgb": "srcalpha", 5 | "dstrgb": "1-srcalpha" 6 | }, 7 | "vertex": "rendertype_text_see_through", 8 | "fragment": "slide_show:rendertype_palette_slide", 9 | "attributes": [ 10 | "Position", 11 | "Color", 12 | "UV0" 13 | ], 14 | "samplers": [ 15 | { "name": "Sampler0" }, 16 | { "name": "Sampler3" } 17 | ], 18 | "uniforms": [ 19 | { "name": "ModelViewMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, 20 | { "name": "ProjMat", "type": "matrix4x4", "count": 16, "values": [ 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0 ] }, 21 | { "name": "ColorModulator", "type": "float", "count": 4, "values": [ 1.0, 1.0, 1.0, 1.0 ] } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/shaders/post/projector_outline.json: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | "slide_show:swap", 4 | "slide_show:final" 5 | ], 6 | "passes": [{ 7 | "name": "minecraft:entity_outline", 8 | "intarget": "slide_show:final", 9 | "outtarget": "slide_show:swap" 10 | }, { 11 | "name": "minecraft:blur", 12 | "intarget": "slide_show:swap", 13 | "outtarget": "slide_show:final", 14 | "uniforms": [{ 15 | "name": "BlurDir", 16 | "values": [1.0, 0.0] 17 | }, { 18 | "name": "Radius", 19 | "values": [2.0] 20 | }] 21 | }, { 22 | "name": "minecraft:blur", 23 | "intarget": "slide_show:final", 24 | "outtarget": "slide_show:swap", 25 | "uniforms": [{ 26 | "name": "BlurDir", 27 | "values": [0.0, 1.0] 28 | }, { 29 | "name": "Radius", 30 | "values": [2.0] 31 | }] 32 | }, { 33 | "name": "minecraft:blit", 34 | "intarget": "slide_show:swap", 35 | "outtarget": "slide_show:final" 36 | }] 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/block/projector_base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/block/projector_base.png -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/block/projector_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/block/projector_main.png -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/gui/projector_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/gui/projector_gui.png -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/gui/slide_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/gui/slide_default.png -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/gui/slide_icon_blocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/gui/slide_icon_blocked.png -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/gui/slide_icon_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/gui/slide_icon_empty.png -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/gui/slide_icon_failed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/gui/slide_icon_failed.png -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/gui/slide_icon_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/gui/slide_icon_loading.png -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/item/slide_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/item/slide_item.png -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/item/slide_item_allowed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/item/slide_item_allowed.png -------------------------------------------------------------------------------- /src/main/resources/assets/slide_show/textures/item/slide_item_blocked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teaconmc/SlideShow/c12519ffc7db2428759b47b7505a6b83bfba4c15/src/main/resources/assets/slide_show/textures/item/slide_item_blocked.png -------------------------------------------------------------------------------- /src/main/resources/data/slide_show/loot_table/blocks/projector.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "minecraft:block", 3 | "pools": [ 4 | { 5 | "rolls": 1, 6 | "bonus_rolls": 0.0, 7 | "entries": [ 8 | { 9 | "type": "minecraft:item", 10 | "name": "slide_show:projector", 11 | "functions": [ 12 | { 13 | "function": "minecraft:copy_components", 14 | "include": [ 15 | "minecraft:container" 16 | ], 17 | "source": "block_entity" 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | ], 24 | "random_sequence": "slide_show:blocks/projector" 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/data/slide_show/tags/item/slide_items.json: -------------------------------------------------------------------------------- 1 | { 2 | "values": [ 3 | "slide_show:slide_item" 4 | ] 5 | } --------------------------------------------------------------------------------