commit af2189e1b2f28344a21d697ebd92f2a296048d1d
Author: HeroBrineGoat <76707404+MasterOfTheFish@users.noreply.github.com>
Date: Mon Nov 8 17:00:01 2021 -0500
Initial commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..8a0a60d2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+# Project exclude paths
+/.gradle/
+/build/
+/build/classes/java/main/
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 00000000..659bf431
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/discord.xml b/.idea/discord.xml
new file mode 100644
index 00000000..30bab2ab
--- /dev/null
+++ b/.idea/discord.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 00000000..ba1ec5c7
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 00000000..12511d1b
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 00000000..6680db93
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 00000000..797acea5
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 00000000..e96534fb
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..2a4ce6ae
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,49 @@
+plugins {
+ id 'java'
+ id 'com.github.johnrengelman.shadow' version '6.1.0'
+}
+
+group 'io.github.fisher2911'
+version '1.0.0'
+
+repositories {
+ mavenCentral()
+ mavenLocal()
+ maven { url 'https://papermc.io/repo/repository/maven-public/' }
+ maven { url = 'https://repo.mattstudios.me/artifactory/public/' }
+ maven { url = 'https://jitpack.io' }
+ maven { url = 'https://repo.extendedclip.com/content/repositories/placeholderapi/' }
+ maven { url = 'https://repo.leonardobishop.com/releases/' }
+ maven { url = 'https://jitpack.io' }
+}
+
+dependencies {
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
+ compileOnly 'io.papermc.paper:paper:1.17.1-R0.1-SNAPSHOT'
+ compileOnly 'org.jetbrains:annotations:22.0.0'
+ compileOnly 'net.kyori:adventure-api:4.9.3'
+ implementation 'net.kyori:adventure-text-minimessage:4.2.0-SNAPSHOT'
+ implementation 'net.kyori:adventure-platform-bukkit:4.0.0'
+ implementation 'dev.triumphteam:triumph-gui:3.0.3'
+ implementation 'me.mattstudios.utils:matt-framework:1.4.6'
+ implementation 'org.spongepowered:configurate-yaml:4.1.2'
+}
+
+test {
+ useJUnitPlatform()
+}
+
+shadowJar {
+ relocate 'dev.triumphteam.gui', 'io.github.fisher2911.hmccosmetics.gui'
+ relocate 'me.mattstudios.mf', 'io.github.fisher2911.hmccosmetics.mf'
+ relocate 'net.kyori.adventure.text.minimessage', 'io.github.fisher2911.hmccosmetics.adventure.minimessage'
+ relocate 'net.kyori.adventure.platform', 'io.github.fisher2911.hmccosmetics.adventure.platform'
+ relocate 'org.spongepowered.configurate', 'io.github.fisher2911.hmccosmetics.configurate'
+}
+
+shadowJar {
+ archiveBaseName.set('BackpackCosmetics')
+ archiveClassifier.set('')
+ archiveVersion.set('')
+}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..7454180f
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 00000000..69a97150
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 00000000..744e882e
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## 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='"-Xmx64m" "-Xms64m"'
+
+# 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
+ ;;
+ MSYS* | 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 or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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=`expr $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"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..107acd32
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@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 Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@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="-Xmx64m" "-Xms64m"
+
+@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 execute
+
+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 execute
+
+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
+
+: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 %*
+
+: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 00000000..f7e048dd
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'HMCCosmetics'
+
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/HMCCosmetics.java b/src/main/java/io/github/fisher2911/hmccosmetics/HMCCosmetics.java
new file mode 100644
index 00000000..11c562a6
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/HMCCosmetics.java
@@ -0,0 +1,72 @@
+package io.github.fisher2911.hmccosmetics;
+
+import io.github.fisher2911.hmccosmetics.command.CosmeticsCommand;
+import io.github.fisher2911.hmccosmetics.gui.CosmeticsMenu;
+import io.github.fisher2911.hmccosmetics.listener.JoinListener;
+import io.github.fisher2911.hmccosmetics.message.MessageHandler;
+import io.github.fisher2911.hmccosmetics.message.Messages;
+import io.github.fisher2911.hmccosmetics.user.UserManager;
+import me.mattstudios.mf.base.CommandManager;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.List;
+
+public class HMCCosmetics extends JavaPlugin {
+
+ private UserManager userManager;
+ private MessageHandler messageHandler;
+ private CosmeticsMenu cosmeticsMenu;
+ private CommandManager commandManager;
+
+ @Override
+ public void onEnable() {
+ this.messageHandler = new MessageHandler(this);
+ this.userManager = new UserManager(this);
+ this.cosmeticsMenu = new CosmeticsMenu(this);
+ this.messageHandler.load();
+ this.cosmeticsMenu.load();
+ this.registerCommands();
+ this.registerListeners();
+
+ this.userManager.startTeleportTask();
+ }
+
+ @Override
+ public void onDisable() {
+ this.messageHandler.close();
+ this.userManager.cancelTeleportTask();
+ this.userManager.removeAll();
+ }
+
+ private void registerListeners() {
+ List.of(new JoinListener(this)).
+ forEach(listener ->
+ this.getServer().getPluginManager().registerEvents(listener, this)
+ );
+ }
+
+ private void registerCommands() {
+ this.commandManager = new CommandManager(this, true);
+ this.commandManager.getMessageHandler().register(
+ "cmd.no.console", player ->
+ this.messageHandler.sendMessage(
+ player,
+ Messages.MUST_BE_PLAYER
+ )
+
+ );
+ this.commandManager.register(new CosmeticsCommand(this));
+ }
+
+ public MessageHandler getMessageHandler() {
+ return messageHandler;
+ }
+
+ public UserManager getUserManager() {
+ return userManager;
+ }
+
+ public CosmeticsMenu getCosmeticsMenu() {
+ return cosmeticsMenu;
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/command/CosmeticsCommand.java b/src/main/java/io/github/fisher2911/hmccosmetics/command/CosmeticsCommand.java
new file mode 100644
index 00000000..624d1ba8
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/command/CosmeticsCommand.java
@@ -0,0 +1,31 @@
+package io.github.fisher2911.hmccosmetics.command;
+
+import io.github.fisher2911.hmccosmetics.HMCCosmetics;
+import io.github.fisher2911.hmccosmetics.gui.CosmeticsMenu;
+import io.github.fisher2911.hmccosmetics.message.MessageHandler;
+import me.mattstudios.mf.annotations.Command;
+import me.mattstudios.mf.annotations.Default;
+import me.mattstudios.mf.annotations.Permission;
+import me.mattstudios.mf.base.CommandBase;
+import org.bukkit.entity.Player;
+
+@Command("cosmetics")
+public class CosmeticsCommand extends CommandBase {
+
+ private final HMCCosmetics plugin;
+ private final MessageHandler messageHandler;
+ private final CosmeticsMenu cosmeticsMenu;
+
+ public CosmeticsCommand(final HMCCosmetics plugin) {
+ this.plugin = plugin;
+ this.messageHandler = this.plugin.getMessageHandler();
+ this.cosmeticsMenu = this.plugin.getCosmeticsMenu();
+ }
+
+ @Default
+ @Permission(io.github.fisher2911.hmccosmetics.message.Permission.DEFAULT_COMMAND)
+ public void defaultCommand(final Player player) {
+ this.cosmeticsMenu.openDefault(player);
+ }
+
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/config/GuiSerializer.java b/src/main/java/io/github/fisher2911/hmccosmetics/config/GuiSerializer.java
new file mode 100644
index 00000000..d0c9a3fd
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/config/GuiSerializer.java
@@ -0,0 +1,69 @@
+package io.github.fisher2911.hmccosmetics.config;
+
+import dev.triumphteam.gui.guis.GuiItem;
+import io.github.fisher2911.hmccosmetics.HMCCosmetics;
+import io.github.fisher2911.hmccosmetics.gui.CosmeticGui;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.serialize.TypeSerializer;
+
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+public class GuiSerializer implements TypeSerializer {
+
+ private static final HMCCosmetics plugin;
+
+ static {
+ plugin = HMCCosmetics.getPlugin(HMCCosmetics.class);
+ }
+
+ public static final GuiSerializer INSTANCE = new GuiSerializer();
+
+ private GuiSerializer() {}
+
+ private static final String TITLE = "title";
+ private static final String ROWS = "rows";
+ private static final String ITEMS = "items";
+
+ private ConfigurationNode nonVirtualNode(final ConfigurationNode source, final Object... path) throws SerializationException {
+ if (!source.hasChild(path)) {
+ throw new SerializationException("Required field " + Arrays.toString(path) + " was not present in node");
+ }
+ return source.node(path);
+ }
+
+ @Override
+ public CosmeticGui deserialize(final Type type, final ConfigurationNode source) throws SerializationException {
+ final ConfigurationNode titleNode = this.nonVirtualNode(source, TITLE);
+ final ConfigurationNode rowsNode = this.nonVirtualNode(source, ROWS);
+ final ConfigurationNode itemsNode = source.node(ITEMS);
+
+ final var childrenMap = source.node(ITEMS).childrenMap();
+
+ final Map guiItemMap = new HashMap<>();
+
+ for (final var entry : childrenMap.entrySet()) {
+ if (!(entry.getKey() instanceof final Integer slot)) {
+ continue;
+ }
+
+ final GuiItem guiItem = ItemSerializer.INSTANCE.deserialize(
+ GuiItem.class,
+ entry.getValue()
+ );
+
+ guiItemMap.put(slot, guiItem);
+ }
+
+ return new CosmeticGui(plugin, titleNode.getString(), rowsNode.getInt(), guiItemMap);
+ }
+
+ @Override
+ public void serialize(final Type type, @Nullable final CosmeticGui obj, final ConfigurationNode node) throws SerializationException {
+
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/config/ItemSerializer.java b/src/main/java/io/github/fisher2911/hmccosmetics/config/ItemSerializer.java
new file mode 100644
index 00000000..2a30f395
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/config/ItemSerializer.java
@@ -0,0 +1,205 @@
+package io.github.fisher2911.hmccosmetics.config;
+
+import dev.triumphteam.gui.guis.GuiItem;
+import io.github.fisher2911.hmccosmetics.HMCCosmetics;
+import io.github.fisher2911.hmccosmetics.gui.ArmorItem;
+import io.github.fisher2911.hmccosmetics.util.StringUtils;
+import io.github.fisher2911.hmccosmetics.util.Utils;
+import io.github.fisher2911.hmccosmetics.util.builder.ItemBuilder;
+import io.github.fisher2911.hmccosmetics.util.builder.LeatherArmorBuilder;
+import io.github.fisher2911.hmccosmetics.util.builder.SkullBuilder;
+import org.bukkit.Bukkit;
+import org.bukkit.Color;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.Registry;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.serialize.TypeSerializer;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class ItemSerializer implements TypeSerializer {
+
+ public static final ItemSerializer INSTANCE = new ItemSerializer();
+
+ private static final String MATERIAL = "material";
+ private static final String AMOUNT = "amount";
+ private static final String NAME = "name";
+ private static final String UNBREAKABLE = "unbreakable";
+ private static final String GLOWING = "glowing";
+ private static final String LORE = "lore";
+ private static final String MODEL_DATA = "model-data";
+ private static final String ENCHANTS = "enchants";
+ private static final String ITEM_FLAGS = "item-flags";
+ private static final String TEXTURE = "texture";
+ private static final String OWNER = "owner";
+ private static final String COLOR = "color";
+ private static final String RED = "red";
+ private static final String GREEN = "green";
+ private static final String BLUE = "blue";
+ private static final String PERMISSION = "permission";
+ private static final String TYPE = "type";
+ private static final String OPEN_MENU = "open-menu";
+ private static final String ID = "id";
+
+ private ItemSerializer() {
+ }
+
+ private ConfigurationNode nonVirtualNode(final ConfigurationNode source, final Object... path) throws SerializationException {
+ if (!source.hasChild(path)) {
+ throw new SerializationException("Required field " + Arrays.toString(path) + " was not present in node");
+ }
+
+ return source.node(path);
+ }
+
+ @Override
+ public GuiItem deserialize(final Type type, final ConfigurationNode source) throws SerializationException {
+ final ConfigurationNode materialNode = this.nonVirtualNode(source, MATERIAL);
+ final ConfigurationNode amountNode = source.node(AMOUNT);
+ final ConfigurationNode nameNode = source.node(NAME);
+ final ConfigurationNode unbreakableNode = source.node(UNBREAKABLE);
+ final ConfigurationNode glowingNode = source.node(GLOWING);
+ final ConfigurationNode loreNode = source.node(LORE);
+ final ConfigurationNode modelDataNode = source.node(MODEL_DATA);
+ final ConfigurationNode enchantsNode = source.node(ENCHANTS);
+ final ConfigurationNode itemFlagsNode = source.node(ITEM_FLAGS);
+ final ConfigurationNode textureNode = source.node(TEXTURE);
+ final ConfigurationNode ownerNode = source.node(OWNER);
+ final ConfigurationNode colorNode = source.node(COLOR);
+ final ConfigurationNode redNode = colorNode.node(RED);
+ final ConfigurationNode greenNode = colorNode.node(GREEN);
+ final ConfigurationNode blueNode = colorNode.node(BLUE);
+ final ConfigurationNode permissionNode = source.node(PERMISSION);
+ final ConfigurationNode typeNode = source.node(TYPE);
+ final ConfigurationNode openMenuNode = source.node(OPEN_MENU);
+ final ConfigurationNode idNode = source.node(ID);
+
+
+ final Material material = Utils.stringToEnum(Utils.replaceIfNull(materialNode.getString(), ""),
+ Material.class, Material.AIR);
+ final int amount = amountNode.getInt();
+ final String name = StringUtils.parseStringToString(Utils.replaceIfNull(nameNode.getString(), ""));
+
+ final boolean unbreakable = unbreakableNode.getBoolean();
+ final boolean glowing = glowingNode.getBoolean();
+ final List lore = Utils.replaceIfNull(loreNode.getList(String.class), new ArrayList()).
+ stream().map(StringUtils::parseStringToString).collect(Collectors.toList());
+ final int modelData = modelDataNode.getInt();
+ final Set itemFlags = Utils.replaceIfNull(itemFlagsNode.getList(String.class), new ArrayList()).
+ stream().map(flag -> {
+ try {
+ return ItemFlag.valueOf(flag.toUpperCase());
+ } catch (final Exception ignored) {
+ return null;
+ }
+ }).collect(Collectors.toSet());
+ final String texture = textureNode.getString();
+ final String owner = ownerNode.getString();
+ final Color color = Color.fromBGR(redNode.getInt(), greenNode.getInt(), blueNode.getInt());
+
+ final Map enchantments =
+ Utils.replaceIfNull(enchantsNode.getList(String.class),
+ new ArrayList()).
+ stream().
+ collect(Collectors.toMap(enchantmentString -> {
+
+ if (!enchantmentString.contains(":")) {
+ return null;
+ }
+
+ final NamespacedKey namespacedKey = NamespacedKey.minecraft(enchantmentString.
+ split(":")[0].
+ toLowerCase());
+ return Registry.ENCHANTMENT.get(namespacedKey);
+
+ }, enchantmentString -> {
+ if (!enchantmentString.contains(":")) {
+ return 0;
+ }
+ try {
+ return Integer.parseInt(enchantmentString.split(":")[1]);
+ } catch (final NumberFormatException exception) {
+ return 0;
+ }
+ }));
+
+
+ final ItemBuilder itemBuilder;
+
+ if (material == Material.PLAYER_HEAD) {
+ itemBuilder = SkullBuilder.
+ create();
+
+ if (texture != null) {
+ ((SkullBuilder) itemBuilder).texture(texture);
+ } else if (owner != null) {
+ final OfflinePlayer player = Bukkit.getOfflinePlayer(owner);
+ ((SkullBuilder) itemBuilder).owner(player);
+ }
+ } else if (LeatherArmorBuilder.isLeatherArmor(material)) {
+ itemBuilder = LeatherArmorBuilder.from(material);
+ if (color != null) {
+ ((LeatherArmorBuilder) itemBuilder).color(color);
+ }
+ } else {
+ itemBuilder = ItemBuilder.from(material);
+ }
+
+ final ItemStack itemStack = itemBuilder.
+ amount(amount).
+ name(name).
+ unbreakable(unbreakable).
+ glow(glowing).
+ lore(lore).
+ modelData(modelData).
+ enchants(enchantments, true).
+ itemFlags(itemFlags).
+ build();
+
+ try {
+ final ArmorItem.Type cosmeticType = ArmorItem.Type.valueOf(
+ Utils.replaceIfNull(
+ typeNode.getString(), ""
+ ).toUpperCase(Locale.ROOT)
+ );
+
+ final String permission = permissionNode.getString();
+
+ return new ArmorItem(
+ itemStack,
+ Utils.replaceIfNull(idNode.getString(), ""),
+ permission,
+ cosmeticType);
+
+ } catch (final IllegalArgumentException exception) {
+ final String openMenu = openMenuNode.getString(
+ Utils.replaceIfNull(OPEN_MENU, ""));
+
+ return dev.triumphteam.gui.builder.item.ItemBuilder.from(
+ itemStack).
+ asGuiItem(event -> {
+ final HMCCosmetics plugin = HMCCosmetics.getPlugin(HMCCosmetics.class);
+ plugin.getCosmeticsMenu().openMenu(openMenu, event.getWhoClicked());
+ });
+ }
+ }
+
+ @Override
+ public void serialize(final Type type, @Nullable final GuiItem obj, final ConfigurationNode node) throws SerializationException {
+
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/gui/ArmorItem.java b/src/main/java/io/github/fisher2911/hmccosmetics/gui/ArmorItem.java
new file mode 100644
index 00000000..be36b68b
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/gui/ArmorItem.java
@@ -0,0 +1,81 @@
+package io.github.fisher2911.hmccosmetics.gui;
+
+import dev.triumphteam.gui.components.GuiAction;
+import dev.triumphteam.gui.guis.GuiItem;
+import org.bukkit.Material;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class ArmorItem extends GuiItem {
+
+ private final String id;
+ private final String permission;
+ private final Type type;
+
+ public ArmorItem(
+ @NotNull final ItemStack itemStack,
+ final GuiAction action,
+ final String id, final String permission,
+ final Type type) {
+ super(itemStack, action);
+ this.id = id;
+ this.permission = permission;
+ this.type = type;
+ }
+
+ public ArmorItem(
+ @NotNull final ItemStack itemStack,
+ final String id,
+ final String permission,
+ final Type type) {
+ super(itemStack);
+ this.id = id;
+ this.permission = permission;
+ this.type = type;
+ }
+
+ public ArmorItem(
+ @NotNull final Material material,
+ final String id,
+ final String permission,
+ final Type type) {
+ super(material);
+ this.id = id;
+ this.permission = permission;
+ this.type = type;
+ }
+
+ public ArmorItem(
+ @NotNull final Material material,
+ @Nullable final GuiAction action,
+ final String id,
+ final String permission,
+ final Type type) {
+ super(material, action);
+ this.id = id;
+ this.permission = permission;
+ this.type = type;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getPermission() {
+ return permission;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public enum Type {
+
+ HAT,
+
+ BACKPACK
+
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/gui/CosmeticGui.java b/src/main/java/io/github/fisher2911/hmccosmetics/gui/CosmeticGui.java
new file mode 100644
index 00000000..444eb88a
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/gui/CosmeticGui.java
@@ -0,0 +1,157 @@
+package io.github.fisher2911.hmccosmetics.gui;
+
+import dev.triumphteam.gui.guis.Gui;
+import dev.triumphteam.gui.guis.GuiItem;
+import io.github.fisher2911.hmccosmetics.HMCCosmetics;
+import io.github.fisher2911.hmccosmetics.inventory.PlayerArmor;
+import io.github.fisher2911.hmccosmetics.message.Adventure;
+import io.github.fisher2911.hmccosmetics.message.MessageHandler;
+import io.github.fisher2911.hmccosmetics.message.Messages;
+import io.github.fisher2911.hmccosmetics.message.Placeholder;
+import io.github.fisher2911.hmccosmetics.user.User;
+import io.github.fisher2911.hmccosmetics.user.UserManager;
+import io.github.fisher2911.hmccosmetics.util.builder.ItemBuilder;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.HumanEntity;
+import org.bukkit.entity.Player;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+public class CosmeticGui {
+
+ private final HMCCosmetics plugin;
+ private final MessageHandler messageHandler;
+ private final String title;
+ private final int rows;
+ private final Map guiItemMap;
+ private Gui gui;
+
+ public CosmeticGui(
+ final HMCCosmetics plugin,
+ final String title,
+ final int rows,
+ final Map guiItemMap) {
+ this.plugin = plugin;
+ this.messageHandler = this.plugin.getMessageHandler();
+ this.title = title;
+ this.rows = rows;
+ this.guiItemMap = guiItemMap;
+ }
+
+ private void setItems(final User user) {
+
+ final Player player = user.getPlayer();
+
+ if (player == null) {
+ return;
+ }
+
+ for (final var entry : guiItemMap.entrySet()) {
+ final int slot = entry.getKey();
+
+ final GuiItem guiItem = entry.getValue();
+
+ if (guiItem instanceof final ArmorItem armorItem) {
+
+ final Map placeholders = new HashMap<>();
+
+ final PlayerArmor playerArmor = user.getPlayerArmor();
+
+ final ArmorItem hat = playerArmor.getHat();
+ final ArmorItem backpack = playerArmor.getBackpack();
+
+ final ArmorItem.Type type = armorItem.getType();
+
+ final String id = switch (type) {
+ case HAT -> hat.getId();
+ case BACKPACK -> backpack.getId();
+ };
+
+ placeholders.put(
+ Placeholder.ENABLED,
+ String.valueOf(id.equals(armorItem.getId())).
+ toLowerCase(Locale.ROOT));
+
+ placeholders.put(
+ Placeholder.ALLOWED,
+ String.valueOf(
+ player.hasPermission(armorItem.getPermission())).
+ toUpperCase(Locale.ROOT)
+ );
+
+ this.gui.setItem(slot,
+ new GuiItem(
+ ItemBuilder.from(
+ armorItem.getItemStack()
+ ).namePlaceholders(placeholders).
+ lorePlaceholders(placeholders).
+ build(),
+ event -> {
+ final String permission = armorItem.getPermission();
+
+ if (permission != null &&
+ !permission.isBlank() &&
+ !player.hasPermission(armorItem.getPermission())) {
+ this.messageHandler.sendMessage(
+ player,
+ Messages.NO_PERMISSION
+ );
+ return;
+ }
+
+ this.setUserArmor(player, user, armorItem);
+ }
+ )
+ );
+
+ continue;
+ }
+
+ this.gui.setItem(slot, guiItem);
+ }
+ }
+
+ private void setUserArmor(
+ final HumanEntity player,
+ final User user,
+ final ArmorItem armorItem) {
+
+ if (player == null) {
+ return;
+ }
+
+ final ArmorItem.Type type = armorItem.getType();
+
+ switch (type) {
+ case HAT -> user.setOrUnsetHat(armorItem);
+ case BACKPACK -> user.setOrUnsetBackpack(armorItem);
+ }
+ }
+
+ public void open(final HumanEntity humanEntity) {
+ final Optional optionalUser = this.plugin.getUserManager().get(humanEntity.getUniqueId());
+
+ if (optionalUser.isEmpty()) {
+ return;
+ }
+
+ final User user = optionalUser.get();
+
+ this.gui = Gui.gui().
+ title(Adventure.MINI_MESSAGE.parse(this.title)).
+ rows(this.rows).
+ create();
+
+ this.gui.setDefaultClickAction(event -> {
+ event.setCancelled(true);
+ this.setItems(user);
+ this.gui.update();
+ });
+ this.setItems(user);
+
+ this.gui.open(humanEntity);
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/gui/CosmeticsMenu.java b/src/main/java/io/github/fisher2911/hmccosmetics/gui/CosmeticsMenu.java
new file mode 100644
index 00000000..50b77d59
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/gui/CosmeticsMenu.java
@@ -0,0 +1,89 @@
+package io.github.fisher2911.hmccosmetics.gui;
+
+import dev.triumphteam.gui.guis.GuiItem;
+import io.github.fisher2911.hmccosmetics.HMCCosmetics;
+import io.github.fisher2911.hmccosmetics.config.GuiSerializer;
+import io.github.fisher2911.hmccosmetics.config.ItemSerializer;
+import org.bukkit.entity.HumanEntity;
+import org.spongepowered.configurate.ConfigurateException;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+
+public class CosmeticsMenu {
+
+ public static final String MAIN_MENU = "main";
+
+ private final HMCCosmetics plugin;
+
+ private final Map guiMap = new HashMap<>();
+
+ public CosmeticsMenu(final HMCCosmetics plugin) {
+ this.plugin = plugin;
+ }
+
+ public void openMenu(final String id, final HumanEntity humanEntity) {
+ final CosmeticGui cosmeticGui = this.guiMap.get(id);
+
+ if (cosmeticGui != null) {
+ cosmeticGui.open(humanEntity);
+ }
+ }
+
+ public void openDefault(final HumanEntity humanEntity) {
+ this.openMenu(MAIN_MENU, humanEntity);
+ }
+
+ public void load() {
+ final File file = Path.of(this.plugin.getDataFolder().getPath(),
+ "menus").toFile();
+
+ if (!Path.of(this.plugin.getDataFolder().getPath(),
+ "menus",
+ "main").toFile().exists()) {
+ this.plugin.saveResource(
+ new File("menus", "main.yml").getPath(),
+ false
+ );
+ }
+
+ if (!file.exists() ||
+ !file.isDirectory()) {
+ return;
+ }
+
+ final File[] files = file.listFiles();
+
+ if (files == null) {
+ return;
+ }
+
+ for (final File guiFile : files) {
+ final String id = guiFile.getName().replace(".yml", "");
+
+ final YamlConfigurationLoader loader = YamlConfigurationLoader.
+ builder().
+ path(Path.of(guiFile.getPath())).
+ defaultOptions(opts ->
+ opts.serializers(build -> {
+ build.register(GuiItem.class, ItemSerializer.INSTANCE);
+ build.register(CosmeticGui.class, GuiSerializer.INSTANCE);
+ }))
+ .build();
+
+ try {
+ final ConfigurationNode source = loader.load();
+
+ this.guiMap.put(id, source.get(CosmeticGui.class));
+
+ } catch (final ConfigurateException exception) {
+ exception.printStackTrace();
+ }
+
+ }
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/inventory/PlayerArmor.java b/src/main/java/io/github/fisher2911/hmccosmetics/inventory/PlayerArmor.java
new file mode 100644
index 00000000..0f85a06a
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/inventory/PlayerArmor.java
@@ -0,0 +1,48 @@
+package io.github.fisher2911.hmccosmetics.inventory;
+
+import io.github.fisher2911.hmccosmetics.gui.ArmorItem;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+
+public class PlayerArmor {
+
+ private ArmorItem hat;
+ private ArmorItem backpack;
+
+ public PlayerArmor(final ArmorItem hat, final ArmorItem backpack) {
+ this.hat = hat;
+ this.backpack = backpack;
+ }
+
+ public static PlayerArmor empty() {
+ return new PlayerArmor(
+ new ArmorItem(
+ new ItemStack(Material.AIR),
+ "",
+ "",
+ ArmorItem.Type.HAT
+ ),
+ new ArmorItem(
+ new ItemStack(Material.AIR),
+ "",
+ "",
+ ArmorItem.Type.BACKPACK
+ ));
+ }
+
+ public ArmorItem getHat() {
+ return hat;
+ }
+
+ public void setHat(final ArmorItem hat) {
+ this.hat = hat;
+ }
+
+ public ArmorItem getBackpack() {
+ return backpack;
+ }
+
+ public void setBackpack(final ArmorItem backpack) {
+ this.backpack = backpack;
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/listener/JoinListener.java b/src/main/java/io/github/fisher2911/hmccosmetics/listener/JoinListener.java
new file mode 100644
index 00000000..f1c81910
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/listener/JoinListener.java
@@ -0,0 +1,29 @@
+package io.github.fisher2911.hmccosmetics.listener;
+
+import io.github.fisher2911.hmccosmetics.HMCCosmetics;
+import io.github.fisher2911.hmccosmetics.user.UserManager;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+
+public class JoinListener implements Listener {
+
+ private final HMCCosmetics plugin;
+ private final UserManager userManager;
+
+ public JoinListener(final HMCCosmetics plugin) {
+ this.plugin = plugin;
+ this.userManager = this.plugin.getUserManager();
+ }
+
+ @EventHandler
+ public void onJoin(final PlayerJoinEvent event) {
+ this.userManager.add(event.getPlayer());
+ }
+
+ @EventHandler
+ public void onQuit(final PlayerQuitEvent event) {
+ this.userManager.remove(event.getPlayer().getUniqueId());
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/message/Adventure.java b/src/main/java/io/github/fisher2911/hmccosmetics/message/Adventure.java
new file mode 100644
index 00000000..df99386e
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/message/Adventure.java
@@ -0,0 +1,24 @@
+package io.github.fisher2911.hmccosmetics.message;
+
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import net.kyori.adventure.text.minimessage.transformation.TransformationRegistry;
+import net.kyori.adventure.text.minimessage.transformation.TransformationType;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+
+public class Adventure {
+
+ public static final LegacyComponentSerializer SERIALIZER = LegacyComponentSerializer.builder()
+ .hexColors()
+ .useUnusualXRepeatedCharacterHexFormat()
+ .build();
+
+ public static final MiniMessage MINI_MESSAGE = MiniMessage.builder()
+ .transformations(TransformationRegistry.
+ builder().
+ add(TransformationType.CLICK_EVENT,
+ TransformationType.DECORATION,
+ TransformationType.COLOR
+ ).build())
+ .build();
+
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/message/ErrorMessages.java b/src/main/java/io/github/fisher2911/hmccosmetics/message/ErrorMessages.java
new file mode 100644
index 00000000..c5f06aac
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/message/ErrorMessages.java
@@ -0,0 +1,8 @@
+package io.github.fisher2911.hmccosmetics.message;
+
+public class ErrorMessages {
+
+ public static final String INVALID_ITEM = "%s is not a valid %s in file %s";
+ public static final String ITEM_NOT_FOUND = "%s was not found in file %s";
+
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/message/Message.java b/src/main/java/io/github/fisher2911/hmccosmetics/message/Message.java
new file mode 100644
index 00000000..6b4efc55
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/message/Message.java
@@ -0,0 +1,57 @@
+package io.github.fisher2911.hmccosmetics.message;
+
+import java.util.Objects;
+
+public class Message {
+
+ private final String key;
+ private final String message;
+ private final Type type;
+
+ public Message(final String key, final String message, final Type type) {
+ this.key = key;
+ this.message = message;
+ this.type = type;
+ }
+
+ public Message(final String key, final String message) {
+ this.message = message;
+ this.key = key;
+ this.type = Type.MESSAGE;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getMessage() {
+ return this.message;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ final Message message = (Message) o;
+ return Objects.equals(key, message.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(message);
+ }
+
+ public enum Type {
+
+ MESSAGE,
+
+ ACTION_BAR,
+
+ TITLE
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/message/MessageHandler.java b/src/main/java/io/github/fisher2911/hmccosmetics/message/MessageHandler.java
new file mode 100644
index 00000000..b635fe46
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/message/MessageHandler.java
@@ -0,0 +1,158 @@
+package io.github.fisher2911.hmccosmetics.message;
+
+import io.github.fisher2911.hmccosmetics.HMCCosmetics;
+import io.github.fisher2911.hmccosmetics.util.StringUtils;
+import io.github.fisher2911.hmccosmetics.util.Utils;
+import net.kyori.adventure.platform.bukkit.BukkitAudiences;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.title.Title;
+import org.bukkit.command.CommandSender;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.entity.Player;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+public class MessageHandler {
+
+ private final HMCCosmetics plugin;
+ private final Logger logger;
+ private final BukkitAudiences adventure;
+ private final Map messageMap = new HashMap<>();
+
+ public MessageHandler(final HMCCosmetics plugin) {
+ this.plugin = plugin;
+ this.logger = this.plugin.getLogger();
+ this.adventure = BukkitAudiences.create(this.plugin);
+ }
+
+
+ /**
+ * Closes adventure
+ */
+
+ public void close() {
+ adventure.close();
+ }
+
+ /**
+ *
+ * @param sender receiver of message
+ * @param key message key
+ * @param placeholders placeholders
+ */
+
+ public void sendMessage(final CommandSender sender, final Message key, final Map placeholders) {
+ final String message = StringUtils.applyPlaceholders(this.getMessage(key), placeholders);
+ final Component component = Adventure.MINI_MESSAGE.parse(message);
+ this.adventure.sender(sender).sendMessage(component);
+ }
+
+ /**
+ *
+ * @param sender receiver of message
+ * @param key message key
+ */
+
+ public void sendMessage(final CommandSender sender, final Message key) {
+ this.sendMessage(sender, key, Collections.emptyMap());
+ }
+
+ /**
+ *
+ * @param player receiver of message
+ * @param key message key
+ * @param placeholders placeholders
+ */
+
+ public void sendActionBar(final Player player, final Message key, final Map placeholders) {
+ final String message = StringUtils.applyPlaceholders(this.getMessage(key), placeholders);
+ Component component = Adventure.MINI_MESSAGE.parse(message);
+ this.adventure.player(player).sendActionBar(component);
+ }
+
+ /**
+ *
+ * @param player receiver of message
+ * @param key message key
+ */
+
+ public void sendActionBar(final Player player, final Message key) {
+ this.sendActionBar(player, key, Collections.emptyMap());
+ }
+
+ /**
+ *
+ * @param player receiver of message
+ * @param key message key
+ * @param placeholders placeholders
+ */
+
+ public void sendTitle(final Player player, final Message key, final Map placeholders) {
+ final String message = StringUtils.applyPlaceholders(this.getMessage(key), placeholders);
+ Component component = Adventure.MINI_MESSAGE.parse(message);
+ this.adventure.player(player).showTitle(Title.title(component, Component.empty()));
+ }
+
+ /**
+ *
+ * @param player receiver of message
+ * @param key message key
+ */
+
+ public void sendTitle(final Player player, final Message key) {
+ this.sendTitle(player, key, Collections.emptyMap());
+ }
+
+ /**
+ *
+ * @param key message key
+ * @return message, or empty string if message not found
+ */
+
+ public String getMessage(final Message key) {
+ return this.messageMap.getOrDefault(key.getKey(), key).getMessage();
+ }
+
+ /**
+ * Loads all messages from messages.yml
+ */
+
+ public void load() {
+ final String fileName = "messages.yml";
+
+ final File file = new File(this.plugin.getDataFolder(), fileName);
+
+ if (!file.exists()) {
+ this.plugin.saveResource(fileName, false);
+ }
+
+ final FileConfiguration config = YamlConfiguration.loadConfiguration(file);
+
+ String prefix = config.getString("prefix");
+
+ if (prefix == null) {
+ prefix = "";
+ }
+
+ for (final String key : config.getKeys(false)) {
+
+ final String message = Utils.replaceIfNull(config.getString(key), "", value -> {
+ if (value == null) {
+ this.logger.warning(String.format(ErrorMessages.ITEM_NOT_FOUND, "message", fileName));
+ }
+ }).replace(Placeholder.PREFIX, prefix);
+
+ final Message.Type messageType = Utils.stringToEnum(
+ Utils.replaceIfNull(config.getString("type"), "")
+ , Message.Type.class, Message.Type.MESSAGE
+ );
+
+ this.messageMap.put(key, new Message(key, message, messageType));
+ }
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/message/Messages.java b/src/main/java/io/github/fisher2911/hmccosmetics/message/Messages.java
new file mode 100644
index 00000000..f5255e62
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/message/Messages.java
@@ -0,0 +1,17 @@
+package io.github.fisher2911.hmccosmetics.message;
+
+public class Messages {
+
+ public static final Message NO_PERMISSION =
+ new Message("no-permission", "You do not have permission for this!");
+ public static final Message SET_HAT =
+ new Message("set-hat", "Set hat");
+ public static final Message REMOVED_HAT =
+ new Message("removed-hat", "Removed hat");
+ public static final Message SET_BACKPACK =
+ new Message("set-backpack", "Set backpack");
+ public static final Message REMOVED_BACKPACK =
+ new Message("removed-backpack", "Removed backpack");
+ public static final Message MUST_BE_PLAYER =
+ new Message("must-be-player", "You must be a player to do this!");
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/message/Permission.java b/src/main/java/io/github/fisher2911/hmccosmetics/message/Permission.java
new file mode 100644
index 00000000..498e9722
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/message/Permission.java
@@ -0,0 +1,7 @@
+package io.github.fisher2911.hmccosmetics.message;
+
+public class Permission {
+
+ public static final String DEFAULT_COMMAND = "hmccosmetics.cmd.default";
+
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/message/Placeholder.java b/src/main/java/io/github/fisher2911/hmccosmetics/message/Placeholder.java
new file mode 100644
index 00000000..b314a18b
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/message/Placeholder.java
@@ -0,0 +1,12 @@
+package io.github.fisher2911.hmccosmetics.message;
+
+public class Placeholder {
+
+ public static final String PREFIX = "%prefix%";
+ public static final String TYPE = "%type%";
+ public static final String ITEM = "%item%";
+ public static final String FILE = "%file%";
+
+ public static final String ENABLED = "%enabled%";
+ public static final String ALLOWED = "%allowed%";
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/user/User.java b/src/main/java/io/github/fisher2911/hmccosmetics/user/User.java
new file mode 100644
index 00000000..2ec08ec4
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/user/User.java
@@ -0,0 +1,124 @@
+package io.github.fisher2911.hmccosmetics.user;
+
+import io.github.fisher2911.hmccosmetics.gui.ArmorItem;
+import io.github.fisher2911.hmccosmetics.inventory.PlayerArmor;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.entity.ArmorStand;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.EntityEquipment;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.UUID;
+
+public class User {
+
+ private final UUID uuid;
+ private final PlayerArmor playerArmor;
+ private ArmorStand attached;
+
+ public User(final UUID uuid, final PlayerArmor playerArmor) {
+ this.uuid = uuid;
+ this.playerArmor = playerArmor;
+ }
+
+ public @Nullable Player getPlayer() {
+ return Bukkit.getPlayer(this.uuid);
+ }
+
+ public UUID getUuid() {
+ return uuid;
+ }
+
+ public PlayerArmor getPlayerArmor() {
+ return playerArmor;
+ }
+
+ public void setBackpack(final ArmorItem backpack) {
+ this.playerArmor.setBackpack(backpack);
+ }
+
+ public void setOrUnsetBackpack(final ArmorItem backpack) {
+ if (backpack.getId().equals(this.playerArmor.getBackpack().getId())) {
+ this.setBackpack(new ArmorItem(
+ new ItemStack(Material.AIR),
+ "",
+ "",
+ ArmorItem.Type.BACKPACK
+ ));
+ return;
+ }
+
+ this.setBackpack(backpack);
+ }
+
+
+ public void setHat(final ArmorItem hat) {
+ this.playerArmor.setHat(hat);
+ this.getPlayer().getEquipment().setHelmet(this.playerArmor.getHat().getItemStack());
+ }
+
+ public void setOrUnsetHat(final ArmorItem hat) {
+ if (hat.getId().equals(this.playerArmor.getHat().getId())) {
+ this.setHat(new ArmorItem(
+ new ItemStack(Material.AIR),
+ "",
+ "",
+ ArmorItem.Type.HAT
+ ));
+ return;
+ }
+
+ this.setHat(hat);
+ }
+
+ public void detach() {
+ if (this.attached != null) {
+ this.attached.remove();
+ }
+ }
+
+ // teleports armor stand to the correct position
+ // todo change to packets
+ public void updateArmorStand() {
+ final ArmorItem backpackArmorItem = this.playerArmor.getBackpack();
+ if (backpackArmorItem == null || backpackArmorItem.getItemStack().getType() == Material.AIR) {
+ return;
+ }
+
+ final ItemStack backpackItem = backpackArmorItem.getItemStack();
+
+
+ final Player player = this.getPlayer();
+
+ if (player == null) {
+ return;
+ }
+
+ if (this.attached == null) {
+ this.attached = player.getWorld().spawn(player.getLocation(),
+ ArmorStand.class,
+ armorStand -> {
+ armorStand.setVisible(false);
+ player.addPassenger(armorStand);
+ });
+ }
+
+ if (!player.getPassengers().contains(this.attached)) {
+ player.addPassenger(this.attached);
+ }
+
+ final EntityEquipment equipment = this.attached.getEquipment();
+
+ if (!backpackItem.equals(equipment.getChestplate())) {
+ equipment.setChestplate(backpackItem);
+ }
+
+ this.attached.
+ setRotation(
+ player.getLocation().getYaw(),
+ player.getLocation().getPitch());
+ }
+
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/user/UserManager.java b/src/main/java/io/github/fisher2911/hmccosmetics/user/UserManager.java
new file mode 100644
index 00000000..333f252d
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/user/UserManager.java
@@ -0,0 +1,67 @@
+package io.github.fisher2911.hmccosmetics.user;
+
+import io.github.fisher2911.hmccosmetics.HMCCosmetics;
+import io.github.fisher2911.hmccosmetics.gui.ArmorItem;
+import io.github.fisher2911.hmccosmetics.inventory.PlayerArmor;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.scheduler.BukkitTask;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+public class UserManager {
+
+ private final HMCCosmetics plugin;
+
+ private final Map userMap = new HashMap<>();
+
+ private BukkitTask teleportTask;
+
+ public UserManager(final HMCCosmetics plugin) {
+ this.plugin = plugin;
+ }
+
+ public void add(final Player player) {
+ final UUID uuid = player.getUniqueId();
+ this.userMap.put(uuid, new User(uuid, PlayerArmor.empty()));
+ }
+
+ public Optional get(final UUID uuid) {
+ return Optional.ofNullable(this.userMap.get(uuid));
+ }
+
+ public void remove(final UUID uuid) {
+ this.get(uuid).ifPresent(User::detach);
+ this.userMap.remove(uuid);
+ }
+
+ public void startTeleportTask() {
+ this.teleportTask = Bukkit.getScheduler().runTaskTimer(
+ this.plugin,
+ () -> this.userMap.values().forEach(
+ User::updateArmorStand
+ ),
+ 1,
+ 1
+ );
+ }
+
+ public void removeAll() {
+ for (final var user : this.userMap.values()) {
+ user.detach();
+ }
+
+ this.userMap.clear();
+ }
+
+ public void cancelTeleportTask() {
+ this.teleportTask.cancel();
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/util/StringUtils.java b/src/main/java/io/github/fisher2911/hmccosmetics/util/StringUtils.java
new file mode 100644
index 00000000..8d343297
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/util/StringUtils.java
@@ -0,0 +1,38 @@
+package io.github.fisher2911.hmccosmetics.util;
+
+import io.github.fisher2911.hmccosmetics.message.Adventure;
+import net.kyori.adventure.text.Component;
+
+import java.util.Map;
+
+public class StringUtils {
+
+ /**
+ *
+ * @param message message being translated
+ * @param placeholders placeholders applied
+ * @return message with placeholders applied
+ */
+
+ public static String applyPlaceholders(String message, final Map placeholders) {
+ for (final Map.Entry entry : placeholders.entrySet()) {
+ message = message.replace(entry.getKey(), entry.getValue());
+ }
+ return message;
+ }
+
+
+ /**
+ *
+ * @param parsed message to be parsed
+ * @return MiniMessage parsed string
+ */
+
+ public static Component parse(final String parsed) {
+ return Adventure.MINI_MESSAGE.parse(parsed);
+ }
+
+ public static String parseStringToString(final String parsed) {
+ return Adventure.SERIALIZER.serialize(Adventure.MINI_MESSAGE.parse(parsed));
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/util/Utils.java b/src/main/java/io/github/fisher2911/hmccosmetics/util/Utils.java
new file mode 100644
index 00000000..2aa29606
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/util/Utils.java
@@ -0,0 +1,105 @@
+package io.github.fisher2911.hmccosmetics.util;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public class Utils {
+
+ /**
+ * @param original Object to be checked if null
+ * @param replacement Object returned if original is null
+ * @return original if not null, otherwise replacement
+ */
+
+ public static T replaceIfNull(final @Nullable T original, final @NotNull T replacement) {
+ return replaceIfNull(original, replacement, t -> {});
+ }
+
+ /**
+ *
+ * @param original Object to be checked if null
+ * @param replacement Object returned if original is null
+ * @param consumer accepts the original object, can be used for logging
+ * @return original if not null, otherwise replacement
+ */
+
+ public static T replaceIfNull(final @Nullable T original, final T replacement, final @NotNull Consumer consumer) {
+ if (original == null) {
+ consumer.accept(replacement);
+ return replacement;
+ }
+ consumer.accept(original);
+ return original;
+ }
+
+ /**
+ *
+ * @param t object being checked
+ * @param consumer accepted if t is not null
+ * @param type
+ */
+
+ public static void doIfNotNull(final @Nullable T t, final @NotNull Consumer consumer) {
+ if (t == null) {
+ return;
+ }
+ consumer.accept(t);
+ }
+
+ /**
+ *
+ * @param t object being checked
+ * @param function applied if t is not null
+ * @param type
+ * @return
+ */
+
+ public static Optional returnIfNotNull(final @Nullable T t, final @NotNull Function function) {
+ if (t == null) {
+ return Optional.empty();
+ }
+ return Optional.of(function.apply(t));
+ }
+
+ /**
+ *
+ * @param enumAsString Enum value as a string to be parsed
+ * @param enumClass enum type enumAsString is to be converted to
+ * @param defaultEnum default value to be returned
+ * @return enumAsString as an enum, or default enum if it could not be parsed
+ */
+
+ public static > E stringToEnum(final @NotNull String enumAsString,
+ final @NotNull Class enumClass,
+ E defaultEnum) {
+ return stringToEnum(enumAsString, enumClass, defaultEnum, e -> {});
+ }
+
+ /**
+ *
+ * @param enumAsString Enum value as a string to be parsed
+ * @param enumClass enum type enumAsString is to be converted to
+ * @param defaultEnum default value to be returned
+ * @param consumer accepts the returned enum, can be used for logging
+ * @return enumAsString as an enum, or default enum if it could not be parsed
+ */
+
+ public static > E stringToEnum(final @NotNull String enumAsString,
+ @NotNull final Class enumClass,
+ final E defaultEnum,
+ final @NotNull Consumer consumer) {
+ try {
+ final E value = Enum.valueOf(enumClass, enumAsString);
+ consumer.accept(value);
+ return value;
+ } catch (final IllegalArgumentException exception) {
+ consumer.accept(defaultEnum);
+ return defaultEnum;
+ }
+ }
+
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/util/builder/ItemBuilder.java b/src/main/java/io/github/fisher2911/hmccosmetics/util/builder/ItemBuilder.java
new file mode 100644
index 00000000..c0a27427
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/util/builder/ItemBuilder.java
@@ -0,0 +1,189 @@
+package io.github.fisher2911.hmccosmetics.util.builder;
+
+import io.github.fisher2911.hmccosmetics.message.Adventure;
+import io.github.fisher2911.hmccosmetics.util.StringUtils;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ItemBuilder {
+
+ protected Material material;
+ protected int amount;
+ protected ItemMeta itemMeta;
+
+ /**
+ * @param material builder material
+ */
+
+ ItemBuilder(final Material material) {
+ this.material = material;
+ this.itemMeta = Bukkit.getItemFactory().getItemMeta(material);
+ }
+
+ /**
+ * @param itemStack builder ItemStack
+ */
+
+ ItemBuilder(final ItemStack itemStack) {
+ this.material = itemStack.getType();
+ this.itemMeta = itemStack.hasItemMeta() ? itemStack.getItemMeta() : Bukkit.getItemFactory().getItemMeta(this.material);
+ }
+
+ /**
+ * @param material builder material
+ * @return
+ */
+
+ public static ItemBuilder from(final Material material) {
+ return new ItemBuilder(material);
+ }
+
+ /**
+ * @param itemStack builder ItemStack
+ * @return
+ */
+
+ public static ItemBuilder from(final ItemStack itemStack) {
+ return new ItemBuilder(itemStack);
+ }
+
+ /**
+ * @param amount ItemStack amount
+ * @return this
+ */
+
+ public ItemBuilder amount(final int amount) {
+ this.amount = Math.min(Math.max(1, amount), 64);
+ return this;
+ }
+
+ /**
+ * @param name ItemStack name
+ * @return this
+ */
+
+ public ItemBuilder name(final String name) {
+ this.itemMeta.setDisplayName(name);
+ return this;
+ }
+
+ /**
+ * Sets placeholders to the item's name
+ *
+ * @param placeholders placeholders
+ */
+
+ public ItemBuilder namePlaceholders(final Map placeholders) {
+ final String name = StringUtils.
+ applyPlaceholders(this.itemMeta.getDisplayName(), placeholders);
+ this.itemMeta.displayName(
+ Adventure.MINI_MESSAGE.parse(
+ name));
+ return this;
+ }
+
+ /**
+ * @param lore ItemStack lore
+ * @return this
+ */
+
+ public ItemBuilder lore(final List lore) {
+ this.itemMeta.setLore(lore);
+ return this;
+ }
+
+ /**
+ * Sets placeholders to the item's lore
+ *
+ * @param placeholders placeholders
+ */
+
+
+ public ItemBuilder lorePlaceholders(final Map placeholders) {
+ final List lore = new ArrayList<>();
+
+ final List previousLore = this.itemMeta.getLore();
+
+ if (previousLore == null) {
+ return this;
+ }
+
+ for (final String line : previousLore) {
+ lore.add(StringUtils.applyPlaceholders(
+ line, placeholders
+ ));
+ }
+
+ this.itemMeta.setLore(lore);
+ return this;
+ }
+
+ /**
+ * @param unbreakable whether the ItemStack is unbreakable
+ * @return this
+ */
+
+ public ItemBuilder unbreakable(final boolean unbreakable) {
+ this.itemMeta.setUnbreakable(unbreakable);
+ return this;
+ }
+
+ public ItemBuilder glow(final boolean glow) {
+ if (glow) {
+ this.itemMeta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
+ this.itemMeta.addEnchant(Enchantment.LUCK, 1, true);
+ }
+ return this;
+ }
+
+ /**
+ * @param enchantments enchants to be added to the ItemStack
+ * @param ignoreLeveLRestrictions whether to ignore enchantment level restrictions
+ * @return this
+ */
+
+ public ItemBuilder enchants(final Map enchantments, boolean ignoreLeveLRestrictions) {
+ enchantments.forEach((enchantment, level) -> this.itemMeta.addEnchant(enchantment, level, ignoreLeveLRestrictions));
+ return this;
+ }
+
+ /**
+ * @param itemFlags ItemStack ItemFlags
+ * @return this
+ */
+
+ public ItemBuilder itemFlags(final Set itemFlags) {
+ this.itemMeta.addItemFlags(itemFlags.toArray(new ItemFlag[0]));
+ return this;
+ }
+
+ /**
+ * @param modelData ItemStack modelData
+ * @return this
+ */
+
+ public ItemBuilder modelData(final int modelData) {
+ this.itemMeta.setCustomModelData(modelData);
+ return this;
+ }
+
+ /**
+ * @return built ItemStack
+ */
+
+ public ItemStack build() {
+ final ItemStack itemStack = new ItemStack(this.material, Math.max(this.amount, 1));
+ itemStack.setItemMeta(itemMeta);
+ return itemStack;
+ }
+
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/util/builder/LeatherArmorBuilder.java b/src/main/java/io/github/fisher2911/hmccosmetics/util/builder/LeatherArmorBuilder.java
new file mode 100644
index 00000000..62021832
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/util/builder/LeatherArmorBuilder.java
@@ -0,0 +1,86 @@
+package io.github.fisher2911.hmccosmetics.util.builder;
+
+import org.bukkit.Color;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.LeatherArmorMeta;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public class LeatherArmorBuilder extends ItemBuilder{
+
+ private static final Set VALID_ARMOR = EnumSet.of(Material.LEATHER_BOOTS,
+ Material.LEATHER_LEGGINGS, Material.LEATHER_CHESTPLATE, Material.LEATHER_HELMET);
+
+ /**
+ *
+ * @param material ItemStack material
+ */
+
+ LeatherArmorBuilder(final Material material) {
+ super(material);
+ }
+
+ /**
+ *
+ * @param itemStack ItemStack
+ */
+
+ LeatherArmorBuilder(final ItemStack itemStack) {
+ super(itemStack);
+ }
+
+ /**
+ *
+ * @param material ItemStack material
+ * @return this
+ * @throws IllegalArgumentException thrown if material is not leather armor
+ */
+
+ public static LeatherArmorBuilder from(final Material material) throws IllegalArgumentException {
+ if (!VALID_ARMOR.contains(material)) {
+ throw new IllegalArgumentException(material.name() + " is not leather armor!");
+ }
+ return new LeatherArmorBuilder(material);
+ }
+
+ /**
+ *
+ * @param itemStack ItemStack
+ * @return this
+ * @throws IllegalArgumentException thrown if itemStack's type is not leather armor
+ */
+
+ public static LeatherArmorBuilder from(final ItemStack itemStack) throws IllegalArgumentException {
+ final Material material = itemStack.getType();
+ if (!VALID_ARMOR.contains(material)) {
+ throw new IllegalArgumentException(material.name() + " is not leather armor!");
+ }
+ return new LeatherArmorBuilder(itemStack);
+ }
+
+ /**
+ *
+ * @param color armor color
+ * @return this
+ */
+
+ public LeatherArmorBuilder color(final Color color) {
+ if (itemMeta instanceof final LeatherArmorMeta meta) {
+ meta.setColor(color);
+ this.itemMeta = meta;
+ }
+ return this;
+ }
+
+ /**
+ *
+ * @param material checked material
+ * @return true if is leather armor, else false
+ */
+
+ public static boolean isLeatherArmor(final Material material) {
+ return VALID_ARMOR.contains(material);
+ }
+}
diff --git a/src/main/java/io/github/fisher2911/hmccosmetics/util/builder/SkullBuilder.java b/src/main/java/io/github/fisher2911/hmccosmetics/util/builder/SkullBuilder.java
new file mode 100644
index 00000000..1745e346
--- /dev/null
+++ b/src/main/java/io/github/fisher2911/hmccosmetics/util/builder/SkullBuilder.java
@@ -0,0 +1,95 @@
+package io.github.fisher2911.hmccosmetics.util.builder;
+
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.properties.Property;
+import org.bukkit.Material;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.SkullMeta;
+import org.jetbrains.annotations.NotNull;
+
+import java.lang.reflect.Field;
+import java.util.UUID;
+
+/**
+ * Some parts taken from https://github.com/TriumphTeam/triumph-gui/blob/master/core/src/main/java/dev/triumphteam/gui/builder/item/SkullBuilder.java
+ */
+
+public class SkullBuilder extends ItemBuilder {
+
+ private static final Field PROFILE_FIELD;
+
+ static {
+ Field field;
+
+ try {
+ final SkullMeta skullMeta = (SkullMeta) new ItemStack(Material.PLAYER_HEAD).getItemMeta();
+ field = skullMeta.getClass().getDeclaredField("profile");
+ field.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ field = null;
+ }
+
+ PROFILE_FIELD = field;
+ }
+
+
+ /**
+ *
+ * @param material The material
+ */
+
+ SkullBuilder(final Material material) {
+ super(material);
+ }
+
+ /**
+ * Creates a new SkullBuilder instance
+ * @return this
+ */
+
+ public static SkullBuilder create() {
+ return new SkullBuilder(Material.PLAYER_HEAD);
+ }
+
+
+ /**
+ *
+ * @param player skull owner
+ * @return this
+ */
+ public SkullBuilder owner(final OfflinePlayer player) {
+ if (this.itemMeta instanceof final SkullMeta skullMeta) {
+ skullMeta.setOwningPlayer(player);
+ this.itemMeta = skullMeta;
+ }
+ return this;
+ }
+
+ /**
+ *
+ * @param texture skull texture
+ * @return this
+ */
+
+ public SkullBuilder texture(@NotNull final String texture) {
+ if (PROFILE_FIELD == null) {
+ return this;
+ }
+
+ final SkullMeta skullMeta = (SkullMeta) this.itemMeta;
+ final GameProfile profile = new GameProfile(UUID.randomUUID(), null);
+ profile.getProperties().put("textures", new Property("textures", texture));
+
+ try {
+ PROFILE_FIELD.set(skullMeta, profile);
+ } catch (IllegalArgumentException | IllegalAccessException ex) {
+ ex.printStackTrace();
+ }
+
+ this.itemMeta = skullMeta;
+ return this;
+ }
+
+}
diff --git a/src/main/resources/menus/main.yml b/src/main/resources/menus/main.yml
new file mode 100644
index 00000000..5faddfba
--- /dev/null
+++ b/src/main/resources/menus/main.yml
@@ -0,0 +1,47 @@
+title: "Main"
+rows: 3
+items:
+ 10:
+ material: NETHERITE_CHESTPLATE
+ name: "Backpack"
+ lore:
+ - ""
+ - "Enabled: %enabled%"
+ - "Allowed: %allowed%"
+ amount: 1
+ type: BACKPACK
+ permission: ""
+ id: netherite_backpack
+ 11:
+ material: NETHERITE_HELMET
+ name: "Hat"
+ lore:
+ - ""
+ - "Enabled: %enabled%"
+ - "Allowed: %allowed%"
+ amount: 1
+ type: HAT
+ permission: ""
+ id: netherite_hat
+ 12:
+ material: DIAMOND_CHESTPLATE
+ name: "Backpack"
+ lore:
+ - ""
+ - "Enabled: %enabled%"
+ - "Allowed: %allowed%"
+ amount: 1
+ type: BACKPACK
+ permission: ""
+ id: diamond_backpack
+ 13:
+ material: DIAMOND_HELMET
+ name: "Hat "
+ lore:
+ - ""
+ - "Enabled: %enabled%"
+ - "Allowed: %allowed%"
+ amount: 1
+ type: HAT
+ permission: ""
+ id: diamond_hat
diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml
new file mode 100644
index 00000000..d8f6fa31
--- /dev/null
+++ b/src/main/resources/messages.yml
@@ -0,0 +1,6 @@
+no-permission: "No Permission!"
+set-hat: "Set Hat!"
+removed-hat: "Removed Hat"
+set-backpack: "Set Backpack!"
+removed-backpack: "Removed Backpack"
+must-be-player: "You must be a player to do this!"
\ No newline at end of file
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
new file mode 100644
index 00000000..3b181880
--- /dev/null
+++ b/src/main/resources/plugin.yml
@@ -0,0 +1,8 @@
+name: HMCCosmetics
+main: io.github.fisher2911.hmccosmetics.HMCCosmetics
+version: 1.0.0
+api-version: 1.17
+permissions:
+ hmccosmetics.cmd.default:
+ default: op
+ description: Permission to execute the default command
\ No newline at end of file