From ff0ce5c2276c989a48105db617d78eb12d4d680b Mon Sep 17 00:00:00 2001 From: spazzylemons Date: Mon, 18 May 2020 12:18:19 -0400 Subject: [PATCH] Initial commit --- .gitignore | 17 + LICENSE | 21 ++ README.md | 19 ++ build.gradle | 95 ++++++ gradle.properties | 22 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52818 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++++++++ gradlew.bat | 84 +++++ settings.gradle | 16 + .../AbstractClientPlayerEntityMixin.java | 42 +++ .../extraplayermodels/mixin/CameraMixin.java | 49 +++ .../mixin/EntityRenderDispatcherMixin.java | 40 +++ .../mixin/MinecraftClientMixin.java | 43 +++ .../mixin/MinecraftServerMixin.java | 22 ++ .../mixin/ModelPartMixin.java | 23 ++ .../extraplayermodels/mixin/MouseMixin.java | 31 ++ .../mixin/PlayerManagerMixin.java | 42 +++ .../mixin/SkinOptionsScreenMixin.java | 32 ++ .../mixin/TitleScreenMixin.java | 78 +++++ .../extraplayermodels/CameraKeybind.kt | 8 + .../spazzylemons/extraplayermodels/Entry.kt | 11 + .../extraplayermodels/ExtraPlayerModels.kt | 86 +++++ .../ExtraPlayerModelsModMenu.kt | 18 + .../extraplayermodels/MixinInterfaces.kt | 13 + .../extraplayermodels/client/ClientData.kt | 126 +++++++ .../extraplayermodels/client/Configuration.kt | 10 + .../client/gui/ConfigScreen.kt | 66 ++++ .../client/gui/PreviewPlayerFunctions.kt | 53 +++ .../extraplayermodels/model/CustomModelBox.kt | 49 +++ .../model/CustomModelPart.kt | 73 ++++ .../model/CustomPlayerModel.kt | 33 ++ .../extraplayermodels/model/ImageArray.kt | 20 ++ .../extraplayermodels/model/ModelBuilder.kt | 54 +++ .../extraplayermodels/model/ModelLoader.kt | 311 ++++++++++++++++++ .../extraplayermodels/model/ModelRotate.kt | 12 + .../extraplayermodels/model/UVCoordinate.kt | 6 + .../model/json/ConfigurationDeserializer.kt | 52 +++ .../model/json/CustomModelBoxDeserializer.kt | 30 ++ .../model/json/CustomModelPartDeserializer.kt | 28 ++ .../json/CustomPlayerModelDeserializer.kt | 29 ++ .../model/json/DeserializerHelpers.kt | 27 ++ .../model/json/ImageArrayDeserializer.kt | 50 +++ .../model/json/UVDeserializer.kt | 36 ++ .../model/json/VectorDeserializer.kt | 38 +++ .../packet/C2SInsertModel.kt | 24 ++ .../packet/C2SRemoveModel.kt | 22 ++ .../packet/NewPacketByteBuf.kt | 6 + .../extraplayermodels/packet/Packet.kt | 47 +++ .../packet/S2CInsertModel.kt | 23 ++ .../packet/S2CRemoveModel.kt | 21 ++ .../render/CustomPlayerEntityModel.kt | 40 +++ .../render/CustomPlayerEntityRenderer.kt | 17 + .../render/PreviewPlayerEntity.kt | 83 +++++ .../extraplayermodels/server/ServerData.kt | 52 +++ .../assets/extraplayermodels/icon.png | Bin 0 -> 8080 bytes .../assets/extraplayermodels/lang/en_us.json | 14 + .../extraplayermodels/textures/skin/steve.png | Bin 0 -> 1350 bytes .../resources/extraplayermodels.mixins.json | 22 ++ src/main/resources/fabric.mod.json | 40 +++ 60 files changed, 2504 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/spazzylemons/extraplayermodels/mixin/AbstractClientPlayerEntityMixin.java create mode 100644 src/main/java/spazzylemons/extraplayermodels/mixin/CameraMixin.java create mode 100644 src/main/java/spazzylemons/extraplayermodels/mixin/EntityRenderDispatcherMixin.java create mode 100644 src/main/java/spazzylemons/extraplayermodels/mixin/MinecraftClientMixin.java create mode 100644 src/main/java/spazzylemons/extraplayermodels/mixin/MinecraftServerMixin.java create mode 100644 src/main/java/spazzylemons/extraplayermodels/mixin/ModelPartMixin.java create mode 100644 src/main/java/spazzylemons/extraplayermodels/mixin/MouseMixin.java create mode 100644 src/main/java/spazzylemons/extraplayermodels/mixin/PlayerManagerMixin.java create mode 100644 src/main/java/spazzylemons/extraplayermodels/mixin/SkinOptionsScreenMixin.java create mode 100644 src/main/java/spazzylemons/extraplayermodels/mixin/TitleScreenMixin.java create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/CameraKeybind.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/Entry.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/ExtraPlayerModels.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/ExtraPlayerModelsModMenu.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/MixinInterfaces.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/client/ClientData.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/client/Configuration.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/client/gui/ConfigScreen.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/client/gui/PreviewPlayerFunctions.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/CustomModelBox.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/CustomModelPart.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/CustomPlayerModel.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/ImageArray.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/ModelBuilder.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/ModelLoader.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/ModelRotate.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/UVCoordinate.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/json/ConfigurationDeserializer.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomModelBoxDeserializer.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomModelPartDeserializer.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomPlayerModelDeserializer.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/json/DeserializerHelpers.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/json/ImageArrayDeserializer.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/json/UVDeserializer.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/model/json/VectorDeserializer.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/packet/C2SInsertModel.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/packet/C2SRemoveModel.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/packet/NewPacketByteBuf.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/packet/Packet.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/packet/S2CInsertModel.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/packet/S2CRemoveModel.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/render/CustomPlayerEntityModel.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/render/CustomPlayerEntityRenderer.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/render/PreviewPlayerEntity.kt create mode 100644 src/main/kotlin/spazzylemons/extraplayermodels/server/ServerData.kt create mode 100644 src/main/resources/assets/extraplayermodels/icon.png create mode 100644 src/main/resources/assets/extraplayermodels/lang/en_us.json create mode 100644 src/main/resources/assets/extraplayermodels/textures/skin/steve.png create mode 100644 src/main/resources/extraplayermodels.mixins.json create mode 100644 src/main/resources/fabric.mod.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c20fb20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# gradle + +.gradle/ +build/ +out/ + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# fabric + +run/ +minecraft/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3abd544 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 spazzylemons + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..18f7abc --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Extra Player Models + +Adds the ability to create custom player models. + +## Installing + +This mod requires the [Fabric Language Kotlin](https://minecraft.curseforge.com/projects/fabric-language-kotlin) mod. + +## Configuration + +The configuration for Extra Player Models can be found under Appearance on the title screen, as a subpage of the vanilla Skin Customization, as well as in [Mod Menu](https://minecraft.curseforge.com/projects/modmenu). + +## Wiki + +Coming soon. + +## License + +This mod is licensed under the [MIT License](https://opensource.org/licenses/MIT). diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..dbe1e45 --- /dev/null +++ b/build.gradle @@ -0,0 +1,95 @@ +plugins { + id 'fabric-loom' + id 'maven-publish' + id "org.jetbrains.kotlin.jvm" +} + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +archivesBaseName = project.archives_base_name +version = project.mod_version +group = project.maven_group + +minecraft { +} + + +repositories { + maven { url = "http://maven.fabricmc.net/" } +} + +dependencies { + //to change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + modImplementation "net.fabricmc:fabric-language-kotlin:${project.fabric_kotlin_version}" + + // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. + // You may need to force-disable transitiveness on them. + + // ModMenu support + modCompileOnly("io.github.prospector:modmenu:1.10.2+build.32") + + +} + +processResources { + inputs.property "version", project.version + + from(sourceSets.main.resources.srcDirs) { + include "fabric.mod.json" + expand "version": project.version + } + + from(sourceSets.main.resources.srcDirs) { + exclude "fabric.mod.json" + } +} + +// ensure that the encoding is set to UTF-8, no matter what the system default is +// this fixes some edge cases with special characters not displaying correctly +// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html +tasks.withType(JavaCompile) { + options.encoding = "UTF-8" +} + +// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task +// if it is present. +// If you remove this task, sources will not be generated. +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = "sources" + from sourceSets.main.allSource +} + +jar { + from "LICENSE" +} + +// configure the maven publication +publishing { + publications { + mavenJava(MavenPublication) { + // add all the jars that should be included when publishing to maven + artifact(remapJar) { + builtBy remapJar + } + artifact(sourcesJar) { + builtBy remapSourcesJar + } + } + } + + // select the repositories you want to publish to + repositories { + // uncomment to publish to the local maven + // mavenLocal() + } +} + +compileKotlin.kotlinOptions.jvmTarget = "1.8" \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..a69fd6a --- /dev/null +++ b/gradle.properties @@ -0,0 +1,22 @@ +kotlin.code.style=official +org.gradle.jvmargs=-Xmx1G + +# Fabric Properties + # Check these on https://modmuss50.me/fabric.html + minecraft_version=1.15.2 + yarn_mappings=1.15.2+build.15 + loader_version=0.8.2+build.194 + + #Fabric api + fabric_version=0.10.1+build.307-1.15 + + loom_version=0.2.7-SNAPSHOT + + # Mod Properties + mod_version = 1.0.0 + maven_group = spazzylemons + archives_base_name = extraplayermodels + +# Kotlin + kotlin_version=1.3.61 + fabric_kotlin_version=1.3.61+build.1 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..deedc7fa5e6310eac3148a7dd0b1f069b07364cb GIT binary patch literal 52818 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≺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 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b24234d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Jul 27 10:29:07 IDT 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..2f33060 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + jcenter() + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } + + plugins { + id 'fabric-loom' version loom_version + id "org.jetbrains.kotlin.jvm" version kotlin_version + } + +} diff --git a/src/main/java/spazzylemons/extraplayermodels/mixin/AbstractClientPlayerEntityMixin.java b/src/main/java/spazzylemons/extraplayermodels/mixin/AbstractClientPlayerEntityMixin.java new file mode 100644 index 0000000..3ae9454 --- /dev/null +++ b/src/main/java/spazzylemons/extraplayermodels/mixin/AbstractClientPlayerEntityMixin.java @@ -0,0 +1,42 @@ +package spazzylemons.extraplayermodels.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.util.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import spazzylemons.extraplayermodels.client.ClientData; + +import java.util.UUID; + +/** + * Mixin to override the usual skin texture logic. + */ +@Environment(EnvType.CLIENT) +@Mixin(AbstractClientPlayerEntity.class) +public class AbstractClientPlayerEntityMixin { + + // Use a custom skin texture if a custom model exists + @Inject(at = @At("HEAD"), method = "getSkinTexture", cancellable = true) + public void getSkinTexture(CallbackInfoReturnable cir) { + UUID uuid = ((AbstractClientPlayerEntity) (Object) this).getUuid(); + if (MinecraftClient.getInstance().player != null && uuid.equals(MinecraftClient.getInstance().player.getUuid())) { + // Only use custom texture if there is a model to go with it + if (ClientData.INSTANCE.getCurrentModel() != null) { + Identifier identifier = ClientData.INSTANCE.getLocalTextureIdentifier(); + if (identifier != null) { + cir.setReturnValue(identifier); + } + } + } else if (ClientData.INSTANCE.getPlayerModels().containsKey(uuid)) { + Identifier identifier = ClientData.INSTANCE.getTextureIdentifiers().get(uuid); + if (identifier != null) { + cir.setReturnValue(identifier); + } + } + } +} diff --git a/src/main/java/spazzylemons/extraplayermodels/mixin/CameraMixin.java b/src/main/java/spazzylemons/extraplayermodels/mixin/CameraMixin.java new file mode 100644 index 0000000..3bf8a4b --- /dev/null +++ b/src/main/java/spazzylemons/extraplayermodels/mixin/CameraMixin.java @@ -0,0 +1,49 @@ +package spazzylemons.extraplayermodels.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.Mouse; +import net.minecraft.client.render.Camera; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.MathHelper; +import net.minecraft.world.BlockView; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import spazzylemons.extraplayermodels.CameraKeybindKt; + +/** + * Mixin to add feature for looking at player model in third-person. + */ +@Environment(EnvType.CLIENT) +@Mixin(Camera.class) +public abstract class CameraMixin { + @Shadow private float cameraY; + + @Shadow private float lastCameraY; + + @Shadow protected abstract void setRotation(float yaw, float pitch); + + @Shadow protected abstract void setPos(double x, double y, double z); + + @Shadow protected abstract void moveBy(double x, double y, double z); + + @Shadow protected abstract double clipToSpace(double desiredCameraDistance); + + // WIP camera method for looking around, useful for looking at model + @Inject(at = @At("TAIL"), method = "update") + public void update(BlockView area, Entity focusedEntity, boolean thirdPerson, boolean inverseView, float tickDelta, CallbackInfo ci) { + MinecraftClient client = MinecraftClient.getInstance(); + if (thirdPerson) { + if (CameraKeybindKt.getCameraKeybind().isPressed() && client.player != null) { + Mouse mouse = client.mouse; + this.setPos(MathHelper.lerp(tickDelta, focusedEntity.prevX, focusedEntity.getX()), MathHelper.lerp(tickDelta, focusedEntity.prevY, focusedEntity.getY()) + MathHelper.lerp(tickDelta, this.lastCameraY, this.cameraY), MathHelper.lerp(tickDelta, focusedEntity.prevZ, focusedEntity.getZ())); + this.setRotation((float) mouse.getX(), 180 + (float) mouse.getY()); + this.moveBy(-this.clipToSpace(4.0D), 0.0D, 0.0D); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/spazzylemons/extraplayermodels/mixin/EntityRenderDispatcherMixin.java b/src/main/java/spazzylemons/extraplayermodels/mixin/EntityRenderDispatcherMixin.java new file mode 100644 index 0000000..ee54d11 --- /dev/null +++ b/src/main/java/spazzylemons/extraplayermodels/mixin/EntityRenderDispatcherMixin.java @@ -0,0 +1,40 @@ +package spazzylemons.extraplayermodels.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.entity.EntityRenderDispatcher; +import net.minecraft.client.render.entity.EntityRenderer; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import spazzylemons.extraplayermodels.client.ClientData; +import spazzylemons.extraplayermodels.render.CustomPlayerEntityRenderer; +import spazzylemons.extraplayermodels.render.PreviewPlayerEntity; + +/** + * Mixin to override the usual renderer logic. + */ +@Environment(EnvType.CLIENT) +@Mixin(EntityRenderDispatcher.class) +public class EntityRenderDispatcherMixin { + // Use a custom renderer if we have one, or if the entity is the dummy entity, use the local renderer. + @Inject(at = @At("HEAD"), method = "getRenderer", cancellable = true) + public void getRenderer(T entity, CallbackInfoReturnable> cir) { + if (!(entity instanceof PlayerEntity)) return; + PlayerEntity mcPlayer = MinecraftClient.getInstance().player; + CustomPlayerEntityRenderer renderer; + if (entity instanceof PreviewPlayerEntity || mcPlayer != null && mcPlayer.getUuid().equals(entity.getUuid())) { + renderer = ClientData.INSTANCE.getCurrentRenderer(); + } else { + renderer = ClientData.INSTANCE.getPlayerModels().get(entity.getUuid()); + } + + if (renderer != null) { + cir.setReturnValue((EntityRenderer) renderer); + } + } +} \ No newline at end of file diff --git a/src/main/java/spazzylemons/extraplayermodels/mixin/MinecraftClientMixin.java b/src/main/java/spazzylemons/extraplayermodels/mixin/MinecraftClientMixin.java new file mode 100644 index 0000000..a56daa6 --- /dev/null +++ b/src/main/java/spazzylemons/extraplayermodels/mixin/MinecraftClientMixin.java @@ -0,0 +1,43 @@ +package spazzylemons.extraplayermodels.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.client.world.ClientWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import spazzylemons.extraplayermodels.client.ClientData; +import spazzylemons.extraplayermodels.client.gui.PreviewPlayerFunctionsKt; + +/** + * Several mixins. + */ +@Environment(EnvType.CLIENT) +@Mixin(MinecraftClient.class) +public abstract class MinecraftClientMixin { + @Shadow public ClientWorld world; + + @Shadow public abstract ClientPlayNetworkHandler getNetworkHandler(); + + public boolean playerModelSentOnJoin; + // Notify the server when we join a world + @Inject(at = @At("TAIL"), method = "joinWorld") + public void joinWorld(ClientWorld clientWorld, CallbackInfo ci) { + playerModelSentOnJoin = false; + } + + // 1. Try to send the first model update packet when connected + // 2. Every tick, tick the dummy entity + @Inject(at = @At("TAIL"), method = "tick") + public void tick(CallbackInfo ci) { + if (world != null && !playerModelSentOnJoin && getNetworkHandler() != null) { + ClientData.INSTANCE.notifyServer(); + playerModelSentOnJoin = true; + } + PreviewPlayerFunctionsKt.tickDummyEntity(); + } +} diff --git a/src/main/java/spazzylemons/extraplayermodels/mixin/MinecraftServerMixin.java b/src/main/java/spazzylemons/extraplayermodels/mixin/MinecraftServerMixin.java new file mode 100644 index 0000000..c88b704 --- /dev/null +++ b/src/main/java/spazzylemons/extraplayermodels/mixin/MinecraftServerMixin.java @@ -0,0 +1,22 @@ +package spazzylemons.extraplayermodels.mixin; + +import net.minecraft.server.MinecraftServer; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import spazzylemons.extraplayermodels.IMinecraftServerMixin; +import spazzylemons.extraplayermodels.server.ServerData; + +/** + * Interface implementation. + */ +@Mixin(MinecraftServer.class) +public class MinecraftServerMixin implements IMinecraftServerMixin { + // Add server data to each instance of MinecraftServer + public final ServerData playerModelData = new ServerData((MinecraftServer) (Object) this); + + // Accessor method because you can't type cast to a mixin, so an interface must be used instead. + @NotNull @Override + public ServerData getPlayerModelData() { + return playerModelData; + } +} diff --git a/src/main/java/spazzylemons/extraplayermodels/mixin/ModelPartMixin.java b/src/main/java/spazzylemons/extraplayermodels/mixin/ModelPartMixin.java new file mode 100644 index 0000000..4b6ab52 --- /dev/null +++ b/src/main/java/spazzylemons/extraplayermodels/mixin/ModelPartMixin.java @@ -0,0 +1,23 @@ +package spazzylemons.extraplayermodels.mixin; + +import it.unimi.dsi.fastutil.objects.ObjectList; +import net.minecraft.client.model.ModelPart; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import spazzylemons.extraplayermodels.IModelPartMixin; + +/** + * Interface implementation + */ +@Mixin(ModelPart.class) +public class ModelPartMixin implements IModelPartMixin { + @Shadow @Final private ObjectList cuboids; + + // Accessor for a ModelPart's cuboids, useful for making a custom model + @NotNull @Override + public ObjectList getCuboids() { + return this.cuboids; + } +} diff --git a/src/main/java/spazzylemons/extraplayermodels/mixin/MouseMixin.java b/src/main/java/spazzylemons/extraplayermodels/mixin/MouseMixin.java new file mode 100644 index 0000000..752759d --- /dev/null +++ b/src/main/java/spazzylemons/extraplayermodels/mixin/MouseMixin.java @@ -0,0 +1,31 @@ +package spazzylemons.extraplayermodels.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.Mouse; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; +import spazzylemons.extraplayermodels.CameraKeybindKt; + +/** + * Mixin for custom camera keybind. + */ +@Environment(EnvType.CLIENT) +@Mixin(Mouse.class) +public class MouseMixin { + @Shadow @Final private MinecraftClient client; + + // More mixins for camera look keybind + @Inject(at = @At("TAIL"), method = "updateMouse", locals = LocalCapture.CAPTURE_FAILEXCEPTION) + public void updateMouse(CallbackInfo ci, double e, double l, double m, int n) { + if (this.client.gameRenderer.getCamera().isThirdPerson() && CameraKeybindKt.getCameraKeybind().isPressed() && this.client.player != null) { + this.client.player.changeLookDirection(-l, -(m * n)); + } + } +} \ No newline at end of file diff --git a/src/main/java/spazzylemons/extraplayermodels/mixin/PlayerManagerMixin.java b/src/main/java/spazzylemons/extraplayermodels/mixin/PlayerManagerMixin.java new file mode 100644 index 0000000..3f55a97 --- /dev/null +++ b/src/main/java/spazzylemons/extraplayermodels/mixin/PlayerManagerMixin.java @@ -0,0 +1,42 @@ +package spazzylemons.extraplayermodels.mixin; + +import io.netty.buffer.Unpooled; +import net.fabricmc.fabric.api.network.ServerSidePacketRegistry; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.PacketByteBuf; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import spazzylemons.extraplayermodels.IMinecraftServerMixin; +import spazzylemons.extraplayermodels.packet.S2CInsertModel; + +/** + * Mixins for managing the server's player models + */ +@Mixin(PlayerManager.class) +public class PlayerManagerMixin { + @Shadow @Final private MinecraftServer server; + + // When a player connects, send them all of the player models + @Inject(at = @At("TAIL"), method = "onPlayerConnect") + public void onPlayerConnect(ClientConnection connection, ServerPlayerEntity player, CallbackInfo ci) { + ((IMinecraftServerMixin) server).getPlayerModelData().getModelList().forEach((key, value) -> { + PacketByteBuf buf = new PacketByteBuf(Unpooled.buffer()); + buf.writeUuid(key); + value.writeToBuf(buf); + ServerSidePacketRegistry.INSTANCE.sendToPlayer(player, S2CInsertModel.INSTANCE.getId(), buf); + }); + } + + // When a player leaves, remove their player model, in turn notifying everyone + @Inject(at = @At("TAIL"), method = "remove") + public void remove(ServerPlayerEntity player, CallbackInfo ci) { + ((IMinecraftServerMixin) server).getPlayerModelData().remove(player.getUuid()); + } +} \ No newline at end of file diff --git a/src/main/java/spazzylemons/extraplayermodels/mixin/SkinOptionsScreenMixin.java b/src/main/java/spazzylemons/extraplayermodels/mixin/SkinOptionsScreenMixin.java new file mode 100644 index 0000000..2eb6257 --- /dev/null +++ b/src/main/java/spazzylemons/extraplayermodels/mixin/SkinOptionsScreenMixin.java @@ -0,0 +1,32 @@ +package spazzylemons.extraplayermodels.mixin; + +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.options.SkinOptionsScreen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import spazzylemons.extraplayermodels.client.gui.ConfigScreen; + +/** + * Mixin to add a button to the skin options. + */ +@Mixin(SkinOptionsScreen.class) +public class SkinOptionsScreenMixin extends Screen { + protected SkinOptionsScreenMixin(Text title) { + super(title); + } + + // Add a button for Extra Player Models settings in the skin options, as it makes sense to put one there + @Inject(at = @At("TAIL"), method = "init") + public void init(CallbackInfo ci) { + this.addButton(new ButtonWidget(this.width / 2 - 100, height - 48, 200, 20, I18n.translate("text.extraplayermodels.appearance.withmodname"), (buttonWidget) -> { + if (minecraft != null) { + minecraft.openScreen(new ConfigScreen(this)); + } + })); + } +} diff --git a/src/main/java/spazzylemons/extraplayermodels/mixin/TitleScreenMixin.java b/src/main/java/spazzylemons/extraplayermodels/mixin/TitleScreenMixin.java new file mode 100644 index 0000000..ba6cc0a --- /dev/null +++ b/src/main/java/spazzylemons/extraplayermodels/mixin/TitleScreenMixin.java @@ -0,0 +1,78 @@ +package spazzylemons.extraplayermodels.mixin; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.TitleScreen; +import net.minecraft.client.gui.widget.AbstractButtonWidget; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TexturedButtonWidget; +import net.minecraft.client.resource.language.I18n; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import spazzylemons.extraplayermodels.client.ClientData; +import spazzylemons.extraplayermodels.client.gui.ConfigScreen; +import spazzylemons.extraplayermodels.client.gui.PreviewPlayerFunctionsKt; + +/** + * Mixin to add a button and player model to the title screen. + */ +@Environment(EnvType.CLIENT) +@Mixin(TitleScreen.class) +public abstract class TitleScreenMixin extends Screen { + protected TitleScreenMixin(Text title) { + super(title); + } + + /** + * X position to render the player model at. + */ + private int renderPlayerAtX; + + /** + * Y position to render the player model at. + */ + private int renderPlayerAtY; + + // Add a button for Extra Player Models settings on the title screen, because why not? + @Inject(at = @At("TAIL"), method = "init") + public void init(CallbackInfo ci) { + ClientData.INSTANCE.reload(); + + AbstractButtonWidget accessibilityButton = null; + String text = I18n.translate("narrator.button.accessibility"); + + for (AbstractButtonWidget button : buttons) { + if (button instanceof TexturedButtonWidget && button.getMessage().equals(text)) { + accessibilityButton = button; + break; + } + } + + if (accessibilityButton == null) { + renderPlayerAtX = width / 2 + 124 + 39; + renderPlayerAtY = height / 4 + 48 + 72 + 12 - 4; + } else { + renderPlayerAtX = accessibilityButton.x + accessibilityButton.getWidth() + 39; + renderPlayerAtY = accessibilityButton.y - 4; + } + addButton(new ButtonWidget(renderPlayerAtX - 35, renderPlayerAtY + 4, 70, 20, I18n.translate("text.extraplayermodels.appearance"), buttonWidget -> { + if (minecraft != null) { + minecraft.openScreen(new ConfigScreen(this)); + } + })); + } + + // Show the player model on the title screen, Bedrock style + @Inject(at = @At("TAIL"), method = "render") + public void render(int mouseX, int mouseY, float delta, CallbackInfo ci) { + PreviewPlayerFunctionsKt.renderPlayerOnScreen( + renderPlayerAtX, + renderPlayerAtY, + 30 + ); + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/CameraKeybind.kt b/src/main/kotlin/spazzylemons/extraplayermodels/CameraKeybind.kt new file mode 100644 index 0000000..ace8da9 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/CameraKeybind.kt @@ -0,0 +1,8 @@ +package spazzylemons.extraplayermodels + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.client.keybinding.FabricKeyBinding + +@Environment(EnvType.CLIENT) +lateinit var CameraKeybind: FabricKeyBinding diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/Entry.kt b/src/main/kotlin/spazzylemons/extraplayermodels/Entry.kt new file mode 100644 index 0000000..b900c36 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/Entry.kt @@ -0,0 +1,11 @@ +package spazzylemons.extraplayermodels + +@Suppress("unused") +fun init() { + ExtraPlayerModels.init() +} + +@Suppress("unused") +fun clientInit() { + ExtraPlayerModels.clientInit() +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/ExtraPlayerModels.kt b/src/main/kotlin/spazzylemons/extraplayermodels/ExtraPlayerModels.kt new file mode 100644 index 0000000..d708d82 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/ExtraPlayerModels.kt @@ -0,0 +1,86 @@ +package spazzylemons.extraplayermodels + +import net.fabricmc.fabric.api.client.keybinding.FabricKeyBinding +import net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry +import net.fabricmc.fabric.api.network.ClientSidePacketRegistry +import net.fabricmc.fabric.api.network.ServerSidePacketRegistry +import net.minecraft.client.util.InputUtil +import net.minecraft.text.TranslatableText +import net.minecraft.util.Identifier +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import org.lwjgl.glfw.GLFW +import spazzylemons.extraplayermodels.packet.C2SInsertModel +import spazzylemons.extraplayermodels.packet.C2SRemoveModel +import spazzylemons.extraplayermodels.packet.S2CInsertModel +import spazzylemons.extraplayermodels.packet.S2CRemoveModel + +/** + * Contains useful variables and methods for the initialization and operation of the mod. + */ +object ExtraPlayerModels { + /** + * The mod's namespace. + */ + const val NAMESPACE = "extraplayermodels" + + /** + * In older versions of Minecraft, the length of a custom packet's name is limited. To enable the possibility of + * creating server-side plugins that can run on old Minecraft versions such as 1.8, a shorter namespace is used + * for packet channels. + */ + const val PACKET_NAMESPACE = "xtrapmodels" + + /** + * The mod logger. + */ + private val LOGGER: Logger = LogManager.getLogger() + + /** + * Log information. + * @param info The translation key. + * @param args The arguments + */ + fun logInfo(info: String, vararg args: Any?) { + LOGGER.info(TranslatableText(info, args).string) + } + + /** + * Log an error. + * @param info The translation key. + * @param args The arguments + */ + fun logError(info: String, vararg args: Any?) { + LOGGER.error(TranslatableText(info, args).string) + } + + fun init() { + // Packets + C2SInsertModel.register(ServerSidePacketRegistry.INSTANCE) + C2SRemoveModel.register(ServerSidePacketRegistry.INSTANCE) + } + + fun clientInit() { + // Packets + S2CInsertModel.register(ClientSidePacketRegistry.INSTANCE) + S2CRemoveModel.register(ClientSidePacketRegistry.INSTANCE) + + // Keybinding + CameraKeybind = FabricKeyBinding.Builder.create( + Identifier(NAMESPACE, "camerakey"), + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_Y, + "Extra Player Models" + ).build() + + KeyBindingRegistry.INSTANCE.addCategory("Extra Player Models") + KeyBindingRegistry.INSTANCE.register(CameraKeybind) + } +} + +/* +ClientTickCallback.EVENT.register(e -> +{ + if(keyBinding.isPressed()) System.out.println("was pressed!"); +}); + */ \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/ExtraPlayerModelsModMenu.kt b/src/main/kotlin/spazzylemons/extraplayermodels/ExtraPlayerModelsModMenu.kt new file mode 100644 index 0000000..959e895 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/ExtraPlayerModelsModMenu.kt @@ -0,0 +1,18 @@ +package spazzylemons.extraplayermodels + +import io.github.prospector.modmenu.api.ConfigScreenFactory +import io.github.prospector.modmenu.api.ModMenuApi +import net.minecraft.client.MinecraftClient +import net.minecraft.client.gui.screen.Screen +import spazzylemons.extraplayermodels.client.gui.ConfigScreen + +@Suppress("unused") +class ExtraPlayerModelsModMenu : ModMenuApi { + override fun getModId(): String = ExtraPlayerModels.NAMESPACE + + override fun getModConfigScreenFactory(): ConfigScreenFactory<*> = ConfigScreenFactory { parent: Screen -> + val screen = ConfigScreen(parent) + MinecraftClient.getInstance().openScreen(screen) + screen + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/MixinInterfaces.kt b/src/main/kotlin/spazzylemons/extraplayermodels/MixinInterfaces.kt new file mode 100644 index 0000000..a53a431 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/MixinInterfaces.kt @@ -0,0 +1,13 @@ +package spazzylemons.extraplayermodels + +import it.unimi.dsi.fastutil.objects.ObjectList +import net.minecraft.client.model.ModelPart +import spazzylemons.extraplayermodels.server.ServerData + +interface IMinecraftServerMixin { + val playerModelData: ServerData +} + +interface IModelPartMixin { + fun getCuboids(): ObjectList +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/client/ClientData.kt b/src/main/kotlin/spazzylemons/extraplayermodels/client/ClientData.kt new file mode 100644 index 0000000..eda1508 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/client/ClientData.kt @@ -0,0 +1,126 @@ +package spazzylemons.extraplayermodels.client + +import com.google.gson.JsonParseException +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.MinecraftClient +import net.minecraft.client.texture.NativeImageBackedTexture +import net.minecraft.util.Identifier +import spazzylemons.extraplayermodels.ExtraPlayerModels +import spazzylemons.extraplayermodels.IModelPartMixin +import spazzylemons.extraplayermodels.model.CustomPlayerModel +import spazzylemons.extraplayermodels.model.ModelLoader +import spazzylemons.extraplayermodels.packet.C2SInsertModel +import spazzylemons.extraplayermodels.packet.C2SRemoveModel +import spazzylemons.extraplayermodels.packet.newPacketByteBuf +import spazzylemons.extraplayermodels.render.CustomPlayerEntityRenderer +import java.util.* + +/** + * Contains a collection of player models as well as the current player model. + * Has methods for adding, removing, and updating models. + */ +@Environment(EnvType.CLIENT) +object ClientData { + /** + * The player models known by the client, stored as player model renderers for efficiency. + */ + val playerModels = mutableMapOf() + + /** + * The current player model being used by the client. + */ + var currentModel: CustomPlayerModel? = null + + /** + * The current renderer being used by the client. + */ + var currentRenderer: CustomPlayerEntityRenderer? = null + + /** + * A map of user ids to texture identifiers + */ + val textureIdentifiers = mutableMapOf() + + /** + * The current identifier for the local texture. + */ + var localTextureIdentifier: Identifier? = null + + /** + * Add or insert a model. + * + * @param uuid The UUID of the owner of the model. + * @param model The model. + */ + fun insert(uuid: UUID, model: CustomPlayerModel) { + textureIdentifiers[uuid] = MinecraftClient.getInstance().textureManager.registerDynamicTexture( + "epmcustomtexture.$uuid", + NativeImageBackedTexture(model.texture.toNativeImage()) + ) + playerModels[uuid] = CustomPlayerEntityRenderer(MinecraftClient.getInstance().entityRenderManager, model) + } + + /** + * Remove a model. + * + * @param uuid The UUID of the owner of the model. + */ + fun remove(uuid: UUID) { + textureIdentifiers.remove(uuid) + playerModels.remove(uuid) + } + + /** + * Reload the player model from the config file. + * + * @return If the model was successfully loaded. + */ + fun reload(): Boolean { + try { + // Try to load from the config file + val model = ModelLoader.load().model + if (model != null) { + localTextureIdentifier = MinecraftClient.getInstance().textureManager.registerDynamicTexture( + "epmcustomtexture.local", + NativeImageBackedTexture(model.texture.toNativeImage()) + ) + currentRenderer = CustomPlayerEntityRenderer(MinecraftClient.getInstance().entityRenderManager, model) + } else { + currentRenderer = null + } + currentModel = model + } catch (e: JsonParseException) { + // Notify the user why the model failed + ExtraPlayerModels.logError("text.extraplayermodels.loadmodel.explanation", e.message) + e.printStackTrace() + + // Fail + return false + } + + // Success! Tell the server (if we're connected) + if (MinecraftClient.getInstance().networkHandler != null) { + notifyServer() + } + return true + } + + /** + * Notify the server that the client's model has changed. + */ + fun notifyServer() { + when (val model = currentModel) { + null -> { + // When model is null, remove the model in the server + C2SRemoveModel.sendToServer() + } + else -> { + // When model is not null, add the model in the server + val buf = newPacketByteBuf() + model.writeToBuf(buf) + C2SInsertModel.sendToServer(buf) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/client/Configuration.kt b/src/main/kotlin/spazzylemons/extraplayermodels/client/Configuration.kt new file mode 100644 index 0000000..f8d45ac --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/client/Configuration.kt @@ -0,0 +1,10 @@ +package spazzylemons.extraplayermodels.client + +import spazzylemons.extraplayermodels.model.CustomPlayerModel + +/** + * Configuration structure class for Extra Player Models. + * + * @property model The model to use, can be null if the player is using the vanilla model. + */ +class Configuration(val model: CustomPlayerModel?) \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/client/gui/ConfigScreen.kt b/src/main/kotlin/spazzylemons/extraplayermodels/client/gui/ConfigScreen.kt new file mode 100644 index 0000000..55acfbb --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/client/gui/ConfigScreen.kt @@ -0,0 +1,66 @@ +package spazzylemons.extraplayermodels.client.gui + +import net.minecraft.client.gui.screen.Screen +import net.minecraft.client.gui.widget.ButtonWidget +import net.minecraft.client.gui.widget.ButtonWidget.PressAction +import net.minecraft.client.resource.language.I18n +import net.minecraft.text.TranslatableText +import net.minecraft.util.Util +import spazzylemons.extraplayermodels.client.ClientData +import java.io.File + +/** + * Represents a screen for configuring the player model. + * + * @property parent The parent screen + */ +class ConfigScreen(private val parent: Screen) : Screen(TranslatableText("text.extraplayermodels.config.title")) { + companion object { + /** + * Message that the config screen isn't finished + */ + private val unfinishedText = TranslatableText("text.extraplayermodels.config.unfinished") + } + + override fun init() { + // Add a button to open the config folder + addButton(ButtonWidget( + width / 2 - 100, height / 4 - 12, 200, 20, I18n.translate("text.extraplayermodels.config.view"), + PressAction { + val dir = minecraft?.runDirectory + if (dir != null) { + Util.getOperatingSystem().open(File(dir, "config/extraplayermodels")) + } + } + )) + + // Add a button to reload the configuration + addButton(ButtonWidget( + width / 2 - 100, height / 4 + 12, 200, 20, I18n.translate("text.extraplayermodels.config.reload"), + PressAction { + ClientData.reload() + } + )) + + // Add a button to leave this screen + addButton(ButtonWidget( + width / 2 - 100, height - 48, 200, 20, I18n.translate("gui.done"), + PressAction { + minecraft?.openScreen(parent) + } + )) + } + + override fun render(mouseX: Int, mouseY: Int, delta: Float) { + // Render background + renderBackground() + // Draw title + drawCenteredString(font, title.asFormattedString(), width / 2, 15, 16777215) + // Draw "this is unfinished" text + drawCenteredString(font, unfinishedText.asFormattedString(), width / 2, 30, 16777215) + // Render the player as a preview + renderPlayerOnScreen(width / 2, height - 108, 60) + // Super call + super.render(mouseX, mouseY, delta) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/client/gui/PreviewPlayerFunctions.kt b/src/main/kotlin/spazzylemons/extraplayermodels/client/gui/PreviewPlayerFunctions.kt new file mode 100644 index 0000000..4d52cba --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/client/gui/PreviewPlayerFunctions.kt @@ -0,0 +1,53 @@ +package spazzylemons.extraplayermodels.client.gui + +import com.mojang.blaze3d.systems.RenderSystem +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.Camera +import net.minecraft.client.util.math.MatrixStack +import net.minecraft.client.util.math.Vector3f +import spazzylemons.extraplayermodels.render.PreviewPlayerEntity +import kotlin.math.atan + +private val entity = PreviewPlayerEntity() + +/** + * Render the dummy entity on the screen. + */ +fun renderPlayerOnScreen(x: Int, y: Int, size: Int) { + val mouse = MinecraftClient.getInstance().mouse + val scaleFactor = MinecraftClient.getInstance().window.scaleFactor + val f = atan((x - mouse.x / scaleFactor) / 40.0).toFloat() + val g = atan((y - (size * 1.5) - mouse.y / scaleFactor) / 40.0).toFloat() + RenderSystem.pushMatrix() + RenderSystem.translatef(x.toFloat(), y.toFloat(), 1050F) + RenderSystem.scalef(1F, 1F, -1F) + val matrixStack = MatrixStack() + matrixStack.translate(0.0, 0.0, 1000.0) + matrixStack.scale(size.toFloat(), size.toFloat(), size.toFloat()) + val quaternion = Vector3f.POSITIVE_Z.getDegreesQuaternion(180F) + val quaternion2 = Vector3f.POSITIVE_X.getDegreesQuaternion(g * 20) + quaternion.hamiltonProduct(quaternion2) + matrixStack.multiply(quaternion) + entity.bodyYaw = 180F + f * 20F + entity.yaw = 180F + f * 40F + entity.pitch = -g * 20F + entity.headYaw = entity.yaw + entity.prevHeadYaw = entity.yaw + val entityRenderDispatcher = MinecraftClient.getInstance().entityRenderManager + entityRenderDispatcher.camera = Camera() + quaternion2.conjugate() + entityRenderDispatcher.rotation = quaternion2 + entityRenderDispatcher.setRenderShadows(false) + val immediate = MinecraftClient.getInstance().bufferBuilders.entityVertexConsumers + entityRenderDispatcher.render(entity, 0.0, 0.0, 0.0, 0F, 1F, matrixStack, immediate, 15728880) + immediate.draw() + entityRenderDispatcher.setRenderShadows(true) + RenderSystem.popMatrix() +} + +/** + * Tick the dummy entity, for arm animation + */ +fun tickDummyEntity() { + ++entity.age +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/CustomModelBox.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/CustomModelBox.kt new file mode 100644 index 0000000..1556424 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/CustomModelBox.kt @@ -0,0 +1,49 @@ +package spazzylemons.extraplayermodels.model + +import net.minecraft.client.model.ModelPart +import net.minecraft.client.util.math.Vector3f +import net.minecraft.util.PacketByteBuf + +/** + * Represents a cuboid part of a [CustomModelPart]. + * + * @property start The starting coordinate of the box, relative to something??? (figure this out) + * @property size The size of the box. + * @property extra The outer padding of the box. + * @property uv The location of the box's texture. + */ +class CustomModelBox( + private val start: Vector3f, + private val size: Vector3f, + private val extra: Vector3f, + private val uv: UVCoordinate +) { + /** + * Adds this box to a [ModelPart]. + * + * @param part The part to add this box to. + */ + fun addToPart(part: ModelPart) { + part.setTextureOffset(uv.u, uv.v) + part.addCuboid(start.x, start.y, start.z, size.x, size.y, size.z, extra.x, extra.y, extra.z) + } + + /** + * Writes the data in this box to a [PacketByteBuf]. + * + * @param buf The buffer to write to. + */ + fun writeToBuf(buf: PacketByteBuf) { + buf.writeFloat(start.x) + buf.writeFloat(start.y) + buf.writeFloat(start.z) + buf.writeFloat(size.x) + buf.writeFloat(size.y) + buf.writeFloat(size.z) + buf.writeFloat(extra.x) + buf.writeFloat(extra.y) + buf.writeFloat(extra.z) + buf.writeVarInt(uv.u) + buf.writeVarInt(uv.v) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/CustomModelPart.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/CustomModelPart.kt new file mode 100644 index 0000000..e18b93d --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/CustomModelPart.kt @@ -0,0 +1,73 @@ +package spazzylemons.extraplayermodels.model + +import net.minecraft.client.model.Model +import net.minecraft.client.model.ModelPart +import net.minecraft.client.util.math.Vector3f +import net.minecraft.util.PacketByteBuf +import net.minecraft.util.math.MathHelper + +/** + * Represents a part of a custom model. + * + * @property pivot Where the part rotates. + * @property rotation The part's rotation. + * @property boxes A list of [CustomModelBox]es for this part. + * @property children A list of [CustomModelPart]s that inherit some properties of this part. + */ +class CustomModelPart( + private val pivot: Vector3f, + private val rotation: Vector3f, + private val boxes: List, + private val children: List +) { + /** + * Convert this part to a [ModelPart]. + * + * @param model The [Model] to connect this part to. + * @return The [ModelPart] generated from this part. + */ + fun asModelPart(model: Model): ModelPart { + // Make a ModelPart. + val part = ModelPart(model) + // Set its pivot. + part.setPivot(pivot.x, pivot.y, pivot.z) + // Add the boxes. + for (box in boxes) { + box.addToPart(part) + } + // Use 0.017453292 to convert degrees to radians. + part.rotate( + MathHelper.wrapDegrees(rotation.x) * 0.017453292F, + MathHelper.wrapDegrees(rotation.y) * 0.017453292F, + MathHelper.wrapDegrees(rotation.z) * 0.01745329F + ) + // Add children. + for (child in children) { + part.addChild(child.asModelPart(model)) + } + // Return. + return part + } + + /** + * Writes the data in this part to a [PacketByteBuf]. + * + * @param buf The buffer to write to. + */ + fun writeToBuf(buf: PacketByteBuf) { + buf.writeFloat(pivot.x) + buf.writeFloat(pivot.y) + buf.writeFloat(pivot.z) + buf.writeFloat(rotation.x) + buf.writeFloat(rotation.y) + buf.writeFloat(rotation.z) + buf.writeVarInt(boxes.size) + for (box in boxes) { + box.writeToBuf(buf) + } + buf.writeVarInt(children.size) + for (child in children) { + child.writeToBuf(buf) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/CustomPlayerModel.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/CustomPlayerModel.kt new file mode 100644 index 0000000..c12f71a --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/CustomPlayerModel.kt @@ -0,0 +1,33 @@ +package spazzylemons.extraplayermodels.model + +import net.minecraft.util.PacketByteBuf + +/** + * Represents a custom player model with six [CustomModelPart]s: + * the [head], [torso], [leftArm], [rightArm], [leftLeg], and [rightLeg], + * as well as a custom [texture]. + */ +class CustomPlayerModel( + val head: CustomModelPart, + val torso: CustomModelPart, + val leftArm: CustomModelPart, + val rightArm: CustomModelPart, + val leftLeg: CustomModelPart, + val rightLeg: CustomModelPart, + val texture: ImageArray +) { + /** + * Writes the data in this model to a [PacketByteBuf]. + * + * @param buf The buffer to write to. + */ + fun writeToBuf(buf: PacketByteBuf) { + head.writeToBuf(buf) + torso.writeToBuf(buf) + leftArm.writeToBuf(buf) + rightArm.writeToBuf(buf) + leftLeg.writeToBuf(buf) + rightLeg.writeToBuf(buf) + buf.writeIntArray(texture.ints) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/ImageArray.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/ImageArray.kt new file mode 100644 index 0000000..0fe5d24 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/ImageArray.kt @@ -0,0 +1,20 @@ +package spazzylemons.extraplayermodels.model + +import net.minecraft.client.texture.NativeImage + +/** + * A simple class that wraps an IntArray + */ +class ImageArray(val ints: IntArray) { + fun toNativeImage(): NativeImage { + val nativeImage = NativeImage(64, 64, true) + var i = 0 + for (y in 0..63) { + for (x in 0..63) { + val idk = ints[i++] + nativeImage.setPixelRgba(x, y, idk) + } + } + return nativeImage + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/ModelBuilder.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/ModelBuilder.kt new file mode 100644 index 0000000..852eab5 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/ModelBuilder.kt @@ -0,0 +1,54 @@ +package spazzylemons.extraplayermodels.model + +import net.minecraft.client.util.math.Vector3f +import net.minecraft.util.PacketByteBuf + +/** + * Contains methods for building models from [PacketByteBuf]s. + */ +object ModelBuilder { + /** + * Create a [CustomModelBox] from a [PacketByteBuf]. + * + * @param buf The PacketByteBuf to read from. + */ + private fun boxFromBuf(buf: PacketByteBuf): CustomModelBox = CustomModelBox( + Vector3f(buf.readFloat(), buf.readFloat(), buf.readFloat()), + Vector3f(buf.readFloat(), buf.readFloat(), buf.readFloat()), + Vector3f(buf.readFloat(), buf.readFloat(), buf.readFloat()), + UVCoordinate(buf.readVarInt(), buf.readVarInt()) + ) + + /** + * Create a [CustomModelPart] from a [PacketByteBuf]. + * + * @param buf The PacketByteBuf to read from. + */ + private fun partFromBuf(buf: PacketByteBuf): CustomModelPart = CustomModelPart( + Vector3f(buf.readFloat(), buf.readFloat(), buf.readFloat()), + Vector3f(buf.readFloat(), buf.readFloat(), buf.readFloat()), + (0 until buf.readVarInt()).map { + boxFromBuf(buf) + }, + (0 until buf.readVarInt()).map { + partFromBuf(buf) + } + ) + + /** + * Create a [CustomPlayerModel] from a [PacketByteBuf]. + * + * @param buf The PacketByteBuf to read from. + */ + fun modelFromBuf(buf: PacketByteBuf): CustomPlayerModel { + return CustomPlayerModel( + partFromBuf(buf), + partFromBuf(buf), + partFromBuf(buf), + partFromBuf(buf), + partFromBuf(buf), + partFromBuf(buf), + ImageArray(buf.readIntArray()) + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/ModelLoader.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/ModelLoader.kt new file mode 100644 index 0000000..0f0d9c7 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/ModelLoader.kt @@ -0,0 +1,311 @@ +package spazzylemons.extraplayermodels.model + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import net.minecraft.client.MinecraftClient +import net.minecraft.client.util.math.Vector3f +import spazzylemons.extraplayermodels.client.Configuration +import spazzylemons.extraplayermodels.model.json.* +import java.io.File + +object ModelLoader { + /** + * The default configuration. + */ + private const val DEFAULT_MODEL = +"""{ + "__comment": [ + "An explanation of this model file:", + + "Each of the six limbs here is a model part. A model part is part of an entity model.", + "Rotation is in degrees, and coordinates are 16 times that of Minecraft coordinates.", + + "A model part has these properties:", + "pivot ------- The point on the part where it is rotated.", + "rotation ---- The rotation of the part.", + "boxes ------- The boxes for the part.", + "children ---- Parts which inherit some properties of this part.", + + "A box has these properties:", + "start ------- Where the lower corner of the box is.", + "size -------- The size of the box.", + "extra ------- The amount to stretch the box.", + "uv ---------- Where the texture is." + ], + "head": { + "pivot": [0, 0, 0], + "rotation": [0, 0, 0], + "boxes": [ + { + "start": [-4, -8, -4], + "size": [8, 8, 8], + "extra": [0, 0, 0], + "uv": [0, 0] + }, + { + "start": [-4, -8, -4], + "size": [8, 8, 8], + "extra": [0.5, 0.5, 0.5], + "uv": [32, 0] + } + ], + "children": [] + }, + "torso": { + "pivot": [0, 0, 0], + "rotation": [0, 0, 0], + "boxes": [ + { + "start": [-4, 0, -2], + "size": [8, 12, 4], + "extra": [0, 0, 0], + "uv": [16, 16] + }, + { + "start": [-4, 0, -2], + "size": [8, 12, 4], + "extra": [0.25, 0.25, 0.25], + "uv": [16, 32] + } + ], + "children": [] + }, + "leftArm": { + "pivot": [0, 0, 0], + "rotation": [0, 0, 0], + "boxes": [ + { + "start": [-1, -2, -2], + "size": [4, 12, 4], + "extra": [0, 0, 0], + "uv": [32, 48] + }, + { + "start": [-1, -2, -2], + "size": [4, 12, 4], + "extra": [0.25, 0.25, 0.25], + "uv": [48, 48] + } + ], + "children": [] + }, + "rightArm": { + "pivot": [0, 0, 0], + "rotation": [0, 0, 0], + "boxes": [ + { + "start": [-3, -2, -2], + "size": [4, 12, 4], + "extra": [0, 0, 0], + "uv": [40, 16] + }, + { + "start": [-3, -2, -2], + "size": [4, 12, 4], + "extra": [0.25, 0.25, 0.25], + "uv": [40, 32] + } + ], + "children": [] + }, + "leftLeg": { + "pivot": [0, 0, 0], + "rotation": [0, 0, 0], + "boxes": [ + { + "start": [-4, 0, -2], + "size": [4, 12, 4], + "extra": [0, 0, 0], + "uv": [0, 16] + }, + { + "start": [-4, 0, -2], + "size": [4, 12, 4], + "extra": [0.25, 0.25, 0.25], + "uv": [0, 32] + } + ], + "children": [] + }, + "rightLeg": { + "pivot": [0, 0, 0], + "rotation": [0, 0, 0], + "boxes": [ + { + "start": [0, 0, -2], + "size": [4, 12, 4], + "extra": [0, 0, 0], + "uv": [16, 48] + }, + { + "start": [0, 0, -2], + "size": [4, 12, 4], + "extra": [0.25, 0.25, 0.25], + "uv": [0, 48] + } + ], + "children": [] + }, + "texture": "default.png" +}""" + + /** + * The default skin, as the bytes of the PNG. Thinking about ways to make this neater. + */ + private val DEFAULT_TEXTURE = byteArrayOf( + 0x89.toByte(),0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a,0x00,0x00,0x00,0x0d,0x49,0x48,0x44,0x52, + 0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x40,0x08,0x06,0x00,0x00,0x00,0xaa.toByte(),0x69,0x71, + 0xde.toByte(),0x00,0x00,0x05,0x0d,0x49,0x44,0x41,0x54,0x78,0xda.toByte(),0xed.toByte(),0x9a.toByte(),0xdb.toByte(),0x4b,0x14, + 0x51,0x1c,0xc7.toByte(),0xf7.toByte(),0xb1.toByte(),0xb4.toByte(),0x2c,0xa3.toByte(),0x28,0xb4.toByte(),0xfb.toByte(),0x96.toByte(),0x6d,0x5a,0x6c,0x96.toByte(), + 0x3d,0x68,0x94.toByte(),0x95.toByte(),0xdd.toByte(),0x8c.toByte(),0x28,0x32,0x8a.toByte(),0xca.toByte(),0x30,0xb1.toByte(),0x28,0xf3.toByte(),0x82.toByte(),0xb0.toByte(), + 0x74,0x81.toByte(),0x22,0xb2.toByte(),0x0b,0x95.toByte(),0x05,0xd5.toByte(),0x93.toByte(),0x50,0x58,0x41,0x10,0x48,0x14,0x3d, + 0x74,0x7b,0x08,0x7b,0xec.toByte(),0xc9.toByte(),0xff.toByte(),0xe9.toByte(),0xd7.toByte(),0x7c,0xcf.toByte(),0xec.toByte(),0x6f,0xfa.toByte(),0xcd.toByte(),0xe1.toByte(), + 0x9c.toByte(),0x19,0xf7.toByte(),0xa2.toByte(),0xce.toByte(),0xae.toByte(),0x73,0xe0.toByte(),0xcb.toByte(),0x1c,0xcf.toByte(),0xfc.toByte(),0xce.toByte(),0xd9.toByte(),0xf9.toByte(),0x7e, + 0xce.toByte(),0x65,0xcf.toByte(),0x7a,0x26,0x91.toByte(),0x08,0x49,0xf5.toByte(),0xab.toByte(),0x16,0x12,0xb4.toByte(),0x61,0xc5.toByte(),0x3c, + 0x92.toByte(),0x79,0x68,0xcd.toByte(),0xe2.toByte(),0x4a,0xba.toByte(),0x71,0x38,0x1d,0xa8.toByte(),0x44,0xa9.toByte(),0x27,0x36,0x9c.toByte(), + 0x5e,0xb3.toByte(),0x48,0x49,0x02,0x80.toByte(),0x60,0xb2.toByte(),0x6f,0xef.toByte(),0x36,0x7a,0x7c,0xaa.toByte(),0xc9.toByte(),0xbb.toByte(), + 0x42,0x65,0x05,0x80.toByte(),0xcd.toByte(),0xd7.toByte(),0xd5.toByte(),0x54,0x58,0x01,0x24,0x93.toByte(),0x49,0x9f.toByte(),0x50,0x56, + 0x16,0x00,0xb8.toByte(),0xf7.toByte(),0xf5.toByte(),0x69,0xc0.toByte(),0x65,0x65,0x0f,0x40,0x9a.toByte(),0xc5.toByte(),0x08,0x80.toByte(),0xe4.toByte(), + 0x74,0x98.toByte(),0x13,0x00,0x4c,0xe6.toByte(),0xc3.toByte(),0x00,0x74,0xa4.toByte(),0xea.toByte(),0xca.toByte(),0x07,0x00,0x1b,0xdf.toByte(), + 0xbc.toByte(),0x72,0x81.toByte(),0xca.toByte(),0x63,0xf5.toByte(),0xd7.toByte(),0x01,0x98.toByte(),0x54,0x36,0x00,0xd8.toByte(),0xb8.toByte(),0x14,0x8f.toByte(), + 0x8c.toByte(),0x92.toByte(),0xff.toByte(),0x1a,0x4c,0xd5.toByte(),0x56,0x10,0x04,0x53,0x9b.toByte(),0x6a,0x2b,0x95.toByte(),0xd9.toByte(),0xed.toByte(), + 0xc9.toByte(),0xc5.toByte(),0xb4.toByte(),0x75,0xad.toByte(),0xab.toByte(),0xf4.toByte(),0xda.toByte(),0x85.toByte(),0xd4.toByte(),0xb0.toByte(),0xda.toByte(),0x5d,0x07,0x52,0x2b, + 0x2b,0xd5.toByte(),0x42,0x88.toByte(),0x58,0x94.toByte(),0x01,0x00,0xc7.toByte(),0xa1.toByte(),0x0e,0xea.toByte(),0xa2.toByte(),0x0d,0xdc.toByte(),0xe7.toByte(), + 0x76,0xc3.toByte(),0xf6.toByte(),0x11,0x91.toByte(),0x02,0x00,0x6d,0x59,0x5d,0xa5.toByte(),0x8c.toByte(),0xc0.toByte(),0xe0.toByte(),0x8e.toByte(),0x0d, + 0xd5.toByte(),0xea.toByte(),0xca.toByte(),0xc2.toByte(),0xdf.toByte(),0xfb.toByte(),0xeb.toByte(),0x97.toByte(),0x50,0x6b,0xfd.toByte(),0x32,0x15,0x07,0x20,0x1c, + 0x83.toByte(),0x3a,0x28,0xe3.toByte(),0x76,0x24,0x80.toByte(),0xa0.toByte(),0x7d,0xc4.toByte(),0xac.toByte(),0x03,0x50,0x0f,0xbd.toByte(),0xc2.toByte(), + 0xed.toByte(),0x35,0xd7.toByte(),0xb8.toByte(),0x63,0xac.toByte(),0x16,0x3d,0x5d,0xe1.toByte(),0x81.toByte(),0xf8.toByte(),0x39,0x32,0x40,0x13, + 0x2f,0x32,0xf4.toByte(),0x77,0x74,0x98.toByte(),0x26,0x9e.toByte(),0x67,0x9c.toByte(),0xfc.toByte(),0x75,0xfa.toByte(),0x7c,0xf3.toByte(),0x3c, + 0x35,0x6d,0x5c,0xa2.toByte(),0x62,0x10,0x8b.toByte(),0x3a,0x0d,0x59,0x78,0x0a,0x82.toByte(),0xd3.toByte(),0x26,0xda.toByte(), + 0x0e,0xdb.toByte(),0x47,0x44,0x63,0x04,0xd4.toByte(),0x38,0x43,0x79,0x55,0x95.toByte(),0xdb.toByte(),0xab.toByte(),0xb5.toByte(),0xee.toByte(), + 0x54,0x40,0x3e,0xbd.toByte(),0xbe.toByte(),0x9a.toByte(),0x3e,0xdd.toByte(),0xbd.toByte(),0x42,0xdf.toByte(),0x1f,0x0d,0xaa.toByte(),0xeb.toByte(),0x97.toByte(), + 0xe1.toByte(),0x41,0x1a,0xcb.toByte(),0x5c,0xa0.toByte(),0xdf.toByte(),0x23,0x19,0x1a,0xbb.toByte(),0x76,0x4c,0x09,0x31,0x88.toByte(), + 0x45,0x1d,0xd4.toByte(),0x45,0x1e,0x6d,0xa1.toByte(),0x4d,0xfc.toByte(),0x1d,0xb6.toByte(),0x8f.toByte(),0x88.toByte(),0xcc.toByte(),0x14,0x70, + 0xa7.toByte(),0xc1.toByte(),0x02,0x4a,0x2e,0x9f.toByte(),0x4f,0x4f,0xce.toByte(),0xb6.toByte(),0xd0.toByte(),0xd8.toByte(),0xa5.toByte(),0x83.toByte(),0xd4.toByte(),0x92.toByte(), + 0x5a,0xaa.toByte(),0xcc.toByte(),0x23,0x75,0xec.toByte(),0x1e,0xa1.toByte(),0x1f,0x77,0xae.toByte(),0x2a,0xf3.toByte(),0xc8.toByte(),0x23,0x01, + 0x0a,0x62,0x10,0x8b.toByte(),0x3a,0xa8.toByte(),0x8b.toByte(),0x36,0x64,0x9b.toByte(),0x61,0xfb.toByte(),0x88.toByte(),0xe8.toByte(),0x00,0x70, + 0x7a,0xcc.toByte(),0x1d,0xb6.toByte(),0xd5.toByte(),0xf4.toByte(),0xa8.toByte(),0xa3.toByte(),0x91.toByte(),0x46,0x2f,0xee.toByte(),0xa3.toByte(),0xa7.toByte(),0xe7.toByte(),0x9b.toByte(), + 0x1d,0x73,0x87.toByte(),0xe8.toByte(),0xdb.toByte(),0x9d.toByte(),0x1e,0xfa.toByte(),0xf5.toByte(),0x20,0xe3.toByte(),0x7d,0xbd.toByte(),0xfd.toByte(),0x79,0x3a, + 0x40,0x1f,0xfa.toByte(),0x0e,0xd3.toByte(),0xb0.toByte(),0xb3.toByte(),0xe7.toByte(),0x47,0x0c,0x62,0x51,0x07,0x75,0xd1.toByte(),0x06, + 0xf7.toByte(),0xbe.toByte(),0x5c,0x04,0x6d,0xfb.toByte(),0x88.toByte(),0x48,0xac.toByte(),0x01,0x78,0x50,0x0c,0xdb.toByte(),0x9e.toByte(),0xb6.toByte(), + 0x34,0xbd.toByte(),0xed.toByte(),0x3b,0x41,0x6f,0x2e,0x1d,0xf0.toByte(),0x7e,0xd0.toByte(),0xbc.toByte(),0xea.toByte(),0x6a,0x73,0x0c, + 0xee.toByte(),0xa5.toByte(),0x5b,0xfb.toByte(),0x77,0xfa.toByte(),0xf4.toByte(),0xb2.toByte(),0x6b,0x0f,0xdd.toByte(),0x3f,0xd5.toByte(),0xac.toByte(),0x62,0x10, + 0x8b.toByte(),0x3a,0xa8.toByte(),0x8b.toByte(),0x36,0xd4.toByte(),0x14,0xc8.toByte(),0x4e,0x87.toByte(),0xb0.toByte(),0x7d,0xc4.toByte(),0xac.toByte(),0x03,0xe0.toByte(), + 0x5f,0x6f,0x6c,0x7a,0xec.toByte(),0xea.toByte(),0x11,0xd5.toByte(),0xa3.toByte(),0xcf.toByte(),0xce.toByte(),0xed.toByte(),0xa2.toByte(),0xeb.toByte(),0x47,0xd2.toByte(), + 0x34,0x74,0x70,0x0b,0x0d,0xb6.toByte(),0xd5.toByte(),0x2b,0xb3.toByte(),0xd0.toByte(),0xed.toByte(),0xe3.toByte(),0x3b,0xe8.toByte(),0xe1.toByte(),0xe9.toByte(), + 0x5d,0x4a,0xc8.toByte(),0x23,0x06,0xb1.toByte(),0xef.toByte(),0x7b,0xdb.toByte(),0x55,0xdd.toByte(),0x37,0x97.toByte(),0x8f.toByte(),0xba.toByte(),0xd7.toByte(), + 0x6c,0x7b,0x0c,0x52,0xdf.toByte(),0x47,0x30,0xb8.toByte(),0xc8.toByte(),0xed.toByte(),0x0b,0xce.toByte(),0xb4.toByte(),0x4f,0x92.toByte(),0x54, + 0x2a,0x95.toByte(),0xf2.toByte(),0x29,0xb4.toByte(),0x81.toByte(),0xc9.toByte(),0x49,0xfb.toByte(),0xa6.toByte(),0xc8.toByte(),0xb9.toByte(),0x97.toByte(),0xe8.toByte(),0xee.toByte(),0x0e, + 0x56,0x58,0x1a,0x1f,0x77,0xdb.toByte(),0x81.toByte(),0x9c.toByte(),0x3c,0xa6.toByte(),0xe4.toByte(),0xc9.toByte(),0xc6.toByte(),0x8d.toByte(),0xde.toByte(),0x35, + 0x12,0x00,0x6c,0x5b,0xe3.toByte(),0xa2.toByte(),0x00,0x60,0xf3.toByte(),0x59,0xcd.toByte(),0x6d,0x00,0xce.toByte(),0x08,0x90.toByte(), + 0xe6.toByte(),0xd5.toByte(),0x67,0xcc.toByte(),0xb5.toByte(),0x29,0x50,0x30,0x80.toByte(),0x6d,0xa9.toByte(),0x0c,0x41,0x75,0xeb.toByte(),0x3a, + 0xd5.toByte(),0xf5.toByte(),0x50,0xcb.toByte(),0x47,0x9f.toByte(),0xe4.toByte(),0x3d,0xfd.toByte(),0x7e,0x4b,0xe3.toByte(),0x08,0x25,0xee.toByte(),0xdd.toByte(), + 0xfb.toByte(),0xaf.toByte(),0xce.toByte(),0x4e,0x4a,0xbc.toByte(),0x7e,0x4d,0x89.toByte(),0x77,0xef.toByte(),0xd4.toByte(),0x82.toByte(),0x89.toByte(),0xaf.toByte(),0x4a, + 0x5e,0x3c,0x51,0xa6.toByte(),0x24,0xe3.toByte(),0x21,0x94.toByte(),0xc1.toByte(),0x14,0xeb.toByte(),0xeb.toByte(),0x57,0x57,0xfc.toByte(),0xb7.toByte(), + 0x2d,0x9e.toByte(),0x63,0xf4.toByte(),0xfb.toByte(),0x5c,0x9f.toByte(),0x35,0x15,0x00,0x30,0x98.toByte(),0x0f,0x00,0x48,0x7d, + 0x68,0x6f,0xaf.toByte(),0x2b,0xe4.toByte(),0xf9.toByte(),0xc1.toByte(),0xb3.toByte(),0x10,0x3c,0xf3.toByte(),0xfc.toByte(),0x40,0x32,0x1e,0x92.toByte(), + 0xa6.toByte(),0xa5.toByte(),0xb8.toByte(),0x1d,0x19,0xcb.toByte(),0x06,0xb9.toByte(),0x0e,0x03,0xd5.toByte(),0xef.toByte(),0xe7.toByte(),0x03,0xc0.toByte(),0x66, + 0x30,0x14,0x80.toByte(),0xe9.toByte(),0x01,0xf9.toByte(),0x21,0xf1.toByte(),0x80.toByte(),0x18,0x11,0xd2.toByte(),0xa4.toByte(),0x8c.toByte(),0x87.toByte(),0x4c, + 0xe6.toByte(),0x65,0x1b,0x26,0xc0.toByte(),0x32,0x66,0xd6.toByte(),0x01,0x60,0xd8.toByte(),0xf3.toByte(),0x03,0x20,0x2f,0x3f, + 0x9c.toByte(),0x01,0xc8.toByte(),0x32,0x5b,0x3c,0xc7.toByte(),0xc1.toByte(),0xa0.toByte(),0xcc.toByte(),0xeb.toByte(),0x06,0xe5.toByte(),0x68,0x9a.toByte(),0xa9.toByte(), + 0x11,0x20,0x63,0x30,0xef.toByte(),0xa5.toByte(),0x3c,0x23,0x2c,0x3c,0xbc.toByte(),0x34,0xcd.toByte(),0x79,0x2e,0x97.toByte(), + 0xb1.toByte(),0x0c,0x40,0x37,0x2d,0xea.toByte(),0x63,0x53,0xc6.toByte(),0x7a,0xe2.toByte(),0x6c,0xbe.toByte(),0xf0.toByte(),0xfb.toByte(),0x43, + 0x0a,0x65,0x32,0x46,0xbf.toByte(),0x9f.toByte(),0x17,0x00,0x36,0x27,0x47,0x80.toByte(),0x15,0x80.toByte(),0x34,0x03, + 0x18,0x12,0x80.toByte(),0xc9.toByte(),0x98.toByte(),0x04,0xc6.toByte(),0x53,0xc0.toByte(),0x54,0x27,0x3b,0xd4.toByte(),0x4d,0x00,0x46, + 0x87.toByte(),0x3a,0x3d,0x83.toByte(),0x5c,0x5e,0x54,0x00,0x39,0xaf.toByte(),0x01,0x3a,0x00,0xd9.toByte(),0xfb.toByte(),0x72, + 0xc5.toByte(),0x96.toByte(),0x00,0xe4.toByte(),0x1a,0xa0.toByte(),0x99.toByte(),0x96.toByte(),0x40,0xa4.toByte(),0x39,0x1d,0x00,0xae.toByte(),0x26,0x00, + 0xaa.toByte(),0x3c,0x1b,0x33,0xe5.toByte(),0xaf.toByte(),0xc1.toByte(),0xbc.toByte(),0x01,0xe8.toByte(),0x73,0x30,0x0c,0x80.toByte(),0xfe.toByte(),0x2d, + 0x60,0x8a.toByte(),0xb5.toByte(),0x00,0xd0.toByte(),0x7b,0x58,0x07,0x60,0x9a.toByte(),0x22,0x45,0x01,0x10,0x74,0x3f, + 0x70,0x11,0xd2.toByte(),0xbf.toByte(),0x01,0x74,0x00,0xa6.toByte(),0x45,0x4b,0x93.toByte(),0x34,0x67,0x03,0x10,0xb4.toByte(), + 0x46,0x84.toByte(),0x02,0x68,0x6d,0x6d,0x25,0x28,0x0c,0x00,0xc7.toByte(),0xf5.toByte(),0xf7.toByte(),0xf7.toByte(),0xfb.toByte(),0xe4.toByte(), + 0x19,0xe2.toByte(),0x8d.toByte(),0x08,0xf7.toByte(),0x9e.toByte(),0xde.toByte(),0xfb.toByte(),0x36,0x00,0x32,0xde.toByte(),0xb0.toByte(),0x1e,0x48,0x73, + 0xa6.toByte(),0x39,0x2e,0x01,0xc8.toByte(),0x29,0xc0.toByte(),0xd7.toByte(),0x29,0x03,0xb0.toByte(),0x19,0x0c,0xbb.toByte(),0x2f,0xa7.toByte(), + 0x08,0xae.toByte(),0x3a,0x40,0x6f,0xc7.toByte(),0x28,0x00,0xe8.toByte(),0x1b,0x2f,0x6f,0x41,0xe5.toByte(),0x69,0xe2.toByte(), + 0x88.toByte(),0x17,0x61,0x1b,0x00,0x36,0x69,0x1a,0x21,0xb3.toByte(),0x06,0xc0.toByte(),0x34,0x82.toByte(),0x7c,0x10, + 0x1c,0x63,0xa1.toByte(),0x5b,0x6b,0x27,0x46,0x96.toByte(),0xe9.toByte(),0x3d,0xec.toByte(),0x5b,0x00,0x1d,0x99.toByte(),0xd6.toByte(), + 0x88.toByte(),0x9c.toByte(),0x00,0xc4.toByte(),0x29,0x4e,0x71,0x8a.toByte(),0x53,0x9c.toByte(),0xe2.toByte(),0x14,0xa7.toByte(),0x38,0xc5.toByte(),0x29, + 0x4e,0x71,0x8a.toByte(),0x53,0x9c.toByte(),0xe2.toByte(),0x14,0xa7.toByte(),0x38,0x15,0x9a.toByte(),0x0a,0x3e,0x5c,0x2d,0xf6.toByte(), + 0xe1.toByte(),0x67,0xc9.toByte(),0x01,0xd0.toByte(),0xce.toByte(),0xff.toByte(),0xe7.toByte(),0x1e,0x00,0x39,0x02,0xa6.toByte(),0xe3.toByte(),0x05,0x88.toByte(), + 0x52,0x9b.toByte(),0x02,0x91.toByte(),0x07,0x90.toByte(),0xeb.toByte(),0xfb.toByte(),0x05,0xfc.toByte(),0xdf.toByte(),0x5e,0xdf.toByte(),0xb9.toByte(),0x82.toByte(),0x94.toByte(), + 0xfc.toByte(),0x57,0xba.toByte(),0xe9.toByte(),0x7d,0x82.toByte(),0x5c,0x0f,0x3f,0xa3.toByte(),0x02,0xc0.toByte(),0x76,0xdf.toByte(),0x7a,0xfa.toByte(), + 0xcb.toByte(),0x20,0x6c,0x27,0x49,0xa5.toByte(),0x04,0x20,0xe8.toByte(),0x7e,0xe0.toByte(),0x49,0x91.toByte(),0xe9.toByte(),0x78,0xbc.toByte(), + 0xec.toByte(),0x01,0x14,0xfb.toByte(),0xfc.toByte(),0x7f,0x26,0x00,0x14,0xf2.toByte(),0x7e,0xc1.toByte(),0xb4.toByte(),0x9f.toByte(),0xff.toByte(),0xcf.toByte(), + 0x14,0x00,0xd3.toByte(),0x51,0x57,0x2e,0x00,0xc2.toByte(),0x8e.toByte(),0xbe.toByte(),0x22,0x0d,0xa0.toByte(),0x90.toByte(),0x29,0xa0.toByte(), + 0x03,0x90.toByte(),0xc7.toByte(),0x5a,0x3a,0x00,0xf9.toByte(),0x7e,0xc0.toByte(),0x94.toByte(),0xcf.toByte(),0xff.toByte(),0xa3.toByte(),0x02,0xc0.toByte(),0x36, + 0x45,0xf4.toByte(),0xc3.toByte(),0x4d,0xf9.toByte(),0xf6.toByte(),0x87.toByte(),0x7e,0xfa.toByte(),0x9b.toByte(),0xd7.toByte(),0xf1.toByte(),0xf7.toByte(),0x4c,0x01,0xc8.toByte(), + 0xf7.toByte(),0xfd.toByte(),0x02,0x7d,0x88.toByte(),0x9b.toByte(),0x00,0x14,0xf4.toByte(),0x02,0xc4.toByte(),0x74,0xa7.toByte(),0x42,0xdf.toByte(),0x2f, + 0x30,0x9d.toByte(),0xef.toByte(),0xdb.toByte(),0x00,0x98.toByte(),0xa6.toByte(),0x48,0x64,0x00,0xe4.toByte(),0x7b,0xbc.toByte(),0x6e,0xea.toByte(),0xe1.toByte(), + 0xa0.toByte(),0x97.toByte(),0xa0.toByte(),0xca.toByte(),0x12,0x80.toByte(),0x6f,0x21,0x14,0x0b,0x9c.toByte(),0xfe.toByte(),0x0a,0xcc.toByte(),0x74,0x00, + 0xf8.toByte(),0x07,0xf9.toByte(),0x07,0x97.toByte(),0xd7.toByte(),0x5a,0xec.toByte(),0xd7.toByte(),0xbd.toByte(),0x00,0x00,0x00,0x00,0x49,0x45, + 0x4e,0x44,0xae.toByte(),0x42,0x60,0x82.toByte() + ) + + /** + * The default config. + */ + private const val DEFAULT_CONFIG = +"""{ + "__comment": [ + "Model property is the model in models folder, or null if disabled" + ], + "model": "default.json" +}""" + + /** + * The [Gson] used to parse the configuration file. + */ + val gson: Gson = GsonBuilder() + .registerTypeAdapter(Vector3f::class.java, + VectorDeserializer() + ) + .registerTypeAdapter(UVCoordinate::class.java, + UVDeserializer() + ) + .registerTypeAdapter(ImageArray::class.java, + ImageArrayDeserializer() + ) + .registerTypeAdapter(CustomPlayerModel::class.java, + CustomPlayerModelDeserializer() + ) + .registerTypeAdapter(CustomModelPart::class.java, + CustomModelPartDeserializer() + ) + .registerTypeAdapter(CustomModelBox::class.java, + CustomModelBoxDeserializer() + ) + .registerTypeAdapter(Configuration::class.java, + ConfigurationDeserializer() + ) + .create() + + /** + * Load the player model from the configuration file, or create one if none exists. + * + * @return The [CustomPlayerModel] generated. + */ + fun load(): Configuration { + // Get our folder first. + val ourFolder = File(MinecraftClient.getInstance().runDirectory, "config/extraplayermodels") + // Make if it doesn't exist. + ourFolder.mkdirs() + // Also make sure that the models and textures folders exist. + File(ourFolder, "models").mkdirs() + File(ourFolder, "textures").mkdirs() + // Get the config file. + val configFile = File(ourFolder, "config.json") + if (!configFile.exists()) { + // If there's no config file, make it, and add defaults if not present. + configFile.writeText(DEFAULT_CONFIG, Charsets.UTF_8) + val defaultModel = File(ourFolder, "models/default.json") + if (!defaultModel.exists()) { + defaultModel.writeText(DEFAULT_MODEL, Charsets.UTF_8) + } + val defaultTexture = File(ourFolder, "textures/default.png") + if (!defaultTexture.exists()) { + defaultTexture.writeBytes(DEFAULT_TEXTURE) + } + } + + return gson.fromJson(configFile.reader(), Configuration::class.java) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/ModelRotate.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/ModelRotate.kt new file mode 100644 index 0000000..725b0c3 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/ModelRotate.kt @@ -0,0 +1,12 @@ +package spazzylemons.extraplayermodels.model + +import net.minecraft.client.model.ModelPart + +/** + * Rotate a [ModelPart] more directly. + */ +fun ModelPart.rotate(pitch: Float, yaw: Float, roll: Float) { + this.pitch = pitch + this.yaw = yaw + this.roll = roll +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/UVCoordinate.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/UVCoordinate.kt new file mode 100644 index 0000000..7cbfd4b --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/UVCoordinate.kt @@ -0,0 +1,6 @@ +package spazzylemons.extraplayermodels.model + +/** + * Represents a texture coordinate. + */ +class UVCoordinate(val u: Int, val v: Int) \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/json/ConfigurationDeserializer.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/ConfigurationDeserializer.kt new file mode 100644 index 0000000..7ddc3b6 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/ConfigurationDeserializer.kt @@ -0,0 +1,52 @@ +package spazzylemons.extraplayermodels.model.json + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import net.minecraft.client.MinecraftClient +import spazzylemons.extraplayermodels.client.Configuration +import spazzylemons.extraplayermodels.model.CustomPlayerModel +import spazzylemons.extraplayermodels.model.ModelLoader +import java.io.File +import java.lang.reflect.Type + +/** + * Deserialize a [Configuration]. + */ +class ConfigurationDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Configuration { + // Make sure configuration is an object + if (json?.isJsonObject != true) { + throw JsonParseException("Configuration must be an object") + } + // Convert it + val obj = json.asJsonObject + // Get the model property + val modelProperty = obj["model"] + return when { + // If null, return a configuration with no model + modelProperty.isJsonNull -> { + Configuration(null) + } + // If string, look for model file and parse + modelProperty.isJsonPrimitive and modelProperty.asJsonPrimitive.isString -> { + // Get the file + val modelFile = File(File(MinecraftClient.getInstance().runDirectory, "config/extraplayermodels/models"), modelProperty.asString) + // Doesn't exist? Exception. No trial, no nothing. We have strongest deserializer because of exception. + if (!modelFile.exists()) { + throw JsonParseException("Model file does not exist") + } + // Make the configuration + Configuration( + ModelLoader.gson.fromJson(modelFile.reader(), CustomPlayerModel::class.java) + ) + } + // Otherwise, throw an exception + else -> { + throw JsonParseException("Model name must be a string or null") + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomModelBoxDeserializer.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomModelBoxDeserializer.kt new file mode 100644 index 0000000..432ca1f --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomModelBoxDeserializer.kt @@ -0,0 +1,30 @@ +package spazzylemons.extraplayermodels.model.json + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import net.minecraft.client.util.math.Vector3f +import spazzylemons.extraplayermodels.model.CustomModelBox +import java.lang.reflect.Type + +/** + * Deserialize a [CustomModelBox]. + */ +class CustomModelBoxDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): CustomModelBox { + // Make sure it is an object + if (json?.isJsonObject != true) { + throw JsonParseException("Model box must be an object") + } + // Convert it + val obj = json.asJsonObject + // Make it + return CustomModelBox( + obj.requiredProperty("start"), + obj.requiredProperty("size"), + obj.optionalProperty("extra", Vector3f(0F, 0F, 0F)), + obj.requiredProperty("uv") + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomModelPartDeserializer.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomModelPartDeserializer.kt new file mode 100644 index 0000000..5fadc05 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomModelPartDeserializer.kt @@ -0,0 +1,28 @@ +package spazzylemons.extraplayermodels.model.json + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import net.minecraft.client.util.math.Vector3f +import spazzylemons.extraplayermodels.model.CustomModelBox +import spazzylemons.extraplayermodels.model.CustomModelPart +import java.lang.reflect.Type + +class CustomModelPartDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): CustomModelPart { + // Make sure it is an object + if (json?.isJsonObject != true) { + throw JsonParseException("Model part must be an object") + } + // Convert it + val obj = json.asJsonObject + // Make it + return CustomModelPart( + obj.optionalProperty("pivot", Vector3f(0F, 0F, 0F)), + obj.optionalProperty("rotation", Vector3f(0F, 0F, 0F)), + (obj.optionalProperty("boxes", emptyArray())).toList(), + (obj.optionalProperty("children", emptyArray())).toList() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomPlayerModelDeserializer.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomPlayerModelDeserializer.kt new file mode 100644 index 0000000..ad10331 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/CustomPlayerModelDeserializer.kt @@ -0,0 +1,29 @@ +package spazzylemons.extraplayermodels.model.json + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import spazzylemons.extraplayermodels.model.CustomPlayerModel +import java.lang.reflect.Type + +class CustomPlayerModelDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): CustomPlayerModel { + // Make sure it is an object + if (json?.isJsonObject != true) { + throw JsonParseException("Model must be an object") + } + // Convert it + val obj = json.asJsonObject + // Make it + return CustomPlayerModel( + obj.requiredProperty("head"), + obj.requiredProperty("torso"), + obj.requiredProperty("leftArm"), + obj.requiredProperty("rightArm"), + obj.requiredProperty("leftLeg"), + obj.requiredProperty("rightLeg"), + obj.requiredProperty("texture") + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/json/DeserializerHelpers.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/DeserializerHelpers.kt new file mode 100644 index 0000000..dccd5ca --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/DeserializerHelpers.kt @@ -0,0 +1,27 @@ +package spazzylemons.extraplayermodels.model.json + +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import spazzylemons.extraplayermodels.model.ModelLoader + +/** + * Deserialize a required property. If the property is not found, a [JsonParseException] is thrown. + * + * @param name The name of the property. + */ +inline fun JsonObject.requiredProperty(name: String): T { + val property = this[name] ?: throw JsonParseException("Property '$name' is required") + return ModelLoader.gson.fromJson(property, T::class.java) +} + + +/** + * Deserialize an optional property. If the property is not found, a sensible alternative is used. + * + * @param name The name of the property. + * @param alternate The alternative object. + */ +inline fun JsonObject.optionalProperty(name: String, alternate: T): T { + val property = this[name] ?: return alternate + return ModelLoader.gson.fromJson(property, T::class.java) +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/json/ImageArrayDeserializer.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/ImageArrayDeserializer.kt new file mode 100644 index 0000000..e24ba42 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/ImageArrayDeserializer.kt @@ -0,0 +1,50 @@ +package spazzylemons.extraplayermodels.model.json + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import net.minecraft.client.MinecraftClient +import spazzylemons.extraplayermodels.model.ImageArray +import java.io.File +import java.lang.reflect.Type +import javax.imageio.ImageIO + +/** + * Deserialize an [ImageArray]. + */ +class ImageArrayDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): ImageArray { + // Must be a string. ImageArray represents a texture, so we want a file name + if (json?.isJsonPrimitive != true || !json.asJsonPrimitive.isString) { + throw JsonParseException("Texture name must be a string") + } + // Get that string + val filename = json.asJsonPrimitive.asString + // Find the file + val file = File(File(MinecraftClient.getInstance().runDirectory, "config/extraplayermodels/textures"), filename) + // The file must exist, obviously + if (!file.exists()) { + throw JsonParseException("Skin file not found") + } + // Read it + val image = ImageIO.read(file) + // Enforce image size standard + if (image.width != 64 || image.height != 64) { + throw JsonParseException("Skin is not 64x64") + } + // Make an IntArray for the pixels + val intArray = IntArray(4096) + // Put each pixel in + var i = 0 + for (y in 0..63) { + for (x in 0..63) { + val pixel = image.getRGB(x, y) + // BGR to RGB conversion, or the other way around, I forgot + intArray[i++] = (pixel.inv() or 0x00FF00FF).inv() + ((pixel and 0xFF0000) shr 16) + ((pixel and 0xFF) shl 16) + } + } + // Wrap the IntArray in an ImageArray + return ImageArray(intArray) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/json/UVDeserializer.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/UVDeserializer.kt new file mode 100644 index 0000000..d8773e6 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/UVDeserializer.kt @@ -0,0 +1,36 @@ +package spazzylemons.extraplayermodels.model.json + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import spazzylemons.extraplayermodels.model.UVCoordinate +import java.lang.reflect.Type + +/** + * Deserializes a [UVCoordinate]. + */ +class UVDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): UVCoordinate { + // Expect an array for the two numbers + if (json?.isJsonArray != true) { + throw JsonParseException("UV must be an array.") + } + // Convert + val arr = json.asJsonArray + // Two elements, please + if (arr?.size() != 2) { + throw JsonParseException("UV must have 2 elements") + } + // Must be numbers + if (!arr[0].isJsonPrimitive or !arr[0].asJsonPrimitive.isNumber or + !arr[1].isJsonPrimitive or !arr[1].asJsonPrimitive.isNumber) { + throw JsonParseException("Element should be a number") + } + // Convert and make + return UVCoordinate( + arr[0].asJsonPrimitive.asNumber.toInt(), + arr[1].asJsonPrimitive.asNumber.toInt() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/model/json/VectorDeserializer.kt b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/VectorDeserializer.kt new file mode 100644 index 0000000..8d8971d --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/model/json/VectorDeserializer.kt @@ -0,0 +1,38 @@ +package spazzylemons.extraplayermodels.model.json + +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import net.minecraft.client.util.math.Vector3f +import java.lang.reflect.Type + +/** + * Deserializes a [Vector3f]. + */ +class VectorDeserializer : JsonDeserializer { + override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): Vector3f { + // Expect an array for the three numbers + if (json?.isJsonArray != true) { + throw JsonParseException("Vector must be an array") + } + // Convert + val arr = json.asJsonArray + // Three elements, please + if (arr?.size() != 3) { + throw JsonParseException("Vector must have 3 elements") + } + // Must be numbers + if (!arr[0].isJsonPrimitive or !arr[0].asJsonPrimitive.isNumber or + !arr[1].isJsonPrimitive or !arr[1].asJsonPrimitive.isNumber or + !arr[2].isJsonPrimitive or !arr[2].asJsonPrimitive.isNumber) { + throw JsonParseException("Element should be a number") + } + // Convert and make + return Vector3f( + arr[0].asJsonPrimitive.asNumber.toFloat(), + arr[1].asJsonPrimitive.asNumber.toFloat(), + arr[2].asJsonPrimitive.asNumber.toFloat() + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/packet/C2SInsertModel.kt b/src/main/kotlin/spazzylemons/extraplayermodels/packet/C2SInsertModel.kt new file mode 100644 index 0000000..c578235 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/packet/C2SInsertModel.kt @@ -0,0 +1,24 @@ +package spazzylemons.extraplayermodels.packet + +import net.fabricmc.fabric.api.network.PacketContext +import net.minecraft.util.Identifier +import net.minecraft.util.PacketByteBuf +import spazzylemons.extraplayermodels.ExtraPlayerModels +import spazzylemons.extraplayermodels.IMinecraftServerMixin +import spazzylemons.extraplayermodels.model.ModelBuilder + +/** + * Inserts a model into a server's model collection. + */ +object C2SInsertModel : Packet { + override val id = Identifier(ExtraPlayerModels.PACKET_NAMESPACE, "insert") + override val action = { ctx: PacketContext, data: PacketByteBuf -> + val model = ModelBuilder.modelFromBuf(data) + val server = ctx.player.server + val uuid = ctx.player.uuid + + ctx.taskQueue.execute { + (server as IMinecraftServerMixin).playerModelData.insert(uuid, model) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/packet/C2SRemoveModel.kt b/src/main/kotlin/spazzylemons/extraplayermodels/packet/C2SRemoveModel.kt new file mode 100644 index 0000000..001124d --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/packet/C2SRemoveModel.kt @@ -0,0 +1,22 @@ +package spazzylemons.extraplayermodels.packet + +import net.fabricmc.fabric.api.network.PacketContext +import net.minecraft.util.Identifier +import net.minecraft.util.PacketByteBuf +import spazzylemons.extraplayermodels.ExtraPlayerModels +import spazzylemons.extraplayermodels.IMinecraftServerMixin + +/** + * Removes a model from a server's model collection. + */ +object C2SRemoveModel : Packet { + override val id = Identifier(ExtraPlayerModels.PACKET_NAMESPACE, "remove") + override val action = { ctx: PacketContext, _: PacketByteBuf -> + val server = ctx.player.server + val uuid = ctx.player.uuid + + ctx.taskQueue.execute { + (server as IMinecraftServerMixin).playerModelData.remove(uuid) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/packet/NewPacketByteBuf.kt b/src/main/kotlin/spazzylemons/extraplayermodels/packet/NewPacketByteBuf.kt new file mode 100644 index 0000000..ffcd5c2 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/packet/NewPacketByteBuf.kt @@ -0,0 +1,6 @@ +package spazzylemons.extraplayermodels.packet + +import io.netty.buffer.Unpooled +import net.minecraft.util.PacketByteBuf + +fun newPacketByteBuf(): PacketByteBuf = PacketByteBuf(Unpooled.buffer()) \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/packet/Packet.kt b/src/main/kotlin/spazzylemons/extraplayermodels/packet/Packet.kt new file mode 100644 index 0000000..879c6b9 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/packet/Packet.kt @@ -0,0 +1,47 @@ +package spazzylemons.extraplayermodels.packet + +import io.netty.buffer.Unpooled +import net.fabricmc.fabric.api.network.ClientSidePacketRegistry +import net.fabricmc.fabric.api.network.PacketContext +import net.fabricmc.fabric.api.network.PacketRegistry +import net.fabricmc.fabric.api.network.ServerSidePacketRegistry +import net.minecraft.entity.player.PlayerEntity +import net.minecraft.util.Identifier +import net.minecraft.util.PacketByteBuf + +/** + * Represents a packet. + */ +interface Packet { + /** + * The identifier of the packet used to determine what the packet contains. + */ + val id: Identifier + + /** + * The action to be performed when the packet is received. + */ + val action: (PacketContext, PacketByteBuf) -> Unit + + /** + * Send this packet to a player. + * @param player The player to send this packet to. + * @param buffer The buffer with the packet's data. + */ + fun sendToPlayer(player: PlayerEntity, buffer: PacketByteBuf = PacketByteBuf(Unpooled.buffer())) + = ServerSidePacketRegistry.INSTANCE.sendToPlayer(player, id, buffer) + + /** + * Send this packet to a server. + * @param buffer The buffer with the packet's data. + */ + fun sendToServer(buffer: PacketByteBuf = PacketByteBuf(Unpooled.buffer())) + = ClientSidePacketRegistry.INSTANCE.sendToServer(id, buffer) + + /** + * Register this packet in a registry. + * @param registry The registry to register this packet to. + */ + fun register(registry: PacketRegistry) + = registry.register(id, action) +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/packet/S2CInsertModel.kt b/src/main/kotlin/spazzylemons/extraplayermodels/packet/S2CInsertModel.kt new file mode 100644 index 0000000..51256c5 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/packet/S2CInsertModel.kt @@ -0,0 +1,23 @@ +package spazzylemons.extraplayermodels.packet + +import net.fabricmc.fabric.api.network.PacketContext +import net.minecraft.util.Identifier +import net.minecraft.util.PacketByteBuf +import spazzylemons.extraplayermodels.ExtraPlayerModels +import spazzylemons.extraplayermodels.client.ClientData +import spazzylemons.extraplayermodels.model.ModelBuilder + +/** + * Inserts a model into a client's model collection. + */ +object S2CInsertModel : Packet { + override val id = Identifier(ExtraPlayerModels.PACKET_NAMESPACE, "insert") + override val action = { ctx: PacketContext, data: PacketByteBuf -> + val uuid = data.readUuid() + val model = ModelBuilder.modelFromBuf(data) + + ctx.taskQueue.execute { + ClientData.insert(uuid, model) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/packet/S2CRemoveModel.kt b/src/main/kotlin/spazzylemons/extraplayermodels/packet/S2CRemoveModel.kt new file mode 100644 index 0000000..fea5fc5 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/packet/S2CRemoveModel.kt @@ -0,0 +1,21 @@ +package spazzylemons.extraplayermodels.packet + +import net.fabricmc.fabric.api.network.PacketContext +import net.minecraft.util.Identifier +import net.minecraft.util.PacketByteBuf +import spazzylemons.extraplayermodels.ExtraPlayerModels +import spazzylemons.extraplayermodels.client.ClientData + +/** + * Removes a model from a client's model collection. + */ +object S2CRemoveModel : Packet { + override val id = Identifier(ExtraPlayerModels.PACKET_NAMESPACE, "remove") + override val action = { ctx: PacketContext, data: PacketByteBuf -> + val uuid = data.readUuid() + + ctx.taskQueue.execute { + ClientData.remove(uuid) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/render/CustomPlayerEntityModel.kt b/src/main/kotlin/spazzylemons/extraplayermodels/render/CustomPlayerEntityModel.kt new file mode 100644 index 0000000..574b5ef --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/render/CustomPlayerEntityModel.kt @@ -0,0 +1,40 @@ +package spazzylemons.extraplayermodels.render + +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.model.ModelPart +import net.minecraft.client.render.entity.model.PlayerEntityModel +import net.minecraft.entity.LivingEntity +import spazzylemons.extraplayermodels.IModelPartMixin +import spazzylemons.extraplayermodels.model.CustomPlayerModel + +/** + * Represents an entity model. May replace the [PlayerEntityModel] parts if it is custom + */ +@Environment(EnvType.CLIENT) +class CustomPlayerEntityModel(scale: Float, model: CustomPlayerModel) : PlayerEntityModel(scale, false) { + init { + // Head and helmet + head = ModelPart(this) + (helmet as IModelPartMixin).getCuboids().clear() + helmet.addChild(model.head.asModelPart(this)) + // Torso and jacket + torso = ModelPart(this) + (jacket as IModelPartMixin).getCuboids().clear() + jacket.addChild(model.torso.asModelPart(this)) + // Arms and sleeves + leftArm = ModelPart(this) + (leftSleeve as IModelPartMixin).getCuboids().clear() + leftSleeve.addChild(model.leftArm.asModelPart(this)) + rightArm = ModelPart(this) + (rightSleeve as IModelPartMixin).getCuboids().clear() + rightSleeve.addChild(model.rightArm.asModelPart(this)) + // Legs and pants + leftLeg = ModelPart(this) + (leftPantLeg as IModelPartMixin).getCuboids().clear() + leftPantLeg.addChild(model.leftLeg.asModelPart(this)) + rightLeg = ModelPart(this) + (rightPantLeg as IModelPartMixin).getCuboids().clear() + rightPantLeg.addChild(model.rightLeg.asModelPart(this)) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/render/CustomPlayerEntityRenderer.kt b/src/main/kotlin/spazzylemons/extraplayermodels/render/CustomPlayerEntityRenderer.kt new file mode 100644 index 0000000..8669fb8 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/render/CustomPlayerEntityRenderer.kt @@ -0,0 +1,17 @@ +package spazzylemons.extraplayermodels.render + +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.render.entity.EntityRenderDispatcher +import net.minecraft.client.render.entity.PlayerEntityRenderer +import spazzylemons.extraplayermodels.model.CustomPlayerModel + +/** + * Used in place of [PlayerEntityRenderer] to allow custom models. + */ +@Environment(EnvType.CLIENT) +class CustomPlayerEntityRenderer(dispatcher: EntityRenderDispatcher, model: CustomPlayerModel) : PlayerEntityRenderer(dispatcher, false) { + init { + this.model = CustomPlayerEntityModel(0.0F, model) + } +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/render/PreviewPlayerEntity.kt b/src/main/kotlin/spazzylemons/extraplayermodels/render/PreviewPlayerEntity.kt new file mode 100644 index 0000000..7dbe6a4 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/render/PreviewPlayerEntity.kt @@ -0,0 +1,83 @@ +package spazzylemons.extraplayermodels.render + +import com.mojang.authlib.GameProfile +import net.fabricmc.api.EnvType +import net.fabricmc.api.Environment +import net.minecraft.client.MinecraftClient +import net.minecraft.client.network.ClientPlayNetworkHandler +import net.minecraft.client.network.ClientPlayerEntity +import net.minecraft.client.network.PlayerListEntry +import net.minecraft.client.recipe.book.ClientRecipeBook +import net.minecraft.client.render.entity.PlayerModelPart +import net.minecraft.client.world.ClientWorld +import net.minecraft.network.ClientConnection +import net.minecraft.network.NetworkSide +import net.minecraft.recipe.RecipeManager +import net.minecraft.stat.StatHandler +import net.minecraft.util.Identifier +import net.minecraft.util.profiler.DisableableProfiler +import net.minecraft.world.GameMode +import net.minecraft.world.dimension.DimensionType +import net.minecraft.world.level.LevelGeneratorType +import net.minecraft.world.level.LevelInfo +import spazzylemons.extraplayermodels.client.ClientData +import java.util.* +import java.util.function.IntSupplier + +/** + * A player entity used only for previewing the current model. + */ +@Environment(EnvType.CLIENT) +class PreviewPlayerEntity : ClientPlayerEntity( + MinecraftClient.getInstance(), + ClientWorld( + ClientPlayNetworkHandler( + MinecraftClient.getInstance(), + null, + ClientConnection( + NetworkSide.CLIENTBOUND + ), + GameProfile( + UUID(0L, 0L), + MinecraftClient.getInstance().session.username + ) + ), + LevelInfo(0, GameMode.CREATIVE, false, false, LevelGeneratorType.FLAT), + DimensionType.OVERWORLD, + 1, + DisableableProfiler(IntSupplier { 20 }), + MinecraftClient.getInstance().worldRenderer + ), + ClientPlayNetworkHandler( + MinecraftClient.getInstance(), + null, + ClientConnection( + NetworkSide.CLIENTBOUND + ), + GameProfile( + UUID(0L, 0L), + MinecraftClient.getInstance().session.username + ) + ), + StatHandler(), + ClientRecipeBook(RecipeManager()) +) { + // Get the skin texture from the local data + override fun getSkinTexture(): Identifier = if (ClientData.currentModel != null) { + ClientData.localTextureIdentifier!! + } else { + super.getSkinTexture() + } + + // Override to avoid NullPointerExceptions in the super method + override fun getPlayerListEntry(): PlayerListEntry? = null + + // Ditto + override fun isSpectator(): Boolean = false + + // Ditto + override fun isCreative(): Boolean = true + + // Use local data + override fun isPartVisible(modelPart: PlayerModelPart?): Boolean = MinecraftClient.getInstance().options.enabledPlayerModelParts.contains(modelPart) +} \ No newline at end of file diff --git a/src/main/kotlin/spazzylemons/extraplayermodels/server/ServerData.kt b/src/main/kotlin/spazzylemons/extraplayermodels/server/ServerData.kt new file mode 100644 index 0000000..bdc81c4 --- /dev/null +++ b/src/main/kotlin/spazzylemons/extraplayermodels/server/ServerData.kt @@ -0,0 +1,52 @@ +package spazzylemons.extraplayermodels.server + +import io.netty.buffer.Unpooled +import net.fabricmc.fabric.api.server.PlayerStream +import net.minecraft.server.MinecraftServer +import net.minecraft.util.PacketByteBuf +import spazzylemons.extraplayermodels.model.CustomPlayerModel +import spazzylemons.extraplayermodels.packet.S2CInsertModel +import spazzylemons.extraplayermodels.packet.S2CRemoveModel +import java.util.* + +/** + * Used to store data about the custom player models in a [MinecraftServer]. + * + * @property server The attached server. + */ +class ServerData(private val server: MinecraftServer) { + /** + * The list of players. + */ + val modelList = mutableMapOf() + + /** + * Insert or update a player model. + * + * @param playerId The player's UUID. + * @param model The model. + */ + fun insert(playerId: UUID, model: CustomPlayerModel) { + modelList[playerId] = model + val buf = PacketByteBuf(Unpooled.buffer()) + buf.writeUuid(playerId) + model.writeToBuf(buf) + for (player in PlayerStream.all(server)) { + S2CInsertModel.sendToPlayer(player, buf) + } + } + + /** + * Remove a player model. + * + * @param playerId The player's UUID. + */ + fun remove(playerId: UUID) { + modelList.remove(playerId) + val buf = PacketByteBuf(Unpooled.buffer()) + buf.writeUuid(playerId) + for (player in PlayerStream.all(server)) { + S2CRemoveModel.sendToPlayer(player, buf) + } + } +} \ No newline at end of file diff --git a/src/main/resources/assets/extraplayermodels/icon.png b/src/main/resources/assets/extraplayermodels/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f645a71cbe3d108a1fa551dd38f25937be5e3330 GIT binary patch literal 8080 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/extraplayermodels/lang/en_us.json b/src/main/resources/assets/extraplayermodels/lang/en_us.json new file mode 100644 index 0000000..434923e --- /dev/null +++ b/src/main/resources/assets/extraplayermodels/lang/en_us.json @@ -0,0 +1,14 @@ +{ + "text.extraplayermodels.loadmodel.success": "The model was loaded successfully.", + "text.extraplayermodels.loadmodel.failure": "The model could not be loaded, check your syntax and that the file exists", + "text.extraplayermodels.loadmodel.explanation": "Couldn't load the model! Reason: %s", + + "text.extraplayermodels.appearance": "Appearance", + "text.extraplayermodels.appearance.withmodname": "Extra Player Models options", + "text.extraplayermodels.config.title": "Extra Player Models config", + "text.extraplayermodels.config.unfinished": "This screen is unfinished. For now, you can edit your configuration manually.", + "text.extraplayermodels.config.reload": "Reload config", + "text.extraplayermodels.config.view": "Open config folder", + + "key.extraplayermodels.camerakey": "Look at model" +} \ No newline at end of file diff --git a/src/main/resources/assets/extraplayermodels/textures/skin/steve.png b/src/main/resources/assets/extraplayermodels/textures/skin/steve.png new file mode 100644 index 0000000000000000000000000000000000000000..90d4fa236b5512e124a1aaaa2a2a357cca907e99 GIT binary patch literal 1350 zcmV-M1-bf(P)ju5;ka9MAmD+4+9XWzTvhkqAlks}>TpVZ}U>d1%ez zO1g149jHX9CpMg3vq(t-fMBw3?>2gTs>!=TWd(rE*VR;50VI=2pHNm70J!({X~1P= z4?vp52!P~tm=gd@=EDGVr0U8C02|-Da)HWY_16KIR5k$ExD=0!lK&b^)m9J$Q=4il zmD%md#k6&;tMuB1)UeuJ2U065)k26^lmP(8p$_Vz4czB;!}bx80sw7UU8Rh`;EoN| z>dM03`|FobYyC162Ou1SIw<3IB!HlO#H<5Q552`nm*mV zLf;<^s_U2GK^n_kpx+}I8oPE(2{DL89n?i0ZJ|y49k!2%%oK2^!2(JypHENDw$Rw6 zgVd5*s>~yG>>i;Xcdjb)$Rhy?Vi1cusEaman_>Hi=m2nf%pJDX^r)keMlbH8r{|j; zbBE~LogVu2Ame?#{dqb-ANmf{u(Rh!3}R6SbIbtOMX*Z)|$(>JinW z&(7_l>qpWwbZ`p|w$&@M(Cy61Hfi56oBag`62Ou7!lH>jE79-Wg8~IGG^8RVA%5mATK|h0mw32JjcPH%SI&Jdg5%p@cTmwWim#;pBDf&Cr5x4fHVUb z5`f8=1jbXqR3|&sO#nuRIz5A2UQ_@%uK_v$+52+EmH^}$pqZ9Hq&Xb`t|kDgJR39R zufe3F>C0TlcEeCy!^uta0_J@A@o7eI8OaaLE{Q`Z6n*bE_0>pHu zLf=0.7.1", + "fabric": "*", + "fabric-language-kotlin": "*", + "minecraft": "1.15.x" + } +}