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 0000000..deedc7f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ 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 0000000..f645a71 Binary files /dev/null and b/src/main/resources/assets/extraplayermodels/icon.png differ 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 0000000..90d4fa2 Binary files /dev/null and b/src/main/resources/assets/extraplayermodels/textures/skin/steve.png differ diff --git a/src/main/resources/extraplayermodels.mixins.json b/src/main/resources/extraplayermodels.mixins.json new file mode 100644 index 0000000..d7d931a --- /dev/null +++ b/src/main/resources/extraplayermodels.mixins.json @@ -0,0 +1,22 @@ +{ + "required": true, + "package": "spazzylemons.extraplayermodels.mixin", + "compatibilityLevel": "JAVA_8", + "mixins": [ + "MinecraftServerMixin", + "PlayerManagerMixin" + ], + "client": [ + "AbstractClientPlayerEntityMixin", + "CameraMixin", + "EntityRenderDispatcherMixin", + "MinecraftClientMixin", + "ModelPartMixin", + "MouseMixin", + "SkinOptionsScreenMixin", + "TitleScreenMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..eba0c33 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "id": "extraplayermodels", + "version": "${version}", + + "name": "Extra Player Models", + "description": "Adds the ability to create custom player models.", + "authors": [ + "spazzylemons" + ], + "contact": { + "homepage": "https://github.com/spazzylemons/ExtraPlayerModels", + "sources": "https://github.com/spazzylemons/ExtraPlayerModels" + }, + + "license": "MIT", + "icon": "assets/extraplayermodels/icon.png", + + "environment": "*", + "entrypoints": { + "main": [ + "spazzylemons.extraplayermodels.EntryKt::init" + ], + "client": [ + "spazzylemons.extraplayermodels.EntryKt::clientInit" + ], + "modmenu": [ + "spazzylemons.extraplayermodels.ExtraPlayerModelsModMenu" + ] + }, + "mixins": [ + "extraplayermodels.mixins.json" + ], + "depends": { + "fabricloader": ">=0.7.1", + "fabric": "*", + "fabric-language-kotlin": "*", + "minecraft": "1.15.x" + } +}