├── .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 | 
8 | 
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 | 
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------