├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images.png ├── settings.gradle └── src └── main ├── java └── space │ └── essem │ └── image2map │ ├── CardboardWarning.java │ ├── Image2Map.java │ ├── ImageData.java │ ├── config │ └── Image2MapConfig.java │ ├── gui │ ├── MapGui.java │ └── PreviewGui.java │ ├── mixin │ ├── BundleItemMixin.java │ ├── EntityPassengersSetS2CPacketAccessor.java │ ├── ItemFrameEntityMixin.java │ ├── PlayerEntityMixin.java │ └── ServerPlayNetworkHandlerMixin.java │ └── renderer │ └── MapRenderer.java └── resources ├── assets └── image2map │ ├── gui │ └── poly_buttons.png │ └── icon.png ├── fabric.mod.json ├── image2map.accesswidener └── image2map.mixins.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Automatically build the project and run any configured tests for every push 2 | # and submitted pull request. This can help catch issues that only occur on 3 | # certain platforms or Java versions, and provides a first line of defence 4 | # against bad commits. 5 | 6 | name: build 7 | on: [pull_request, push] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | # Use these Java versions 14 | java: [ 15 | 21 # Minimum supported by Minecraft 16 | ] 17 | # and run on both Linux and Windows 18 | os: [ubuntu-20.04] 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - name: checkout repository 22 | uses: actions/checkout@v4 23 | - name: setup jdk ${{ matrix.java }} 24 | uses: actions/setup-java@v1 25 | with: 26 | java-version: ${{ matrix.java }} 27 | - name: make gradle wrapper executable 28 | if: ${{ runner.os != 'Windows' }} 29 | run: chmod +x ./gradlew 30 | - name: Build and publish with Gradle 31 | run: ./gradlew build 32 | - name: capture build artifacts 33 | if: ${{ runner.os == 'Linux' && matrix.java == '21' }} # Only upload artifacts built from latest java on one OS 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: Artifacts 37 | path: build/libs/ 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/cache@v4 15 | with: 16 | path: | 17 | ~/.gradle/loom-cache 18 | ~/.gradle/caches 19 | ~/.gradle/wrapper 20 | key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 21 | restore-keys: | 22 | gradle- 23 | - uses: actions/checkout@v4 24 | - name: Set up JDK 25 | uses: actions/setup-java@v1 26 | with: 27 | java-version: 21 28 | 29 | - name: Grant execute permission for gradlew 30 | run: chmod +x gradlew 31 | 32 | - name: Build and publish with Gradle 33 | run: ./gradlew build publish 34 | env: 35 | MAVEN_URL: ${{ secrets.MAVEN_URL }} 36 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 37 | MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} 38 | CURSEFORGE: ${{ secrets.CURSEFORGE }} 39 | MODRINTH: ${{ secrets.MODRINTH }} 40 | CHANGELOG: ${{ github.event.release.body }} 41 | - name: Upload GitHub release 42 | uses: AButler/upload-release-assets@v3.0 43 | with: 44 | files: 'build/libs/*.jar;!build/libs/*-sources.jar;!build/libs/*-dev.jar' 45 | repo-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # gradle 2 | 3 | .gradle/ 4 | build/ 5 | out/ 6 | classes/ 7 | 8 | # eclipse 9 | 10 | *.launch 11 | 12 | # idea 13 | 14 | .idea/ 15 | *.iml 16 | *.ipr 17 | *.iws 18 | 19 | # vscode 20 | 21 | .settings/ 22 | .vscode/ 23 | bin/ 24 | .classpath 25 | .project 26 | 27 | # fabric 28 | 29 | run/ 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Essem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image2Map 2 | 3 | 4 | 5 | A Fabric mod that allows you to render an image onto a map(s), allowing you to display it on your vanilla compatible server! 6 | 7 | ![Some images](https://raw.githubusercontent.com/TheEssem/Image2Map/master/images.png) 8 | ![More images](https://imgur.com/qy8JF5B.png) 9 | 10 | ## Commands: 11 | - `/image2map create <[dither/none]> ` - Creates map of specified size (in pixels, single map is 128x128), with/without dither, using provided image 12 | - `/image2map create <[dither/none]> ` - Creates map with/without dither, using provided image 13 | - `/image2map preview ` - Creates dynamic preview before saving the map as item 14 | 15 | ### Commands in preview mode 16 | - `/dither <[dither/none]>` - Changes dither mode 17 | - `/size` - Displays current size 18 | - `/size ` - Changes size of map to specified one (in pixels, single map is 128x128) 19 | - `/grid` - Toggles visibility of map grid 20 | - `/save` - Exits preview and saves map as items 21 | - `/exit` - Exits preview without saving 22 | 23 | ### Multimaps 24 | In case of maps bigger than 128x128 pixels, you will get them in a bundle. 25 | Clicking with it on top-left corner of item frames will put all maps in correct places. 26 | Works for any item frame on wall, floor or ceiling. 27 | 28 | ## Downloads: 29 | - Modrinth: https://modrinth.com/mod/image2map 30 | - Curseforge: https://www.curseforge.com/minecraft/mc-mods/image2map 31 | 32 | 33 | Will you port this to Forge/Bukkit/Paper? 34 | ![no lol](https://i.imgur.com/tf5W69k.png) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'fabric-loom' version '1.10.+' 3 | id 'maven-publish' 4 | id "com.modrinth.minotaur" version "2.+" 5 | } 6 | 7 | sourceCompatibility = JavaVersion.VERSION_21 8 | targetCompatibility = JavaVersion.VERSION_21 9 | 10 | archivesBaseName = project.archives_base_name 11 | version = project.mod_version 12 | group = project.maven_group 13 | 14 | repositories{ 15 | maven { url "https://maven.shedaniel.me/" } 16 | maven { url "https://maven.nucleoid.xyz/" } 17 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } 18 | mavenCentral() 19 | } 20 | 21 | dependencies { 22 | //to change the versions see the gradle.properties file 23 | minecraft "com.mojang:minecraft:${project.minecraft_version}" 24 | mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" 25 | modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" 26 | 27 | // Fabric API. This is technically optional, but you probably want it anyway. 28 | modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" 29 | 30 | modImplementation include("eu.pb4:map-canvas-api:${project.mapcanvas_version}") 31 | modImplementation include("eu.pb4:sgui:${project.sgui_version}") 32 | modImplementation include("me.lucko:fabric-permissions-api:0.3.3") 33 | implementation include("com.twelvemonkeys.imageio:imageio-webp:3.12.0") 34 | } 35 | 36 | loom { 37 | accessWidenerPath = file("src/main/resources/image2map.accesswidener") 38 | } 39 | 40 | processResources { 41 | inputs.property "version", project.version 42 | 43 | filesMatching("fabric.mod.json") { 44 | expand "version": project.version 45 | } 46 | } 47 | 48 | tasks.withType(JavaCompile).configureEach { 49 | // ensure that the encoding is set to UTF-8, no matter what the system default is 50 | // this fixes some edge cases with special characters not displaying correctly 51 | // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html 52 | // If Javadoc is generated, this must be specified in that task too. 53 | it.options.encoding = "UTF-8" 54 | 55 | // Minecraft 1.18 (1.18-pre2) upwards uses Java 17. 56 | it.options.release = 21 57 | } 58 | 59 | java { 60 | // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task 61 | // if it is present. 62 | // If you remove this line, sources will not be generated. 63 | withSourcesJar() 64 | } 65 | 66 | jar { 67 | from("LICENSE") { 68 | rename { "${it}_${project.archivesBaseName}"} 69 | } 70 | } 71 | 72 | // configure the maven publication 73 | publishing { 74 | publications { 75 | mavenJava(MavenPublication) { 76 | // add all the jars that should be included when publishing to maven 77 | artifact(remapJar) { 78 | builtBy remapJar 79 | } 80 | artifact(sourcesJar) { 81 | builtBy remapSourcesJar 82 | } 83 | } 84 | } 85 | 86 | // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. 87 | repositories { 88 | // Add repositories to publish to here. 89 | // Notice: This block does NOT have the same function as the block in the top level. 90 | // The repositories here will be used for publishing your artifact, not for 91 | // retrieving dependencies. 92 | } 93 | } 94 | 95 | if (System.getenv("MODRINTH")) { 96 | modrinth { 97 | token = System.getenv("MODRINTH") 98 | projectId = '13RpG7dA' 99 | versionNumber = "" + version 100 | versionType = "release" 101 | changelog = System.getenv("CHANGELOG") 102 | // On fabric, use 'remapJar' instead of 'jar' 103 | uploadFile = remapJar 104 | gameVersions = [((String) project.minecraft_version)] 105 | loaders = ["fabric", "quilt"] 106 | } 107 | 108 | remapJar { 109 | finalizedBy project.tasks.modrinth 110 | } 111 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Done to increase the memory available to gradle. 2 | org.gradle.jvmargs=-Xmx1G 3 | 4 | # Fabric Properties 5 | # check these on https://fabricmc.net/versions.html 6 | minecraft_version=1.21.5 7 | yarn_mappings=1.21.5+build.1 8 | loader_version=0.16.10 9 | 10 | # Mod Properties 11 | mod_version = 0.9.1+1.21.5 12 | maven_group = space.essem 13 | archives_base_name = image2map 14 | 15 | # Dependencies 16 | fabric_version=0.119.1+1.21.5 17 | mapcanvas_version=0.5.1+1.21.5 18 | sgui_version=1.9.0+1.21.5 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patbox/Image2Map/a8e22b008be35c45cf1cf46df6fd45906826734e/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.13-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 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patbox/Image2Map/a8e22b008be35c45cf1cf46df6fd45906826734e/images.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | gradlePluginPortal() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/CardboardWarning.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map; 2 | 3 | import com.mojang.logging.LogUtils; 4 | import net.fabricmc.loader.api.FabricLoader; 5 | import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; 6 | import org.slf4j.Logger; 7 | 8 | import java.util.List; 9 | 10 | public class CardboardWarning implements PreLaunchEntrypoint { 11 | public static final String MOD_NAME = "Image2Map"; 12 | public static final Logger LOGGER = LogUtils.getLogger(); 13 | 14 | // Overwrite heavy and generally problematic bukkit implementation 15 | private static final List BROKEN_BUKKIT_IMPL = List.of("cardboard", "banner", "arclight"); 16 | 17 | public static final String BUKKIT_NAME; 18 | public static final boolean LOADED; 19 | 20 | static { 21 | var name = ""; 22 | var loaded = false; 23 | for (var x : BROKEN_BUKKIT_IMPL) { 24 | var m = FabricLoader.getInstance().getModContainer(x); 25 | if (m.isPresent()) { 26 | name = m.get().getMetadata().getName() + " (" + x + ")"; 27 | loaded = true; 28 | break; 29 | } 30 | } 31 | 32 | BUKKIT_NAME = name; 33 | LOADED = loaded; 34 | } 35 | 36 | @Override 37 | public void onPreLaunch() { 38 | checkAndAnnounce(); 39 | } 40 | 41 | public static void checkAndAnnounce() { 42 | if (LOADED) { 43 | LOGGER.error("=============================================="); 44 | LOGGER.error(""); 45 | LOGGER.error(BUKKIT_NAME + " detected! This mod is known to cause issues!"); 46 | LOGGER.error(MOD_NAME + " might not work correctly because of it."); 47 | LOGGER.error("You won't get any support as long as it's present!"); 48 | LOGGER.error(""); 49 | LOGGER.error("Read more at: https://gist.github.com/Patbox/e44844294c358b614d347d369b0fc3bf"); 50 | LOGGER.error(""); 51 | LOGGER.error("=============================================="); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/Image2Map.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map; 2 | 3 | import com.mojang.brigadier.arguments.IntegerArgumentType; 4 | import com.mojang.brigadier.arguments.StringArgumentType; 5 | import com.mojang.brigadier.context.CommandContext; 6 | import com.mojang.brigadier.exceptions.CommandSyntaxException; 7 | import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; 8 | import com.mojang.brigadier.suggestion.SuggestionProvider; 9 | import com.mojang.brigadier.suggestion.Suggestions; 10 | import com.mojang.brigadier.suggestion.SuggestionsBuilder; 11 | import com.mojang.logging.LogUtils; 12 | import eu.pb4.sgui.api.GuiHelpers; 13 | import me.lucko.fabric.api.permissions.v0.Permissions; 14 | import net.fabricmc.api.ModInitializer; 15 | import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; 16 | import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; 17 | import net.fabricmc.loader.api.FabricLoader; 18 | import net.minecraft.component.DataComponentTypes; 19 | import net.minecraft.component.type.BundleContentsComponent; 20 | import net.minecraft.component.type.LoreComponent; 21 | import net.minecraft.component.type.NbtComponent; 22 | import net.minecraft.entity.Entity; 23 | import net.minecraft.entity.data.DataTracker; 24 | import net.minecraft.entity.decoration.ItemFrameEntity; 25 | import net.minecraft.entity.player.PlayerEntity; 26 | import net.minecraft.item.BundleItem; 27 | import net.minecraft.item.ItemStack; 28 | import net.minecraft.item.Items; 29 | import net.minecraft.nbt.*; 30 | import net.minecraft.server.command.ServerCommandSource; 31 | import net.minecraft.text.HoverEvent; 32 | import net.minecraft.text.Style; 33 | import net.minecraft.text.Text; 34 | import net.minecraft.util.Formatting; 35 | import net.minecraft.util.Hand; 36 | import net.minecraft.util.math.BlockPos; 37 | import net.minecraft.util.math.BlockPos.Mutable; 38 | import net.minecraft.util.math.Box; 39 | import net.minecraft.util.math.Direction; 40 | import net.minecraft.util.math.MathHelper; 41 | import net.minecraft.util.math.Vec3d; 42 | import net.minecraft.world.World; 43 | import org.jetbrains.annotations.Nullable; 44 | import org.slf4j.Logger; 45 | import space.essem.image2map.config.Image2MapConfig; 46 | import space.essem.image2map.gui.PreviewGui; 47 | import space.essem.image2map.renderer.MapRenderer; 48 | 49 | import javax.imageio.ImageIO; 50 | import java.awt.image.BufferedImage; 51 | import java.io.File; 52 | import java.io.IOException; 53 | import java.net.URI; 54 | import java.net.URL; 55 | import java.net.URLConnection; 56 | import java.net.http.HttpClient; 57 | import java.net.http.HttpRequest; 58 | import java.net.http.HttpResponse; 59 | import java.nio.file.FileVisitResult; 60 | import java.nio.file.FileVisitor; 61 | import java.nio.file.Files; 62 | import java.nio.file.Path; 63 | import java.nio.file.attribute.BasicFileAttributes; 64 | import java.time.Duration; 65 | import java.time.temporal.TemporalUnit; 66 | import java.util.ArrayList; 67 | import java.util.List; 68 | import java.util.concurrent.CompletableFuture; 69 | import java.util.concurrent.TimeUnit; 70 | import java.util.concurrent.TimeoutException; 71 | 72 | import static net.minecraft.server.command.CommandManager.argument; 73 | import static net.minecraft.server.command.CommandManager.literal; 74 | 75 | 76 | public class Image2Map implements ModInitializer { 77 | public static final Logger LOGGER = LogUtils.getLogger(); 78 | 79 | public static Image2MapConfig CONFIG = Image2MapConfig.loadOrCreateConfig(); 80 | 81 | @Override 82 | public void onInitialize() { 83 | CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { 84 | dispatcher.register(literal("image2map") 85 | .requires(Permissions.require("image2map.use", CONFIG.minPermLevel)) 86 | .then(literal("create") 87 | .requires(Permissions.require("image2map.create", 0)) 88 | .then(argument("width", IntegerArgumentType.integer(1)) 89 | .then(argument("height", IntegerArgumentType.integer(1)) 90 | .then(argument("mode", StringArgumentType.word()).suggests(new DitherModeSuggestionProvider()) 91 | .then(argument("path", StringArgumentType.greedyString()) 92 | .executes(this::createMap)) 93 | ) 94 | ) 95 | ) 96 | .then(argument("mode", StringArgumentType.word()).suggests(new DitherModeSuggestionProvider()) 97 | .then(argument("path", StringArgumentType.greedyString()) 98 | .executes(this::createMap) 99 | ) 100 | ) 101 | ) 102 | .then(literal("create-folder") 103 | .requires(Permissions.require("image2map.createfolder", 3).and(x -> CONFIG.allowLocalFiles)) 104 | .then(argument("width", IntegerArgumentType.integer(1)) 105 | .then(argument("height", IntegerArgumentType.integer(1)) 106 | .then(argument("mode", StringArgumentType.word()).suggests(new DitherModeSuggestionProvider()) 107 | .then(argument("path", StringArgumentType.greedyString()) 108 | .executes(this::createMapFromFolder)) 109 | ) 110 | ) 111 | ) 112 | .then(argument("mode", StringArgumentType.word()).suggests(new DitherModeSuggestionProvider()) 113 | .then(argument("path", StringArgumentType.greedyString()) 114 | .executes(this::createMapFromFolder) 115 | ) 116 | ) 117 | ) 118 | .then(literal("preview") 119 | .requires(Permissions.require("image2map.preview", 0)) 120 | .then(argument("path", StringArgumentType.greedyString()) 121 | .executes(this::openPreview) 122 | ) 123 | ) 124 | ); 125 | }); 126 | 127 | ServerLifecycleEvents.SERVER_STARTED.register((s) -> CardboardWarning.checkAndAnnounce()); 128 | } 129 | 130 | private int openPreview(CommandContext context) throws CommandSyntaxException { 131 | ServerCommandSource source = context.getSource(); 132 | String input = StringArgumentType.getString(context, "path"); 133 | 134 | source.sendFeedback(() -> Text.literal("Getting image..."), false); 135 | 136 | getImage(input).orTimeout(20, TimeUnit.SECONDS).handleAsync((image, ex) -> { 137 | if (ex instanceof TimeoutException) { 138 | source.sendFeedback(() -> Text.literal("Downloading or reading of the image took too long!"), false); 139 | return null; 140 | } else if (ex != null) { 141 | if (ex instanceof RuntimeException ru && ru.getCause() != null) { 142 | ex = ru.getCause(); 143 | } 144 | 145 | Throwable finalEx = ex; 146 | source.sendFeedback(() -> Text.literal("The image isn't valid (hover for more info)!") 147 | .setStyle(Style.EMPTY.withColor(Formatting.RED).withHoverEvent(new HoverEvent.ShowText(Text.literal(finalEx.getMessage())))), false); 148 | return null; 149 | } 150 | 151 | if (image == null) { 152 | source.sendFeedback(() -> Text.literal("That doesn't seem to be a valid image (unknown reason)!"), false); 153 | return null; 154 | } 155 | 156 | if (GuiHelpers.getCurrentGui(source.getPlayer()) instanceof PreviewGui previewGui) { 157 | previewGui.close(); 158 | } 159 | new PreviewGui(context.getSource().getPlayer(), image, input, DitherMode.NONE, image.getWidth(), image.getHeight()); 160 | 161 | return null; 162 | }, source.getServer()); 163 | 164 | return 1; 165 | } 166 | 167 | class DitherModeSuggestionProvider implements SuggestionProvider { 168 | 169 | @Override 170 | public CompletableFuture getSuggestions(CommandContext context, 171 | SuggestionsBuilder builder) throws CommandSyntaxException { 172 | builder.suggest("none"); 173 | builder.suggest("dither"); 174 | return builder.buildFuture(); 175 | } 176 | 177 | } 178 | 179 | public enum DitherMode { 180 | NONE, 181 | FLOYD; 182 | 183 | public static DitherMode fromString(String string) { 184 | if (string.equalsIgnoreCase("NONE")) 185 | return DitherMode.NONE; 186 | else if (string.equalsIgnoreCase("DITHER") || string.equalsIgnoreCase("FLOYD")) 187 | return DitherMode.FLOYD; 188 | throw new IllegalArgumentException("invalid dither mode"); 189 | } 190 | } 191 | 192 | private CompletableFuture getImage(String input) { 193 | return CompletableFuture.supplyAsync(() -> { 194 | try { 195 | if (isValid(input)) { 196 | try(var client = HttpClient.newHttpClient()) { 197 | var req = HttpRequest.newBuilder().GET().uri(URI.create(input)).timeout(Duration.ofSeconds(30)) 198 | .setHeader("User-Agent", "Image2Map mod").build(); 199 | 200 | var stream = client.send(req, HttpResponse.BodyHandlers.ofInputStream()); 201 | return ImageIO.read(stream.body()); 202 | } 203 | } else if (CONFIG.allowLocalFiles) { 204 | var path = FabricLoader.getInstance().getGameDir().resolve(input); 205 | if (Files.exists(path)) { 206 | return ImageIO.read(Files.newInputStream(path)); 207 | } 208 | return null; 209 | } else { 210 | return null; 211 | } 212 | } catch (Throwable e) { 213 | throw new RuntimeException(e); 214 | } 215 | }); 216 | } 217 | 218 | private List getImageFromFolder(String input) { 219 | if (CONFIG.allowLocalFiles) { 220 | try { 221 | var arr = new ArrayList(); 222 | var path = FabricLoader.getInstance().getGameDir().resolve(input); 223 | if (Files.exists(path) && Files.isDirectory(path)) { 224 | Files.walkFileTree(path, new FileVisitor() { 225 | @Override 226 | public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { 227 | return FileVisitResult.CONTINUE; 228 | } 229 | 230 | @Override 231 | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { 232 | try { 233 | var x = ImageIO.read(Files.newInputStream(file)); 234 | if (x != null) { 235 | arr.add(x); 236 | } 237 | }catch (Throwable e) { 238 | e.printStackTrace(); 239 | } 240 | 241 | return FileVisitResult.CONTINUE; 242 | } 243 | 244 | @Override 245 | public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { 246 | return FileVisitResult.CONTINUE; 247 | } 248 | 249 | @Override 250 | public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { 251 | return FileVisitResult.CONTINUE; 252 | } 253 | }); 254 | } 255 | return arr; 256 | } catch (Throwable e) { 257 | throw new RuntimeException(e); 258 | } 259 | } 260 | 261 | return List.of(); 262 | } 263 | 264 | private int createMap(CommandContext context) throws CommandSyntaxException { 265 | ServerCommandSource source = context.getSource(); 266 | 267 | PlayerEntity player = source.getPlayer(); 268 | DitherMode mode; 269 | String modeStr = StringArgumentType.getString(context, "mode"); 270 | try { 271 | mode = DitherMode.fromString(modeStr); 272 | } catch (IllegalArgumentException e) { 273 | throw new SimpleCommandExceptionType(() -> "Invalid dither mode '" + modeStr + "'").create(); 274 | } 275 | 276 | String input = StringArgumentType.getString(context, "path"); 277 | 278 | source.sendFeedback(() -> Text.literal("Getting image..."), false); 279 | 280 | getImage(input).orTimeout(20, TimeUnit.SECONDS).handleAsync((image, ex) -> { 281 | if (ex instanceof TimeoutException) { 282 | source.sendFeedback(() -> Text.literal("Downloading or reading of the image took too long!"), false); 283 | return null; 284 | } else if (ex != null) { 285 | if (ex instanceof RuntimeException ru && ru.getCause() != null) { 286 | ex = ru.getCause(); 287 | } 288 | 289 | Throwable finalEx = ex; 290 | source.sendFeedback(() -> Text.literal("The image isn't valid (hover for more info)!") 291 | .setStyle(Style.EMPTY.withColor(Formatting.RED).withHoverEvent(new HoverEvent.ShowText(Text.literal(finalEx.getMessage())))), false); 292 | return null; 293 | } 294 | 295 | if (image == null) { 296 | source.sendFeedback(() -> Text.literal("That doesn't seem to be a valid image (unknown reason)!"), false); 297 | return null; 298 | } 299 | 300 | int width; 301 | int height; 302 | 303 | try { 304 | width = IntegerArgumentType.getInteger(context, "width"); 305 | height = IntegerArgumentType.getInteger(context, "height"); 306 | } catch (Throwable e) { 307 | width = image.getWidth(); 308 | height = image.getHeight(); 309 | } 310 | 311 | int finalHeight = height; 312 | int finalWidth = width; 313 | source.sendFeedback(() -> Text.literal("Converting into maps..."), false); 314 | 315 | CompletableFuture.supplyAsync(() -> MapRenderer.render(image, mode, finalWidth, finalHeight)).thenAcceptAsync(mapImage -> { 316 | var items = MapRenderer.toVanillaItems(mapImage, source.getWorld(), input); 317 | giveToPlayer(player, items, input, finalWidth, finalHeight); 318 | source.sendFeedback(() -> Text.literal("Done!"), false); 319 | }, source.getServer()); 320 | return null; 321 | }, source.getServer()); 322 | 323 | return 1; 324 | } 325 | 326 | private int createMapFromFolder(CommandContext context) throws CommandSyntaxException { 327 | ServerCommandSource source = context.getSource(); 328 | 329 | PlayerEntity player = source.getPlayer(); 330 | DitherMode mode; 331 | String modeStr = StringArgumentType.getString(context, "mode"); 332 | try { 333 | mode = DitherMode.fromString(modeStr); 334 | } catch (IllegalArgumentException e) { 335 | throw new SimpleCommandExceptionType(() -> "Invalid dither mode '" + modeStr + "'").create(); 336 | } 337 | 338 | String input = StringArgumentType.getString(context, "path"); 339 | 340 | source.sendFeedback(() -> Text.literal("Getting image..."), false); 341 | 342 | var list = new ArrayList(); 343 | 344 | for (var image : getImageFromFolder(input)) { 345 | int width; 346 | int height; 347 | 348 | try { 349 | width = IntegerArgumentType.getInteger(context, "width"); 350 | height = IntegerArgumentType.getInteger(context, "height"); 351 | } catch (Throwable e) { 352 | width = image.getWidth(); 353 | height = image.getHeight(); 354 | } 355 | 356 | int finalHeight = height; 357 | int finalWidth = width; 358 | source.sendFeedback(() -> Text.literal("Converting into maps..."), false); 359 | 360 | var mapImage = MapRenderer.render(image, mode, finalWidth, finalHeight); 361 | var items = MapRenderer.toVanillaItems(mapImage, source.getWorld(), input); 362 | list.add(toSingleStack(items, input, width, height)); 363 | } 364 | var bundle = new ItemStack(Items.BUNDLE); 365 | bundle.set(DataComponentTypes.BUNDLE_CONTENTS, new BundleContentsComponent(list)); 366 | player.giveItemStack(bundle); 367 | 368 | return 1; 369 | } 370 | 371 | public static void giveToPlayer(PlayerEntity player, List items, String input, int width, int height) { 372 | player.giveItemStack(toSingleStack(items, input, width, height)); 373 | } 374 | 375 | public static ItemStack toSingleStack(List items, String input, int width, int height) { 376 | if (items.size() == 1) { 377 | return items.get(0); 378 | } else { 379 | var bundle = new ItemStack(Items.BUNDLE); 380 | bundle.set(DataComponentTypes.BUNDLE_CONTENTS, new BundleContentsComponent(items)); 381 | bundle.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT.with(NbtOps.INSTANCE, ImageData.CODEC, 382 | ImageData.ofBundle(MathHelper.ceil(width / 128d), MathHelper.ceil(height / 128d))).getOrThrow()); 383 | 384 | bundle.set(DataComponentTypes.LORE, new LoreComponent(List.of(Text.literal(input)))); 385 | bundle.set(DataComponentTypes.ITEM_NAME, Text.literal("Maps").formatted(Formatting.GOLD)); 386 | 387 | return bundle; 388 | } 389 | } 390 | 391 | public static boolean clickItemFrame(PlayerEntity player, Hand hand, ItemFrameEntity itemFrameEntity) { 392 | var stack = player.getStackInHand(hand); 393 | var bundleData = stack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).get(ImageData.CODEC); 394 | 395 | if (stack.isOf(Items.BUNDLE) && bundleData.isSuccess() && bundleData.getOrThrow().quickPlace()) { 396 | var world = itemFrameEntity.getWorld(); 397 | var start = itemFrameEntity.getBlockPos(); 398 | var width = bundleData.getOrThrow().width(); 399 | var height = bundleData.getOrThrow().height(); 400 | 401 | var frames = new ItemFrameEntity[width * height]; 402 | 403 | var facing = itemFrameEntity.getHorizontalFacing(); 404 | Direction right; 405 | Direction down; 406 | 407 | int rot; 408 | 409 | if (facing.getAxis() != Direction.Axis.Y) { 410 | right = facing.rotateYCounterclockwise(); 411 | down = Direction.DOWN; 412 | rot = 0; 413 | } else { 414 | right = player.getHorizontalFacing().rotateYClockwise(); 415 | if (facing.getDirection() == Direction.AxisDirection.POSITIVE) { 416 | down = right.rotateYClockwise(); 417 | rot = player.getHorizontalFacing().getOpposite().getHorizontalQuarterTurns(); 418 | } else { 419 | down = right.rotateYCounterclockwise(); 420 | rot = (right.getAxis() == Direction.Axis.Z ? player.getHorizontalFacing() : player.getHorizontalFacing().getOpposite()).getHorizontalQuarterTurns(); 421 | } 422 | } 423 | 424 | var mut = start.mutableCopy(); 425 | 426 | for (var x = 0; x < width; x++) { 427 | for (var y = 0; y < height; y++) { 428 | mut.set(start); 429 | mut.move(right, x); 430 | mut.move(down, y); 431 | var entities = world.getEntitiesByClass(ItemFrameEntity.class, Box.from(Vec3d.of(mut)), (entity1) -> entity1.getHorizontalFacing() == facing && entity1.getBlockPos().equals(mut)); 432 | if (!entities.isEmpty()) { 433 | frames[x + y * width] = entities.get(0); 434 | } 435 | } 436 | } 437 | 438 | for (var map : stack.getOrDefault(DataComponentTypes.BUNDLE_CONTENTS, BundleContentsComponent.DEFAULT).iterate()) { 439 | var mapData = map.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).get(ImageData.CODEC); 440 | 441 | if (mapData.isSuccess() && mapData.getOrThrow().isReal()) { 442 | map = map.copy(); 443 | var newData = mapData.getOrThrow().withDirection(right, down, facing); 444 | map.apply(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT, x -> x.with(NbtOps.INSTANCE, ImageData.CODEC, newData).getOrThrow()); 445 | 446 | var frame = frames[mapData.getOrThrow().x() + mapData.getOrThrow().y() * width]; 447 | 448 | if (frame != null && frame.getHeldItemStack().isEmpty()) { 449 | frame.setHeldItemStack(map); 450 | frame.setRotation(rot); 451 | frame.setInvisible(true); 452 | } 453 | } 454 | } 455 | 456 | stack.decrement(1); 457 | 458 | return true; 459 | } 460 | 461 | return false; 462 | } 463 | 464 | public static boolean destroyItemFrame(Entity player, ItemFrameEntity itemFrameEntity) { 465 | var stack = itemFrameEntity.getHeldItemStack(); 466 | var tag = stack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).get(ImageData.CODEC); 467 | 468 | 469 | if (stack.getItem() == Items.FILLED_MAP && tag.isSuccess() && tag.getOrThrow().right().isPresent() 470 | && tag.getOrThrow().down().isPresent() && tag.getOrThrow().facing().isPresent()) { 471 | var xo = tag.getOrThrow().x(); 472 | var yo = tag.getOrThrow().y(); 473 | var width = tag.getOrThrow().width(); 474 | var height = tag.getOrThrow().height(); 475 | 476 | Direction right = tag.getOrThrow().right().get(); 477 | Direction down = tag.getOrThrow().down().get(); 478 | Direction facing = tag.getOrThrow().facing().get(); 479 | 480 | var world = itemFrameEntity.getWorld(); 481 | var start = itemFrameEntity.getBlockPos(); 482 | 483 | var mut = start.mutableCopy(); 484 | 485 | mut.move(right, -xo); 486 | mut.move(down, -yo); 487 | 488 | start = mut.toImmutable(); 489 | 490 | for (var x = 0; x < width; x++) { 491 | for (var y = 0; y < height; y++) { 492 | mut.set(start); 493 | mut.move(right, x); 494 | mut.move(down, y); 495 | var entities = world.getEntitiesByClass(ItemFrameEntity.class, Box.from(Vec3d.of(mut)), 496 | (entity1) -> entity1.getHorizontalFacing() == facing && entity1.getBlockPos().equals(mut)); 497 | if (!entities.isEmpty()) { 498 | var frame = entities.get(0); 499 | 500 | // Only apply to frames that contain an image2map map 501 | var frameStack = frame.getHeldItemStack(); 502 | tag = frameStack.getOrDefault(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT).get(ImageData.CODEC); 503 | 504 | if (frameStack.getItem() == Items.FILLED_MAP && tag.isSuccess() && tag.getOrThrow().right().isPresent() 505 | && tag.getOrThrow().down().isPresent() && tag.getOrThrow().facing().isPresent()) { 506 | frame.setHeldItemStack(ItemStack.EMPTY, true); 507 | frame.setInvisible(false); 508 | } 509 | } 510 | } 511 | } 512 | 513 | return true; 514 | } 515 | 516 | return false; 517 | } 518 | 519 | private static boolean isValid(String url) { 520 | try { 521 | new URL(url).toURI(); 522 | return true; 523 | } catch (Exception e) { 524 | return false; 525 | } 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/ImageData.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map; 2 | 3 | import com.mojang.serialization.Codec; 4 | import com.mojang.serialization.MapCodec; 5 | import com.mojang.serialization.codecs.RecordCodecBuilder; 6 | import net.minecraft.util.math.Direction; 7 | 8 | import java.util.Optional; 9 | 10 | public record ImageData(int x, int y, int width, int height, boolean quickPlace, Optional right, Optional down, Optional facing) { 11 | public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( 12 | Codec.INT.optionalFieldOf("image2map:x", 0).forGetter(ImageData::x), 13 | Codec.INT.optionalFieldOf("image2map:y", 0).forGetter(ImageData::y), 14 | Codec.INT.optionalFieldOf("image2map:width", 0).forGetter(ImageData::width), 15 | Codec.INT.optionalFieldOf("image2map:height", 0).forGetter(ImageData::height), 16 | Codec.BOOL.optionalFieldOf("image2map:quick_place", false).forGetter(ImageData::quickPlace), 17 | Direction.CODEC.optionalFieldOf("image2map:right").forGetter(ImageData::right), 18 | Direction.CODEC.optionalFieldOf("image2map:down").forGetter(ImageData::down), 19 | Direction.CODEC.optionalFieldOf("image2map:facing").forGetter(ImageData::facing) 20 | ).apply(instance, ImageData::new)); 21 | 22 | public static ImageData ofSimple(int x, int y, int width, int height) { 23 | return new ImageData(x, y ,width, height, false, Optional.empty(), Optional.empty(), Optional.empty()); 24 | } 25 | 26 | public ImageData withDirection(Direction right, Direction down, Direction facing) { 27 | return new ImageData(x, y, width, height, quickPlace, Optional.of(right), Optional.of(down), Optional.of(facing)); 28 | } 29 | 30 | public static ImageData ofBundle(int width, int height) { 31 | return new ImageData(0, 0, width, height, true, Optional.empty(), Optional.empty(), Optional.empty()); 32 | } 33 | 34 | public boolean isReal() { 35 | return this.width != 0 && this.height != 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/config/Image2MapConfig.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map.config; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | import net.fabricmc.loader.api.FabricLoader; 6 | import org.apache.commons.io.IOUtils; 7 | import space.essem.image2map.Image2Map; 8 | 9 | import java.io.*; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | public class Image2MapConfig { 13 | private static final Gson GSON = new GsonBuilder() 14 | .disableHtmlEscaping().setLenient().setPrettyPrinting() 15 | .create(); 16 | 17 | public boolean allowLocalFiles = false; 18 | 19 | public int minPermLevel = 2; 20 | 21 | 22 | public static Image2MapConfig loadOrCreateConfig() { 23 | try { 24 | Image2MapConfig config; 25 | File configFile = new File(FabricLoader.getInstance().getConfigDir().toFile(), "image2map.json"); 26 | 27 | if (configFile.exists()) { 28 | String json = IOUtils.toString(new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8)); 29 | 30 | config = GSON.fromJson(json, Image2MapConfig.class); 31 | } else { 32 | config = new Image2MapConfig(); 33 | } 34 | 35 | 36 | saveConfig(config); 37 | return config; 38 | } 39 | catch(IOException exception) { 40 | Image2Map.LOGGER.error("Something went wrong while reading config!"); 41 | exception.printStackTrace(); 42 | return new Image2MapConfig(); 43 | } 44 | } 45 | 46 | public static void saveConfig(Image2MapConfig config) { 47 | File configFile = new File(FabricLoader.getInstance().getConfigDir().toFile(), "image2map.json"); 48 | try { 49 | BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(configFile), StandardCharsets.UTF_8)); 50 | writer.write(GSON.toJson(config)); 51 | writer.close(); 52 | } catch (Exception e) { 53 | Image2Map.LOGGER.error("Something went wrong while saving config!"); 54 | e.printStackTrace(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/gui/MapGui.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map.gui; 2 | 3 | import com.google.common.base.Predicates; 4 | import com.mojang.brigadier.arguments.StringArgumentType; 5 | import com.mojang.brigadier.tree.ArgumentCommandNode; 6 | import com.mojang.brigadier.tree.RootCommandNode; 7 | import eu.pb4.mapcanvas.api.core.*; 8 | import eu.pb4.mapcanvas.api.utils.VirtualDisplay; 9 | import eu.pb4.sgui.api.gui.HotbarGui; 10 | import io.netty.buffer.Unpooled; 11 | import it.unimi.dsi.fastutil.ints.IntArrayList; 12 | import it.unimi.dsi.fastutil.ints.IntList; 13 | 14 | import net.minecraft.command.CommandSource; 15 | import net.minecraft.entity.Entity; 16 | import net.minecraft.entity.EntityType; 17 | import net.minecraft.entity.MovementType; 18 | import net.minecraft.entity.passive.HorseEntity; 19 | import net.minecraft.entity.player.PlayerPosition; 20 | import net.minecraft.item.ItemStack; 21 | import net.minecraft.item.Items; 22 | import net.minecraft.network.PacketByteBuf; 23 | import net.minecraft.network.packet.Packet; 24 | import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket; 25 | import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket; 26 | import net.minecraft.network.packet.s2c.play.*; 27 | import net.minecraft.server.network.ServerPlayerEntity; 28 | import net.minecraft.util.PlayerInput; 29 | import net.minecraft.util.math.BlockPos; 30 | import net.minecraft.util.math.Direction; 31 | import net.minecraft.util.math.Vec3d; 32 | import net.minecraft.world.GameMode; 33 | import java.util.Collections; 34 | import java.util.EnumSet; 35 | import java.util.Objects; 36 | import java.util.Set; 37 | 38 | import org.jetbrains.annotations.Nullable; 39 | import space.essem.image2map.mixin.EntityPassengersSetS2CPacketAccessor; 40 | 41 | 42 | public class MapGui extends HotbarGui { 43 | private static final Packet COMMAND_PACKET; 44 | 45 | public final Entity entity; 46 | public CombinedPlayerCanvas canvas; 47 | public VirtualDisplay virtualDisplay; 48 | //public CanvasRenderer renderer; 49 | public final BlockPos pos; 50 | //public final CanvasIcon cursor; 51 | 52 | public final IntList additionalEntities = new IntArrayList(); 53 | 54 | //public float xRot; 55 | //public float yRot; 56 | //public int cursorX; 57 | //public int cursorY; 58 | //public int mouseMoves; 59 | 60 | public MapGui(ServerPlayerEntity player, int width, int height) { 61 | super(player); 62 | var pos = player.getBlockPos().withY(2048); 63 | this.pos = pos; 64 | 65 | this.entity = new HorseEntity(EntityType.HORSE, player.getServerWorld()); 66 | this.entity.setYaw(0); 67 | this.entity.setHeadYaw(0); 68 | this.entity.setNoGravity(true); 69 | this.entity.setPitch(0); 70 | this.entity.setInvisible(true); 71 | this.initialize(width, height); 72 | 73 | //this.cursorX = this.canvas.getWidth(); 74 | //this.cursorY = this.canvas.getHeight(); // MapDecoration.Type.TARGET_POINT 75 | //this.cursor = null;//this.canvas.createIcon(MapIcon.Type.TARGET_POINT, true, this.cursorX, this.cursorY, (byte) 14, null); 76 | player.networkHandler.sendPacket(new EntitySpawnS2CPacket(this.entity.getId(), this.entity.getUuid(), 77 | this.entity.getX(), this.entity.getY(), this.entity.getZ(), this.entity.getPitch(), entity.getYaw(), entity.getType(), 0, Vec3d.ZERO, entity.getHeadYaw())); 78 | 79 | player.networkHandler.sendPacket(new EntityTrackerUpdateS2CPacket(this.entity.getId(), this.entity.getDataTracker().getChangedEntries())); 80 | player.networkHandler.sendPacket(new SetCameraEntityS2CPacket(this.entity)); 81 | //this.xRot = player.getYaw(); 82 | //this.yRot = player.getPitch(); 83 | var buf = new PacketByteBuf(Unpooled.buffer()); 84 | buf.writeVarInt(this.entity.getId()); 85 | buf.writeIntArray(new int[]{player.getId()}); 86 | player.networkHandler.sendPacket(EntityPassengersSetS2CPacketAccessor.createEntityPassengersSetS2CPacket(buf)); 87 | player.networkHandler.sendPacket(new GameStateChangeS2CPacket(GameStateChangeS2CPacket.GAME_MODE_CHANGED, GameMode.SPECTATOR.getIndex())); 88 | player.networkHandler.sendPacket(new EntityS2CPacket.Rotate(player.getId(), (byte) 0, (byte) 0, player.isOnGround())); 89 | 90 | //player.networkHandler.sendPacket(COMMAND_PACKET); 91 | 92 | for (int i = 0; i < 9; i++) { 93 | this.setSlot(i, new ItemStack(Items.STICK)); 94 | } 95 | 96 | //player.networkHandler.sendPacket(new GameMessageS2CPacket(Text.translatable("polyport.cc.press_to_close", "Ctrl", "Q (Drop)"/*new KeybindComponent("key.drop")*/).formatted(Formatting.DARK_RED), true)); 97 | this.open(); 98 | } 99 | 100 | protected void resizeCanvas(int width, int height) { 101 | this.destroy(); 102 | this.initialize(width, height); 103 | this.player.networkHandler.sendPacket(new EntityPositionS2CPacket(this.entity.getId(), new PlayerPosition(this.entity.getPos(), Vec3d.ZERO, this.entity.getYaw(), this.entity.getPitch()), Set.of(), false)); 104 | } 105 | 106 | protected void initialize(int width, int height) { 107 | this.canvas = DrawableCanvas.create(width, height); 108 | this.virtualDisplay = VirtualDisplay.of(this.canvas, pos, Direction.NORTH, 0, true); 109 | //this.renderer = CanvasRenderer.of(new CanvasImage(this.canvas.getWidth(), this.canvas.getHeight())); 110 | 111 | this.canvas.addPlayer(player); 112 | this.virtualDisplay.addPlayer(player); 113 | 114 | this.entity.setPos(pos.getX() - width / 2d + 1, pos.getY() - height / 2d - 0.5, pos.getZ()); 115 | } 116 | 117 | protected void destroy() { 118 | this.virtualDisplay.removePlayer(this.player); 119 | this.virtualDisplay.destroy(); 120 | //this.virtualDisplay2.destroy(); 121 | this.canvas.removePlayer(this.player); 122 | this.canvas.destroy(); 123 | } 124 | 125 | /*public void render() { 126 | this.renderer.render(this.player.world.getTime(), 0, 0/*this.cursorX / 2, this.cursorY / 2); 127 | // Debug maps 128 | if (false && FabricLoader.getInstance().isDevelopmentEnvironment()) { 129 | for (int x = 0; x < this.canvas.getSectionsWidth(); x++) { 130 | CanvasUtils.fill(this.renderer.canvas(), x * 128, 0, x * 128 + 1, this.canvas.getHeight(), CanvasColor.RED_HIGH); 131 | } 132 | for (int x = 0; x < this.canvas.getSectionsHeight(); x++) { 133 | CanvasUtils.fill(this.renderer.canvas(), 0,x * 128, this.canvas.getWidth(), x * 128 + 1, CanvasColor.BLUE_HIGH); 134 | } 135 | } 136 | 137 | CanvasUtils.draw(this.canvas, 0, 0, this.renderer.canvas()); 138 | this.canvas.sendUpdates(); 139 | }*/ 140 | 141 | /*@Override 142 | public void onTick() { 143 | this.render(); 144 | }*/ 145 | 146 | @Override 147 | public void onClose() { 148 | //this.cursor.remove(); 149 | this.destroy(); 150 | this.player.server.getCommandManager().sendCommandTree(this.player); 151 | this.player.networkHandler.sendPacket(new SetCameraEntityS2CPacket(this.player)); 152 | this.player.networkHandler.sendPacket(new EntitiesDestroyS2CPacket(this.entity.getId())); 153 | if (!this.additionalEntities.isEmpty()) { 154 | this.player.networkHandler.sendPacket(new EntitiesDestroyS2CPacket(this.additionalEntities)); 155 | } 156 | this.player.networkHandler.sendPacket(new GameStateChangeS2CPacket(GameStateChangeS2CPacket.GAME_MODE_CHANGED, this.player.interactionManager.getGameMode().getIndex())); 157 | this.player.networkHandler.sendPacket(new PlayerPositionLookS2CPacket(this.player.getId(), new PlayerPosition(this.player.getPos(), Vec3d.ZERO, this.player.getYaw(), this.player.getPitch()), Set.of())); 158 | if (this.player.hasVehicle()) { 159 | this.player.networkHandler.sendPacket(new EntityPassengersSetS2CPacket(Objects.requireNonNull(this.player.getVehicle()))); 160 | } 161 | super.onClose(); 162 | } 163 | 164 | public void onCommandSuggestion(int id, String fullCommand) { 165 | 166 | } 167 | 168 | public void onCameraMove(float xRot, float yRot) { 169 | /*this.mouseMoves++; 170 | 171 | if (this.mouseMoves < 16) { 172 | return; 173 | } 174 | 175 | this.xRot = xRot; 176 | this.yRot = yRot; 177 | 178 | this.cursorX = this.cursorX + (int) ((xRot > 0.3 ? 3: xRot < -0.3 ? -3 : 0) * (Math.abs(xRot) - 0.3)); 179 | this.cursorY = this.cursorY + (int) ((yRot > 0.3 ? 3 : yRot < -0.3 ? -3 : 0) * (Math.abs(yRot) - 0.3)); 180 | 181 | this.cursorX = MathHelper.clamp(this.cursorX, 5, this.canvas.getWidth() * 2 - 5); 182 | this.cursorY = MathHelper.clamp(this.cursorY, 5, this.canvas.getHeight() * 2 - 5); 183 | 184 | //this.cursor.move(this.cursorX + 4, this.cursorY + 4, this.cursor.getRotation());*/ 185 | } 186 | 187 | @Override 188 | public boolean canPlayerClose() { 189 | return false; 190 | } 191 | 192 | @Override 193 | public boolean onClickEntity(int entityId, EntityInteraction type, boolean isSneaking, @Nullable Vec3d interactionPos) { 194 | /*if (type == EntityInteraction.ATTACK) { 195 | this.renderer.click(this.cursorX / 2, this.cursorY / 2, ScreenElement.ClickType.LEFT_DOWN); 196 | } else { 197 | this.renderer.click(this.cursorX / 2, this.cursorY / 2, ScreenElement.ClickType.RIGHT_DOWN); 198 | }*/ 199 | 200 | return super.onClickEntity(entityId, type, isSneaking, interactionPos); 201 | } 202 | 203 | public void setDistance(double i) { 204 | this.entity.setPos(this.entity.getX(), this.entity.getY(), this.pos.getZ() - i); 205 | this.player.networkHandler.sendPacket(new EntityPositionS2CPacket(this.entity.getId(), new PlayerPosition(this.entity.getPos(), Vec3d.ZERO, this.entity.getYaw(), this.entity.getPitch()), Set.of(), false)); 206 | } 207 | 208 | @Override 209 | public boolean onPlayerAction(PlayerActionC2SPacket.Action action, Direction direction) { 210 | if (action == PlayerActionC2SPacket.Action.DROP_ALL_ITEMS) { 211 | this.close(); 212 | } 213 | return false; 214 | } 215 | 216 | public void onPlayerInput(PlayerInput input) { 217 | 218 | } 219 | 220 | public void onPlayerCommand(int id, ClientCommandC2SPacket.Mode command, int data) { 221 | } 222 | 223 | static { 224 | var commandNode = new RootCommandNode(); 225 | 226 | commandNode.addChild( 227 | new ArgumentCommandNode<>( 228 | "command", 229 | StringArgumentType.greedyString(), 230 | null, 231 | Predicates.alwaysTrue(), 232 | null, 233 | null, 234 | true, 235 | (ctx, builder) -> null 236 | ) 237 | ); 238 | 239 | COMMAND_PACKET = new CommandTreeS2CPacket(commandNode); 240 | } 241 | 242 | 243 | public void executeCommand(String command) { 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/gui/PreviewGui.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map.gui; 2 | 3 | import com.mojang.brigadier.CommandDispatcher; 4 | import com.mojang.brigadier.arguments.ArgumentType; 5 | import com.mojang.brigadier.arguments.BoolArgumentType; 6 | import com.mojang.brigadier.arguments.IntegerArgumentType; 7 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 8 | import com.mojang.brigadier.builder.RequiredArgumentBuilder; 9 | import com.mojang.brigadier.tree.RootCommandNode; 10 | import eu.pb4.mapcanvas.api.core.CanvasColor; 11 | import eu.pb4.mapcanvas.api.core.CanvasImage; 12 | import eu.pb4.mapcanvas.api.font.DefaultFonts; 13 | import eu.pb4.mapcanvas.api.utils.CanvasUtils; 14 | import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket; 15 | import net.minecraft.server.network.ServerPlayerEntity; 16 | import net.minecraft.text.Text; 17 | import net.minecraft.util.math.MathHelper; 18 | import space.essem.image2map.Image2Map; 19 | import space.essem.image2map.renderer.MapRenderer; 20 | 21 | import java.awt.image.BufferedImage; 22 | import java.util.concurrent.CompletableFuture; 23 | 24 | public class PreviewGui extends MapGui { 25 | private static final CommandDispatcher COMMANDS = new CommandDispatcher<>(); 26 | 27 | private final BufferedImage sourceImage; 28 | private final String source; 29 | private boolean dirty; 30 | private int xPos; 31 | private CanvasImage image; 32 | private int yPos; 33 | private Image2Map.DitherMode ditherMode; 34 | private int width; 35 | private int height; 36 | private boolean grid = true; 37 | private CompletableFuture imageProcessing; 38 | 39 | public PreviewGui(ServerPlayerEntity player, BufferedImage image, String source, Image2Map.DitherMode ditherMode, int width, int height) { 40 | super(player, MathHelper.ceil(width / 128d) + 2, MathHelper.ceil(height / 128d) + 2); 41 | this.width = width; 42 | this.height = height; 43 | this.ditherMode = ditherMode; 44 | this.source = source; 45 | this.sourceImage = image; 46 | 47 | player.networkHandler.sendPacket(new CommandTreeS2CPacket((RootCommandNode) COMMANDS.getRoot())); 48 | 49 | this.updateImage(); 50 | 51 | } 52 | 53 | protected void updateImage() { 54 | this.setDistance(Math.max(this.height / 128d * 0.8, this.width / 128d * 0.5)); 55 | this.dirty = true; 56 | this.drawLoading(); 57 | } 58 | 59 | @Override 60 | public void onTick() { 61 | if (this.dirty) { 62 | if (this.imageProcessing != null) { 63 | this.imageProcessing.cancel(true); 64 | } 65 | 66 | this.imageProcessing = CompletableFuture.supplyAsync(() -> MapRenderer.render(this.sourceImage, this.ditherMode, this.width, this.height)); 67 | this.dirty = false; 68 | } 69 | 70 | if (this.imageProcessing != null) { 71 | if (this.imageProcessing.isDone()) { 72 | if (this.imageProcessing.isCompletedExceptionally()) { 73 | this.imageProcessing = null; 74 | } else { 75 | try { 76 | this.image = this.imageProcessing.get(); 77 | this.imageProcessing = null; 78 | 79 | this.xPos = (this.canvas.getWidth() - this.image.getWidth()) / 2; 80 | this.yPos = (this.canvas.getHeight() - this.image.getHeight()) / 2; 81 | 82 | this.draw(); 83 | } catch (Throwable e) { 84 | e.printStackTrace(); 85 | this.close(); 86 | } 87 | } 88 | } 89 | } 90 | } 91 | 92 | @Override 93 | public void onClose() { 94 | if (this.imageProcessing != null) { 95 | this.imageProcessing.cancel(true); 96 | } 97 | super.onClose(); 98 | } 99 | 100 | private void drawLoading() { 101 | var text = "Loading..."; 102 | var size = (int) Math.min(this.height / 128d, this.width / 128d) * 16; 103 | var width = DefaultFonts.VANILLA.getTextWidth(text, size); 104 | 105 | CanvasUtils.fill(this.canvas, 106 | (this.width - width) / 2 - size + 128, 107 | (this.height - size) / 2 - size + 128, 108 | (this.width - width) / 2 + size + width + 128, 109 | (this.height - size) / 2 + size * 2 + 128, CanvasColor.BLACK_LOW); 110 | 111 | DefaultFonts.VANILLA.drawText(this.canvas, text, (this.width - width) / 2 + 128, (this.height - size) / 2 + 128, size, CanvasColor.WHITE_HIGH); 112 | 113 | this.canvas.sendUpdates(); 114 | } 115 | 116 | private void draw() { 117 | var image = new CanvasImage(this.canvas.getWidth(), this.canvas.getHeight()); 118 | 119 | if (this.grid) { 120 | for (int x = 0; x < this.canvas.getSectionsWidth(); x++) { 121 | for (int y = 0; y < this.canvas.getSectionsHeight(); y++) { 122 | CanvasUtils.fill(image, x * 128, y * 128, (x + 1) * 128, (y + 1) * 128, 123 | (x + (y % 2)) % 2 == 0 ? CanvasColor.BLACK_LOW : CanvasColor.GRAY_HIGH); 124 | } 125 | } 126 | 127 | CanvasUtils.fill(image, this.xPos - 2, this.yPos - 2, this.xPos + 2 + this.image.getWidth(), this.yPos + 2 + this.image.getHeight(), 128 | CanvasColor.WHITE_HIGH); 129 | } else { 130 | CanvasUtils.clear(image, CanvasColor.CLEAR_FORCE); 131 | } 132 | if (this.image != null) { 133 | CanvasUtils.draw(image, this.xPos, this.yPos, this.image); 134 | } 135 | CanvasUtils.draw(this.canvas, 0, 0, image); 136 | this.canvas.sendUpdates(); 137 | } 138 | 139 | public void setSize(int width, int height) { 140 | if ( 141 | this.canvas.getWidth() < width + 256 || this.canvas.getHeight() < height + 256 142 | || this.canvas.getWidth() > width * 2 || this.canvas.getHeight() > height * 2 143 | ) { 144 | this.resizeCanvas(MathHelper.ceil(width / 128d) + 2, MathHelper.ceil(height / 128d) + 2); 145 | } 146 | 147 | this.width = width; 148 | this.height = height; 149 | this.updateImage(); 150 | } 151 | 152 | public void setDitherMode(Image2Map.DitherMode ditherMode) { 153 | this.ditherMode = ditherMode; 154 | this.updateImage(); 155 | } 156 | 157 | public void setDrawGrid(boolean grid) { 158 | this.grid = grid; 159 | this.draw(); 160 | } 161 | 162 | @Override 163 | public void executeCommand(String command) { 164 | try { 165 | COMMANDS.execute(command, this); 166 | } catch (Throwable e) { 167 | e.printStackTrace(); 168 | } 169 | } 170 | 171 | private static LiteralArgumentBuilder literal(String name) { 172 | return LiteralArgumentBuilder.literal(name); 173 | } 174 | 175 | private static RequiredArgumentBuilder argument(String name, ArgumentType argumentType) { 176 | return RequiredArgumentBuilder.argument(name, argumentType); 177 | } 178 | 179 | static { 180 | COMMANDS.register(literal("exit").executes(x -> { 181 | x.getSource().close(); 182 | return 0; 183 | })); 184 | 185 | COMMANDS.register(literal("save").executes(x -> { 186 | if (x.getSource().imageProcessing == null) { 187 | x.getSource().drawLoading(); 188 | Image2Map.giveToPlayer(x.getSource().player, 189 | MapRenderer.toVanillaItems(x.getSource().image, x.getSource().player.getServerWorld(), x.getSource().source), 190 | x.getSource().source, x.getSource().width, x.getSource().height); 191 | 192 | x.getSource().close(); 193 | } else { 194 | x.getSource().player.sendMessage(Text.literal("Image is still processed!")); 195 | } 196 | return 0; 197 | })); 198 | 199 | COMMANDS.register(literal("size") 200 | .then(argument("width", IntegerArgumentType.integer(1)) 201 | .then(argument("height", IntegerArgumentType.integer(1)).executes(x -> { 202 | x.getSource().setSize(IntegerArgumentType.getInteger(x, "width"), IntegerArgumentType.getInteger(x, "height")); 203 | return 0; 204 | }))) 205 | .executes(x -> { 206 | x.getSource().player.sendMessage(Text.literal("Source: " + x.getSource().sourceImage.getWidth() + " x " + x.getSource().sourceImage.getHeight())); 207 | x.getSource().player.sendMessage(Text.literal("MapImage: " + x.getSource().width + " x " + x.getSource().height)); 208 | return 0; 209 | }) 210 | ); 211 | 212 | COMMANDS.register(literal("dither") 213 | .then(literal("none").executes(x -> { 214 | x.getSource().setDitherMode(Image2Map.DitherMode.NONE); 215 | return 0; 216 | })) 217 | .then(literal("floyd").executes(x -> { 218 | x.getSource().setDitherMode(Image2Map.DitherMode.FLOYD); 219 | return 0; 220 | })) 221 | ); 222 | 223 | COMMANDS.register(literal("grid") 224 | .then(argument("value", BoolArgumentType.bool()).executes(x -> { 225 | x.getSource().setDrawGrid(BoolArgumentType.getBool(x, "value")); 226 | return 0; 227 | })) 228 | .executes(x -> { 229 | x.getSource().setDrawGrid(!x.getSource().grid); 230 | return 0; 231 | }) 232 | ); 233 | } 234 | 235 | } 236 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/mixin/BundleItemMixin.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map.mixin; 2 | 3 | import net.minecraft.component.DataComponentTypes; 4 | import net.minecraft.util.ActionResult; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 9 | 10 | import net.minecraft.entity.player.PlayerEntity; 11 | import net.minecraft.inventory.StackReference; 12 | import net.minecraft.item.BundleItem; 13 | import net.minecraft.item.ItemStack; 14 | import net.minecraft.screen.slot.Slot; 15 | import net.minecraft.util.ClickType; 16 | import net.minecraft.util.Hand; 17 | import net.minecraft.world.World; 18 | 19 | @Mixin(BundleItem.class) 20 | public class BundleItemMixin { 21 | 22 | @Inject(method = "use", at = @At("HEAD"), cancellable = true) 23 | private void image2map$useBundle(World world, PlayerEntity user, Hand hand, 24 | CallbackInfoReturnable cir) { 25 | ItemStack itemStack = user.getStackInHand(hand); 26 | var tag = itemStack.get(DataComponentTypes.CUSTOM_DATA); 27 | 28 | if (tag != null && tag.contains("image2map:quick_place") && !user.isCreative()) { 29 | cir.setReturnValue(ActionResult.FAIL); 30 | cir.cancel(); 31 | } 32 | } 33 | 34 | @Inject(method = "onStackClicked", at = @At("HEAD"), cancellable = true) 35 | private void image2map$addBundleItems(ItemStack bundle, Slot slot, ClickType clickType, PlayerEntity player, 36 | CallbackInfoReturnable cir) { 37 | var tag = bundle.get(DataComponentTypes.CUSTOM_DATA); 38 | 39 | if (tag != null && tag.contains("image2map:quick_place") && !player.isCreative()) { 40 | cir.setReturnValue(false); 41 | cir.cancel(); 42 | } 43 | } 44 | 45 | @Inject(method = "onClicked", at = @At("HEAD"), cancellable = true) 46 | private void image2map$removeBundleItems(ItemStack bundle, ItemStack otherStack, Slot slot, ClickType clickType, 47 | PlayerEntity player, StackReference cursorStackReference, 48 | CallbackInfoReturnable cir) { 49 | var tag = bundle.get(DataComponentTypes.CUSTOM_DATA); 50 | 51 | if (tag != null && tag.contains("image2map:quick_place") && !player.isCreative()) { 52 | cir.setReturnValue(false); 53 | cir.cancel(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/mixin/EntityPassengersSetS2CPacketAccessor.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map.mixin; 2 | 3 | import net.minecraft.network.PacketByteBuf; 4 | import net.minecraft.network.packet.s2c.play.EntityPassengersSetS2CPacket; 5 | import org.spongepowered.asm.mixin.Mixin; 6 | import org.spongepowered.asm.mixin.gen.Invoker; 7 | 8 | @Mixin(EntityPassengersSetS2CPacket.class) 9 | public interface EntityPassengersSetS2CPacketAccessor { 10 | @Invoker("") 11 | static EntityPassengersSetS2CPacket createEntityPassengersSetS2CPacket(PacketByteBuf buf) { 12 | throw new UnsupportedOperationException(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/mixin/ItemFrameEntityMixin.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map.mixin; 2 | 3 | import net.minecraft.entity.Entity; 4 | import net.minecraft.entity.decoration.ItemFrameEntity; 5 | import net.minecraft.entity.player.PlayerEntity; 6 | import net.minecraft.item.ItemStack; 7 | import net.minecraft.item.Items; 8 | import net.minecraft.server.world.ServerWorld; 9 | import net.minecraft.util.ActionResult; 10 | import net.minecraft.util.Hand; 11 | 12 | import org.jetbrains.annotations.Nullable; 13 | import org.spongepowered.asm.mixin.Mixin; 14 | import org.spongepowered.asm.mixin.Shadow; 15 | import org.spongepowered.asm.mixin.injection.At; 16 | import org.spongepowered.asm.mixin.injection.Inject; 17 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 18 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 19 | import space.essem.image2map.Image2Map; 20 | 21 | @Mixin(ItemFrameEntity.class) 22 | public class ItemFrameEntityMixin { 23 | @Shadow 24 | private boolean fixed; 25 | 26 | @Inject(method = "interact", at = @At("HEAD"), cancellable = true) 27 | private void image2map$fillMaps(PlayerEntity player, Hand hand, CallbackInfoReturnable cir) { 28 | if (!this.fixed && Image2Map.clickItemFrame(player, hand, (ItemFrameEntity) (Object) this)) { 29 | cir.setReturnValue(ActionResult.SUCCESS); 30 | } 31 | } 32 | 33 | @Inject(method = "dropHeldStack", at = @At("HEAD"), cancellable = true) 34 | private void image2map$destroyMaps(ServerWorld world, Entity entity, boolean dropSelf, CallbackInfo ci) { 35 | var frame = (ItemFrameEntity) (Object) this; 36 | 37 | if (!this.fixed && Image2Map.destroyItemFrame(entity, frame)) { 38 | if (dropSelf) { 39 | frame.dropStack(world, new ItemStack(Items.ITEM_FRAME)); 40 | } 41 | ci.cancel(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/mixin/PlayerEntityMixin.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map.mixin; 2 | 3 | import eu.pb4.sgui.virtual.VirtualScreenHandlerInterface; 4 | import net.minecraft.entity.damage.DamageSource; 5 | import net.minecraft.entity.player.PlayerEntity; 6 | 7 | import net.minecraft.screen.ScreenHandler; 8 | import net.minecraft.server.world.ServerWorld; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.injection.At; 12 | import org.spongepowered.asm.mixin.injection.Inject; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; 14 | import space.essem.image2map.gui.MapGui; 15 | 16 | @Mixin(PlayerEntity.class) 17 | public class PlayerEntityMixin { 18 | 19 | @Shadow public ScreenHandler currentScreenHandler; 20 | 21 | @Inject(method = "damage", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;damage(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/entity/damage/DamageSource;F)Z", shift = At.Shift.BEFORE)) 22 | private void image2map$closeOnDamage(ServerWorld world, DamageSource source, float amount, CallbackInfoReturnable cir) { 23 | if (amount > 0 && this.currentScreenHandler instanceof VirtualScreenHandlerInterface handler && handler.getGui() instanceof MapGui computerGui) { 24 | computerGui.close(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/mixin/ServerPlayNetworkHandlerMixin.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map.mixin; 2 | 3 | import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; 4 | import eu.pb4.sgui.virtual.VirtualScreenHandlerInterface; 5 | import net.minecraft.network.ClientConnection; 6 | import net.minecraft.network.message.LastSeenMessageList; 7 | import net.minecraft.network.packet.Packet; 8 | import net.minecraft.network.packet.c2s.play.*; 9 | import net.minecraft.server.MinecraftServer; 10 | import net.minecraft.server.network.ConnectedClientData; 11 | import net.minecraft.server.network.ServerCommonNetworkHandler; 12 | import net.minecraft.server.network.ServerPlayNetworkHandler; 13 | import net.minecraft.server.network.ServerPlayerEntity; 14 | import net.minecraft.util.math.Vec3d; 15 | import org.spongepowered.asm.mixin.Final; 16 | import org.spongepowered.asm.mixin.Mixin; 17 | import org.spongepowered.asm.mixin.Shadow; 18 | import org.spongepowered.asm.mixin.injection.At; 19 | import org.spongepowered.asm.mixin.injection.Inject; 20 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 21 | import space.essem.image2map.gui.MapGui; 22 | 23 | 24 | @Mixin(ServerPlayNetworkHandler.class) 25 | public abstract class ServerPlayNetworkHandlerMixin extends ServerCommonNetworkHandler { 26 | @Shadow 27 | public ServerPlayerEntity player; 28 | 29 | @Shadow private double lastTickX; 30 | 31 | @Shadow private double lastTickY; 32 | 33 | @Shadow private double lastTickZ; 34 | 35 | @Shadow public abstract void syncWithPlayerPosition(); 36 | 37 | public ServerPlayNetworkHandlerMixin(MinecraftServer server, ClientConnection connection, ConnectedClientData clientData) { 38 | super(server, connection, clientData); 39 | } 40 | 41 | @WrapWithCondition(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerPlayerEntity;updatePositionAndAngles(DDDFF)V")) 42 | private boolean image2map$allowMovement(ServerPlayerEntity instance, double x, double y, double z, float p, float yaw) { 43 | if (this.player.currentScreenHandler instanceof VirtualScreenHandlerInterface handler && handler.getGui() instanceof MapGui computerGui) { 44 | double l = instance.getX() - this.lastTickX; 45 | double m = instance.getY() - this.lastTickY; 46 | double n = instance.getZ() - this.lastTickZ; 47 | this.player.getServerWorld().getChunkManager().updatePosition(this.player); 48 | this.player.handleFall(l, m , n, player.isOnGround()); 49 | this.player.setOnGround(player.isOnGround()); 50 | this.syncWithPlayerPosition(); 51 | return false; 52 | } 53 | return true; 54 | } 55 | 56 | /*@Inject(method = "onPlayerMove", at = @At("HEAD"), cancellable = true) 57 | private void image2map$onMove(PlayerMoveC2SPacket packet, CallbackInfo ci) { 58 | if (this.player.currentScreenHandler instanceof VirtualScreenHandlerInterface handler && handler.getGui() instanceof MapGui computerGui) { 59 | this.sendPacket(new EntityS2CPacket.Rotate(player.getId(), (byte) 0, (byte) 0, player.isOnGround())); 60 | this.server.execute(() -> { 61 | var xRot = packet.getPitch (computerGui.xRot); 62 | var yRot = packet.getYaw(computerGui.yRot); 63 | if (xRot != 0 || yRot != 0) { 64 | computerGui.onCameraMove(yRot, xRot); 65 | } 66 | }); 67 | ci.cancel(); 68 | } 69 | }*/ 70 | 71 | @Inject(method = "onRequestCommandCompletions", at = @At("HEAD"), cancellable = true) 72 | private void image2map$onCustomSuggestion(RequestCommandCompletionsC2SPacket packet, CallbackInfo ci) { 73 | if (this.player.currentScreenHandler instanceof VirtualScreenHandlerInterface handler && handler.getGui() instanceof MapGui computerGui) { 74 | this.server.execute(() -> { 75 | computerGui.onCommandSuggestion(packet.getCompletionId(), packet.getPartialCommand()); 76 | }); 77 | ci.cancel(); 78 | } 79 | } 80 | 81 | @Inject(method = "executeCommand", at = @At("HEAD"), cancellable = true) 82 | private void image2map$onCommandExecution(String command, CallbackInfo ci) { 83 | if (this.player.currentScreenHandler instanceof VirtualScreenHandlerInterface handler && handler.getGui() instanceof MapGui computerGui) { 84 | computerGui.executeCommand(command); 85 | ci.cancel(); 86 | } 87 | } 88 | 89 | @Inject(method = "onPlayerInput", at = @At("HEAD"), cancellable = true) 90 | private void image2map$onPlayerInput(PlayerInputC2SPacket packet, CallbackInfo ci) { 91 | if (this.player.currentScreenHandler instanceof VirtualScreenHandlerInterface handler && handler.getGui() instanceof MapGui computerGui) { 92 | this.server.execute(() -> { 93 | computerGui.onPlayerInput(packet.input()); 94 | }); 95 | ci.cancel(); 96 | } 97 | } 98 | 99 | @Inject(method = "onClientCommand", at = @At("HEAD"), cancellable = true) 100 | private void image2map$onClientCommand(ClientCommandC2SPacket packet, CallbackInfo ci) { 101 | if (this.player.currentScreenHandler instanceof VirtualScreenHandlerInterface handler && handler.getGui() instanceof MapGui computerGui) { 102 | this.server.execute(() -> { 103 | computerGui.onPlayerCommand(packet.getEntityId(), packet.getMode(), packet.getMountJumpHeight()); 104 | }); 105 | ci.cancel(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/space/essem/image2map/renderer/MapRenderer.java: -------------------------------------------------------------------------------- 1 | package space.essem.image2map.renderer; 2 | 3 | import java.awt.Graphics2D; 4 | import java.awt.Image; 5 | import java.awt.image.BufferedImage; 6 | import java.awt.image.DataBufferByte; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import eu.pb4.mapcanvas.api.core.CanvasColor; 10 | import eu.pb4.mapcanvas.api.core.CanvasImage; 11 | import eu.pb4.mapcanvas.api.utils.CanvasUtils; 12 | import net.minecraft.component.DataComponentTypes; 13 | import net.minecraft.component.type.LoreComponent; 14 | import net.minecraft.component.type.MapIdComponent; 15 | import net.minecraft.component.type.NbtComponent; 16 | import net.minecraft.item.FilledMapItem; 17 | import net.minecraft.item.ItemStack; 18 | import net.minecraft.item.Items; 19 | import net.minecraft.item.map.MapState; 20 | import net.minecraft.nbt.NbtList; 21 | import net.minecraft.nbt.NbtOps; 22 | import net.minecraft.nbt.NbtString; 23 | import net.minecraft.registry.Registries; 24 | import net.minecraft.registry.RegistryKey; 25 | import net.minecraft.registry.RegistryKeys; 26 | import net.minecraft.server.world.ServerWorld; 27 | import net.minecraft.text.Text; 28 | import net.minecraft.util.Formatting; 29 | import net.minecraft.util.Identifier; 30 | import net.minecraft.util.math.ColorHelper; 31 | import net.minecraft.util.math.MathHelper; 32 | import space.essem.image2map.Image2Map.DitherMode; 33 | import space.essem.image2map.ImageData; 34 | 35 | public class MapRenderer { 36 | private static final double shadeCoeffs[] = { 0.71, 0.86, 1.0, 0.53 }; 37 | 38 | private static double distance(double[] vectorA, double[] vectorB) { 39 | return Math.sqrt(Math.pow(vectorA[0] - vectorB[0], 2) + Math.pow(vectorA[1] - vectorB[1], 2) 40 | + Math.pow(vectorA[2] - vectorB[2], 2)); 41 | } 42 | 43 | private static double[] applyShade(double[] color, int ind) { 44 | double coeff = shadeCoeffs[ind]; 45 | return new double[] { color[0] * coeff, color[1] * coeff, color[2] * coeff }; 46 | } 47 | 48 | public static CanvasImage render(BufferedImage image, DitherMode mode, int width, int height) { 49 | Image resizedImage = image.getScaledInstance(width, height, Image.SCALE_DEFAULT); 50 | BufferedImage resized = convertToBufferedImage(resizedImage); 51 | int[][] pixels = convertPixelArray(resized); 52 | 53 | var state = new CanvasImage(width, height); 54 | 55 | for (int i = 0; i < width; i++) { 56 | for (int j = 0; j < height; j++) { 57 | if (mode.equals(DitherMode.FLOYD)) { 58 | state.set(i, j, floydDither(pixels, i, j, pixels[j][i])); 59 | } else { 60 | state.set(i, j, CanvasUtils.findClosestColorARGB(pixels[j][i])); 61 | } 62 | } 63 | } 64 | 65 | return state; 66 | } 67 | 68 | public static List toVanillaItems(CanvasImage image, ServerWorld world, String url) { 69 | var xSections = MathHelper.ceil(image.getWidth() / 128d); 70 | var ySections = MathHelper.ceil(image.getHeight() / 128d); 71 | 72 | var xDelta = (xSections * 128 - image.getWidth()) / 2; 73 | var yDelta = (ySections * 128 - image.getHeight()) / 2; 74 | 75 | var items = new ArrayList(); 76 | 77 | for (int ys = 0; ys < ySections; ys++) { 78 | for (int xs = 0; xs < xSections; xs++) { 79 | var id = world.increaseAndGetMapId(); 80 | var state = MapState.of(0, 0, (byte) 0, false, false, RegistryKey.of(RegistryKeys.WORLD, Identifier.of("image2map", "generated"))); 81 | 82 | for (int xl = 0; xl < 128; xl++) { 83 | for (int yl = 0; yl < 128; yl++) { 84 | var x = xl + xs * 128 - xDelta; 85 | var y = yl + ys * 128 - yDelta; 86 | 87 | if (x >= 0 && y >= 0 && x < image.getWidth() && y < image.getHeight()) { 88 | state.colors[xl + yl * 128] = image.getRaw(x, y); 89 | } 90 | } 91 | } 92 | 93 | world.putMapState(id, state); 94 | 95 | var stack = new ItemStack(Items.FILLED_MAP); 96 | stack.set(DataComponentTypes.MAP_ID, id); 97 | var data = ImageData.ofSimple(xs, ys, xSections, ySections); 98 | stack.apply(DataComponentTypes.CUSTOM_DATA, NbtComponent.DEFAULT, x -> x.with(NbtOps.INSTANCE, ImageData.CODEC, data).getOrThrow()); 99 | stack.set(DataComponentTypes.LORE, new LoreComponent(List.of( 100 | Text.literal(xs + " / " + ys).formatted(Formatting.GRAY), 101 | Text.literal(url) 102 | ))); 103 | items.add(stack); 104 | } 105 | } 106 | 107 | return items; 108 | } 109 | 110 | /*public static ItemStack render(BufferedImage image, DitherMode mode, ServerWorld world, int width, int height, 111 | PlayerEntity player) { 112 | // mojang removed the ability to set a map as locked via the "locked" field in 113 | // 1.17, so we create and apply our own MapState instead 114 | ItemStack stack = new ItemStack(Items.FILLED_MAP); 115 | int id = world.getNextMapId(); 116 | NbtCompound nbt = new NbtCompound(); 117 | 118 | nbt.putString("dimension", world.getRegistryKey().getValue().toString()); 119 | nbt.putInt("xCenter", (int) 0); 120 | nbt.putInt("zCenter", (int) 0); 121 | nbt.putBoolean("locked", true); 122 | nbt.putBoolean("unlimitedTracking", false); 123 | nbt.putBoolean("trackingPosition", false); 124 | nbt.putByte("scale", (byte) 3); 125 | MapState state = MapState.fromNbt(nbt); 126 | world.putMapState(FilledMapItem.getMapName(id), state); 127 | stack.getOrCreateNbt().putInt("map", id); 128 | 129 | Image resizedImage = image.getScaledInstance(128, 128, Image.SCALE_DEFAULT); 130 | BufferedImage resized = convertToBufferedImage(resizedImage); 131 | int width = resized.getWidth(); 132 | int height = resized.getHeight(); 133 | int[][] pixels = convertPixelArray(resized); 134 | MapColor[] mapColors = MapColor.COLORS; 135 | Color imageColor; 136 | mapColors = Arrays.stream(mapColors).filter(Objects::nonNull).toArray(MapColor[]::new); 137 | 138 | for (int i = 0; i < width; i++) { 139 | for (int j = 0; j < height; j++) { 140 | imageColor = new Color(pixels[j][i], true); 141 | if (mode.equals(DitherMode.FLOYD)) 142 | state.colors[i + j * width] = (byte) floydDither(mapColors, pixels, i, j, imageColor); 143 | else 144 | state.colors[i + j * width] = (byte) nearestColor(mapColors, imageColor); 145 | } 146 | } 147 | return stack; 148 | }*/ 149 | 150 | private static int mapColorToRGBColor(CanvasColor color) { 151 | var mcColor = color.getRgbColor(); 152 | double[] mcColorVec = { (double) ColorHelper.getRed(mcColor), (double) ColorHelper.getGreen(mcColor), (double) ColorHelper.getBlue(mcColor) }; 153 | double coeff = shadeCoeffs[color.getColor().id & 3]; 154 | return ColorHelper.getArgb(0, (int) (mcColorVec[0] * coeff), (int) (mcColorVec[1] * coeff), (int) (mcColorVec[2] * coeff)); 155 | } 156 | 157 | private static CanvasColor floydDither(int[][] pixels, int x, int y, int imageColor) { 158 | var closestColor = CanvasUtils.findClosestColorARGB(imageColor); 159 | var palletedColor = mapColorToRGBColor(closestColor); 160 | 161 | var errorR = ColorHelper.getRed(imageColor) - ColorHelper.getRed(palletedColor); 162 | var errorG = ColorHelper.getGreen(imageColor) - ColorHelper.getGreen(palletedColor); 163 | var errorB = ColorHelper.getBlue(imageColor) - ColorHelper.getBlue(palletedColor); 164 | if (pixels[0].length > x + 1) { 165 | pixels[y][x + 1] = applyError(pixels[y][x + 1], errorR, errorG, errorB, 7.0 / 16.0); 166 | } 167 | if (pixels.length > y + 1) { 168 | if (x > 0) { 169 | pixels[y + 1][x - 1] = applyError(pixels[y + 1][x - 1], errorR, errorG, errorB, 3.0 / 16.0); 170 | } 171 | pixels[y + 1][x] = applyError(pixels[y + 1][x], errorR, errorG, errorB, 5.0 / 16.0); 172 | if (pixels[0].length > x + 1) { 173 | pixels[y + 1][x + 1] = applyError(pixels[y + 1][x + 1], errorR, errorG, errorB, 1.0 / 16.0); 174 | } 175 | } 176 | 177 | return closestColor; 178 | } 179 | 180 | private static int applyError(int pixelColor, int errorR, int errorG, int errorB, double quantConst) { 181 | int pR = clamp( ColorHelper.getRed(pixelColor) + (int) ((double) errorR * quantConst), 0, 255); 182 | int pG = clamp(ColorHelper.getGreen(pixelColor) + (int) ((double) errorG * quantConst), 0, 255); 183 | int pB = clamp(ColorHelper.getBlue(pixelColor) + (int) ((double) errorB * quantConst), 0, 255); 184 | return ColorHelper.getArgb(ColorHelper.getAlpha(pixelColor), pR, pG, pB); 185 | } 186 | 187 | private static int clamp(int i, int min, int max) { 188 | if (min > max) 189 | throw new IllegalArgumentException("max value cannot be less than min value"); 190 | if (i < min) 191 | return min; 192 | if (i > max) 193 | return max; 194 | return i; 195 | } 196 | 197 | private static int[][] convertPixelArray(BufferedImage image) { 198 | 199 | final byte[] pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); 200 | final int width = image.getWidth(); 201 | final int height = image.getHeight(); 202 | 203 | int[][] result = new int[height][width]; 204 | final int pixelLength = 4; 205 | for (int pixel = 0, row = 0, col = 0; pixel + 3 < pixels.length; pixel += pixelLength) { 206 | int argb = 0; 207 | argb += (((int) pixels[pixel] & 0xff) << 24); // alpha 208 | argb += ((int) pixels[pixel + 1] & 0xff); // blue 209 | argb += (((int) pixels[pixel + 2] & 0xff) << 8); // green 210 | argb += (((int) pixels[pixel + 3] & 0xff) << 16); // red 211 | result[row][col] = argb; 212 | col++; 213 | if (col == width) { 214 | col = 0; 215 | row++; 216 | } 217 | } 218 | 219 | return result; 220 | } 221 | 222 | private static BufferedImage convertToBufferedImage(Image image) { 223 | BufferedImage newImage = new BufferedImage(image.getWidth(null), image.getHeight(null), 224 | BufferedImage.TYPE_4BYTE_ABGR); 225 | Graphics2D g = newImage.createGraphics(); 226 | g.drawImage(image, 0, 0, null); 227 | g.dispose(); 228 | return newImage; 229 | } 230 | } -------------------------------------------------------------------------------- /src/main/resources/assets/image2map/gui/poly_buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patbox/Image2Map/a8e22b008be35c45cf1cf46df6fd45906826734e/src/main/resources/assets/image2map/gui/poly_buttons.png -------------------------------------------------------------------------------- /src/main/resources/assets/image2map/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Patbox/Image2Map/a8e22b008be35c45cf1cf46df6fd45906826734e/src/main/resources/assets/image2map/icon.png -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "image2map", 4 | "version": "${version}", 5 | 6 | "name": "Image2Map", 7 | "description": "Turn images into maps!", 8 | "authors": [ 9 | "Essem", 10 | "Patbox" 11 | ], 12 | "contact": { 13 | "homepage": "https://essem.space/", 14 | "sources": "https://github.com/TheEssem/Image2Map" 15 | }, 16 | 17 | "license": "MIT", 18 | "icon": "assets/image2map/icon.png", 19 | 20 | "environment": "*", 21 | "entrypoints": { 22 | "main": [ 23 | "space.essem.image2map.Image2Map" 24 | ], 25 | "preLaunch": [ 26 | "space.essem.image2map.CardboardWarning" 27 | ] 28 | }, 29 | "accessWidener": "image2map.accesswidener", 30 | "mixins": [ 31 | "image2map.mixins.json" 32 | ], 33 | "depends": { 34 | "fabricloader": ">=0.14.0", 35 | "fabric-api": "*" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/image2map.accesswidener: -------------------------------------------------------------------------------- 1 | accessWidener v1 named 2 | accessible field net/minecraft/block/MapColor COLORS [Lnet/minecraft/block/MapColor; -------------------------------------------------------------------------------- /src/main/resources/image2map.mixins.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "space.essem.image2map.mixin", 4 | "compatibilityLevel": "JAVA_21", 5 | "mixins": [ 6 | "BundleItemMixin", 7 | "EntityPassengersSetS2CPacketAccessor", 8 | "ItemFrameEntityMixin", 9 | "PlayerEntityMixin", 10 | "ServerPlayNetworkHandlerMixin" 11 | ], 12 | "server": [], 13 | "injectors": { 14 | "defaultRequire": 1 15 | } 16 | } 17 | --------------------------------------------------------------------------------