first commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
.gradle
|
||||||
|
.idea
|
||||||
|
build
|
||||||
|
run
|
||||||
|
backend
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 WinniePatGG
|
||||||
|
|
||||||
|
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.
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
plugins {
|
||||||
|
id 'fabric-loom' version '1.8-SNAPSHOT'
|
||||||
|
id 'maven-publish'
|
||||||
|
id 'java'
|
||||||
|
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
version = project.mod_version
|
||||||
|
group = project.maven_group
|
||||||
|
|
||||||
|
base {
|
||||||
|
archivesName = project.archives_base_name
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
maven { url = "https://maven.fabricmc.net/" }
|
||||||
|
maven { url = 'https://maven.wispforest.io/releases/' }
|
||||||
|
maven { url = 'https://repo.u-team.info' }
|
||||||
|
maven { url = 'https://jitpack.io' }
|
||||||
|
maven { url = "https://maven.terraformersmc.com/releases/" }
|
||||||
|
maven { url = "https://maven.izzel.io" }
|
||||||
|
maven { url = "https://raw.githubusercontent.com/Fuzss/modresources/main/maven/" }
|
||||||
|
maven { url "https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/" }
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
minecraft "com.mojang:minecraft:1.21.1"
|
||||||
|
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
|
||||||
|
|
||||||
|
modImplementation "net.fabricmc:fabric-loader:0.16.14"
|
||||||
|
modImplementation "net.fabricmc.fabric-api:fabric-api:0.105.0+1.21.1"
|
||||||
|
modImplementation "io.wispforest:owo-lib:0.12.15.4+1.21"
|
||||||
|
|
||||||
|
include(implementation("org.reflections:reflections:0.10.2"))
|
||||||
|
include(implementation("com.google.guava:guava:32.1.2-jre"))
|
||||||
|
include(implementation("org.javassist:javassist:3.29.2-GA"))
|
||||||
|
include(implementation("org.slf4j:slf4j-api:2.0.9"))
|
||||||
|
include(implementation("org.apache.httpcomponents:httpclient:4.5.13"))
|
||||||
|
include(implementation("com.google.code.gson:gson:2.10.1"))
|
||||||
|
include(implementation("io.socket:socket.io-client:2.1.0"))
|
||||||
|
|
||||||
|
implementation "com.github.JnCrMx:discord-game-sdk4j:v0.5.5"
|
||||||
|
include(implementation("net.hycrafthd:minecraft_authenticator:3.0.6"))
|
||||||
|
|
||||||
|
implementation "io.github.jaredmdobson:concentus:1.0.2"
|
||||||
|
implementation "org.java-websocket:Java-WebSocket:1.5.6"
|
||||||
|
|
||||||
|
implementation "icyllis.modernui:ModernUI-Core:3.12.0"
|
||||||
|
implementation "icyllis.modernui:ModernUI-Markflow:3.12.0"
|
||||||
|
modImplementation("icyllis.modernui:ModernUI-Fabric:1.21.1-3.12.0.2")
|
||||||
|
modImplementation "fuzs.forgeconfigapiport:forgeconfigapiport-fabric:21.1.0"
|
||||||
|
|
||||||
|
modImplementation "software.bernie.geckolib:geckolib-fabric-1.21:4.5.8"
|
||||||
|
|
||||||
|
modImplementation "com.terraformersmc:modmenu:11.0.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
inputs.property "version", "1.0.0"
|
||||||
|
inputs.property "loader_version", "0.16.14"
|
||||||
|
inputs.property "minecraft_version", "1.21.1"
|
||||||
|
|
||||||
|
filesMatching("fabric.mod.json") {
|
||||||
|
expand(
|
||||||
|
"version": "1.0.0",
|
||||||
|
"loader_version": "0.16.14",
|
||||||
|
"minecraft_version": "1.21.1"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def targetJavaVersion = 21
|
||||||
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
it.options.encoding = "UTF-8"
|
||||||
|
if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
|
||||||
|
it.options.release.set(targetJavaVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
def javaVersion = JavaVersion.toVersion(targetJavaVersion)
|
||||||
|
if (JavaVersion.current() < javaVersion) {
|
||||||
|
toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
|
||||||
|
}
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
remapJar {
|
||||||
|
archiveClassifier.set("")
|
||||||
|
}
|
||||||
|
build.dependsOn remapJar
|
||||||
|
|
||||||
|
jar {
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
java {
|
||||||
|
srcDirs = ['src/main/java', 'src/main/kotlin']
|
||||||
|
}
|
||||||
|
resources {
|
||||||
|
srcDirs = ['src/main/resources']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loom {
|
||||||
|
mixin {
|
||||||
|
defaultRefmapName = "citrus.refmap.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
create("mavenJava", MavenPublication) {
|
||||||
|
artifactId = project.archives_base_name
|
||||||
|
from components.java
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Done to increase the memory available to Gradle.
|
||||||
|
org.gradle.jvmargs=-Xmx16G
|
||||||
|
|
||||||
|
# Fabric Properties
|
||||||
|
# check these on https://modmuss50.me/fabric.html
|
||||||
|
minecraft_version=1.21.1
|
||||||
|
yarn_mappings=1.21.1+build.3
|
||||||
|
loader_version=0.18.4
|
||||||
|
|
||||||
|
owo_version=0.12.15.4+1.21
|
||||||
|
|
||||||
|
# Mod Properties
|
||||||
|
mod_version = 0.1
|
||||||
|
maven_group = de.winniepat.citrus
|
||||||
|
archives_base_name = Citrus
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
# check this on https://modmuss50.me/fabric.html
|
||||||
|
fabric_version=0.105.0+1.21.1
|
||||||
Vendored
BIN
Binary file not shown.
+7
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original 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.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# 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
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# 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"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
Vendored
+94
@@ -0,0 +1,94 @@
|
|||||||
|
@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
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@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=.
|
||||||
|
@rem This is normally unused
|
||||||
|
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% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
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% equ 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!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url = "https://maven.fabricmc.net/" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "Citrus"
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package de.winniepat.citrus;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.beta.*;
|
||||||
|
import de.winniepat.citrus.cosmetics.*;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.*;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.sync.*;
|
||||||
|
import de.winniepat.citrus.client.*;
|
||||||
|
import de.winniepat.citrus.cosmetics.halo.HaloFeatureRenderer;
|
||||||
|
import de.winniepat.citrus.cosmetics.hat.HatFeatureRenderer;
|
||||||
|
import de.winniepat.citrus.cosmetics.wings.WingsFeatureRenderer;
|
||||||
|
import de.winniepat.citrus.cosmetics.wings.sync.*;
|
||||||
|
import de.winniepat.citrus.draggui.*;
|
||||||
|
import de.winniepat.citrus.gui.*;
|
||||||
|
import de.winniepat.citrus.managers.*;
|
||||||
|
import de.winniepat.citrus.utils.*;
|
||||||
|
import icyllis.modernui.mc.MuiModApi;
|
||||||
|
import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityFeatureRendererRegistrationCallback;
|
||||||
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
|
import net.minecraft.client.render.entity.PlayerEntityRenderer;
|
||||||
|
import org.slf4j.*;
|
||||||
|
|
||||||
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.*;
|
||||||
|
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
|
||||||
|
public class Client {
|
||||||
|
|
||||||
|
public static final Logger logger = LoggerFactory.getLogger(Client.class);
|
||||||
|
|
||||||
|
public static final String CLIENT_BUILD = "0.1";
|
||||||
|
public static final String clientInfo = "Citrus 1.21-" + CLIENT_BUILD + " | Minecraft 1.21";
|
||||||
|
|
||||||
|
public static final File CONFIG_DIR = new File(FabricLoader.getInstance().getGameDir().toFile(),"citrus");
|
||||||
|
|
||||||
|
public static final boolean debug = true;
|
||||||
|
public static final boolean betaFeaturesEnabled = false;
|
||||||
|
|
||||||
|
public static final String API_URL = "https://api.citrus-client.de";
|
||||||
|
public static final String CDN_URL = "https://cdn.citrus-client.de";
|
||||||
|
public static final String BETA_URL = "https://beta.citrus-client.de";
|
||||||
|
|
||||||
|
private static BetaManager betaManager;
|
||||||
|
|
||||||
|
protected void init() throws IOException {
|
||||||
|
|
||||||
|
betaManager = BetaManager.getInstance();
|
||||||
|
|
||||||
|
betaManager.initialize();
|
||||||
|
|
||||||
|
ClientCommands.register();
|
||||||
|
ClientKeybinds.register();
|
||||||
|
CosmeticCommands.register();
|
||||||
|
|
||||||
|
CapeHandler.init();
|
||||||
|
ConfigManager.load();
|
||||||
|
HudConfig.load();
|
||||||
|
HudRenderer.register();
|
||||||
|
TestScreen.registerRenderer();
|
||||||
|
|
||||||
|
CapeSyncManager.init();
|
||||||
|
WingSyncManager.init();
|
||||||
|
WingSyncManager.startAutoSync();
|
||||||
|
WallpaperManager.init();
|
||||||
|
|
||||||
|
ClientRegistrationManager.register();
|
||||||
|
FriendManager.init();
|
||||||
|
|
||||||
|
ChatSound.init();
|
||||||
|
|
||||||
|
debugLog(SecurityUtils.getDailyKey());
|
||||||
|
|
||||||
|
RefreshUtil.start();
|
||||||
|
|
||||||
|
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
|
||||||
|
ClientRegistrationManager.register();
|
||||||
|
client.execute(FriendManager::updateServerStatus);
|
||||||
|
});
|
||||||
|
|
||||||
|
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> FriendManager.updateServerStatus());
|
||||||
|
|
||||||
|
ClientLifecycleEvents.CLIENT_STOPPING.register(client -> ClientRegistrationManager.unregister());
|
||||||
|
|
||||||
|
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||||
|
if (client.world == null || client.player == null) return;
|
||||||
|
|
||||||
|
PlayerCapeTracker.tick();
|
||||||
|
|
||||||
|
while (ClientKeybinds.OPEN_CAPE_SCREEN.wasPressed() && client.player != null) {
|
||||||
|
CapeHandler.reloadCapes();
|
||||||
|
client.setScreen(new CapeSelectionScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ClientKeybinds.OPEN_COSMETICS_SCREEN.wasPressed() && client.player != null) {
|
||||||
|
client.setScreen(new CosmeticScreen());
|
||||||
|
|
||||||
|
if (client.world != null || client.player != null){
|
||||||
|
PlayerCosmeticTracker.tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ClientKeybinds.OPEN_FRIENDS_SCREEN.wasPressed() && client.player != null) {
|
||||||
|
FriendManager.refresh();
|
||||||
|
client.setScreen(MuiModApi.get().createScreen(new FriendsFragment(), null, client.currentScreen));
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ClientKeybinds.OPEN_DRAGGUI.wasPressed()) {
|
||||||
|
client.setScreen(MuiModApi.get().createScreen(new HudEditorFragment(), null, client.currentScreen));
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ClientKeybinds.REFRESH_CLIENT.wasPressed() && client.player != null) {
|
||||||
|
RankManager.refreshRankIfChanged();
|
||||||
|
CapeHandler.reloadCapes();
|
||||||
|
CapeHandler.retryFailedCapes();
|
||||||
|
assert MinecraftClient.getInstance().player != null;
|
||||||
|
MinecraftClient.getInstance().player.sendMessage(Text.literal("Refreshed Client"), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ClientKeybinds.TOGGLE_FULLBRIGHT.wasPressed()) {
|
||||||
|
FullbrightManager.toggle(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ClientKeybinds.OPEN_MODULETOGGLE.wasPressed()) {
|
||||||
|
client.setScreen(new ModuleToggleScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ClientKeybinds.REFRESH_CLIENT.wasPressed()) {
|
||||||
|
ClientRegistrationManager.forceUpdateOnlineUsers();
|
||||||
|
CapeHandler.refreshCapeList();
|
||||||
|
CapeHandler.retryFailedCapes();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.world != null && client.player != null) {
|
||||||
|
PlayerWingTracker.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
ZoomManager.setZooming(ClientKeybinds.ZOOM_KEY.isPressed());
|
||||||
|
});
|
||||||
|
|
||||||
|
LivingEntityFeatureRendererRegistrationCallback.EVENT.register((entityType, entityRenderer, registrationHelper, context) -> {
|
||||||
|
if (entityRenderer instanceof PlayerEntityRenderer playerRenderer) {
|
||||||
|
registrationHelper.register(new WingsFeatureRenderer(playerRenderer));
|
||||||
|
registrationHelper.register(new HatFeatureRenderer(playerRenderer));
|
||||||
|
registrationHelper.register(new HaloFeatureRenderer(playerRenderer));
|
||||||
|
|
||||||
|
Client.debugLog("Registered cosmetic renderers for player");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
CosmeticSyncManager.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void debugLog(String message) {
|
||||||
|
if (debug) {
|
||||||
|
logger.info("[Citrus] [DEBUG] {}", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BetaManager getBetaManager() {
|
||||||
|
return betaManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package de.winniepat.citrus;
|
||||||
|
|
||||||
|
import net.fabricmc.api.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ClientEntrypoint implements ModInitializer, ClientModInitializer {
|
||||||
|
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* Hier nichts reinschreiben, sonst gibt es Cockschelle!
|
||||||
|
* Wenn ihr was initializen wollt macht das in der Instance direkt:
|
||||||
|
* Client.java in der init()
|
||||||
|
*
|
||||||
|
* Hier ist nur Client instance!!
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
private static Client client;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInitialize() {
|
||||||
|
System.out.println("Mod initialized");
|
||||||
|
client = new Client();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInitializeClient() {
|
||||||
|
try {
|
||||||
|
client.init();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
System.out.println("Client initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Client getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package de.winniepat.citrus;
|
||||||
|
|
||||||
|
public class ModConfig {
|
||||||
|
public boolean fullbrightEnabled = false;
|
||||||
|
public String mainmenuWallpaper = "default.png";
|
||||||
|
|
||||||
|
public BetaConfig betaConfig = new BetaConfig();
|
||||||
|
|
||||||
|
public static class BetaConfig {
|
||||||
|
public String betaKey = "";
|
||||||
|
public String verificationToken = "";
|
||||||
|
public long tokenExpiresAt = 0;
|
||||||
|
public boolean firstLaunch = true;
|
||||||
|
public String apiKey = "ClientF8CjVGn7VTOAhROa65Ot";
|
||||||
|
public String apiKeyProd = "";
|
||||||
|
|
||||||
|
public BetaConfig() {}
|
||||||
|
|
||||||
|
public String getActiveApiKey() {
|
||||||
|
return (apiKeyProd != null && !apiKeyProd.trim().isEmpty()) ?
|
||||||
|
apiKeyProd : apiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModConfig() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
package de.winniepat.citrus.beta;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.gui.MainMenuScreen;
|
||||||
|
import de.winniepat.citrus.managers.ConfigManager;
|
||||||
|
import de.winniepat.citrus.ModConfig;
|
||||||
|
import icyllis.modernui.mc.MuiModApi;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.Screen;
|
||||||
|
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||||
|
import net.minecraft.client.gui.widget.TextFieldWidget;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class BetaKeyScreen extends Screen {
|
||||||
|
private TextFieldWidget keyField;
|
||||||
|
private ButtonWidget verifyButton;
|
||||||
|
|
||||||
|
private String statusMessage = "";
|
||||||
|
private int statusColor = 0xFFFFFF;
|
||||||
|
private boolean isVerifying = false;
|
||||||
|
|
||||||
|
private String linkedUsername = null;
|
||||||
|
|
||||||
|
public BetaKeyScreen() {
|
||||||
|
super(Text.literal("Citrus Beta Program"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
super.init();
|
||||||
|
|
||||||
|
int centerX = width / 2;
|
||||||
|
int centerY = height / 2;
|
||||||
|
|
||||||
|
keyField = new TextFieldWidget(
|
||||||
|
textRenderer,
|
||||||
|
centerX - 150,
|
||||||
|
centerY - 30,
|
||||||
|
220,
|
||||||
|
20,
|
||||||
|
Text.literal("Enter Beta Key")
|
||||||
|
);
|
||||||
|
keyField.setMaxLength(20);
|
||||||
|
keyField.setChangedListener(text -> updateButtonState());
|
||||||
|
|
||||||
|
ModConfig.BetaConfig betaConfig = ConfigManager.getBetaConfig();
|
||||||
|
if (betaConfig != null && !betaConfig.betaKey.isEmpty()) {
|
||||||
|
keyField.setText(betaConfig.betaKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
addDrawableChild(keyField);
|
||||||
|
|
||||||
|
ButtonWidget pasteButton = ButtonWidget.builder(
|
||||||
|
Text.literal("Paste"),
|
||||||
|
button -> pasteFromClipboard()
|
||||||
|
)
|
||||||
|
.dimensions(centerX + 75, centerY - 30, 75, 20)
|
||||||
|
.build();
|
||||||
|
addDrawableChild(pasteButton);
|
||||||
|
|
||||||
|
verifyButton = ButtonWidget.builder(
|
||||||
|
Text.literal("Verify Key"),
|
||||||
|
button -> verifyKey()
|
||||||
|
)
|
||||||
|
.dimensions(centerX - 150, centerY + 10, 145, 20)
|
||||||
|
.build();
|
||||||
|
addDrawableChild(verifyButton);
|
||||||
|
|
||||||
|
ButtonWidget getKeyButton = ButtonWidget.builder(
|
||||||
|
Text.literal("Get Beta Key"),
|
||||||
|
button -> Util.getOperatingSystem().open("https://dc.citrus-client.de")
|
||||||
|
)
|
||||||
|
.dimensions(centerX + 5, centerY + 10, 145, 20)
|
||||||
|
.build();
|
||||||
|
addDrawableChild(getKeyButton);
|
||||||
|
|
||||||
|
ButtonWidget exitButton = ButtonWidget.builder(
|
||||||
|
Text.literal("Exit Game"),
|
||||||
|
button -> MinecraftClient.getInstance().scheduleStop()
|
||||||
|
)
|
||||||
|
.dimensions(centerX - 75, centerY + 45, 150, 20)
|
||||||
|
.build();
|
||||||
|
addDrawableChild(exitButton);
|
||||||
|
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateButtonState() {
|
||||||
|
if (verifyButton == null) return;
|
||||||
|
verifyButton.active = !keyField.getText().trim().isEmpty() && !isVerifying;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pasteFromClipboard() {
|
||||||
|
String clipboard = MinecraftClient.getInstance().keyboard.getClipboard();
|
||||||
|
if (clipboard != null) {
|
||||||
|
keyField.setText(clipboard.trim().toUpperCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyKey() {
|
||||||
|
String key = keyField.getText().trim().toUpperCase();
|
||||||
|
|
||||||
|
if (!key.matches("CITRUS-[A-Z0-9]{12}")) {
|
||||||
|
statusMessage = "Invalid format. Use: CITRUS-XXXXXXXXXXXX";
|
||||||
|
statusColor = 0xFF5555;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isVerifying = true;
|
||||||
|
statusMessage = "Verifying...";
|
||||||
|
statusColor = 0xFFFF55;
|
||||||
|
updateButtonState();
|
||||||
|
|
||||||
|
VerificationClient verificationClient = VerificationClient.getInstance();
|
||||||
|
HardwareIdentifier hardwareId = HardwareIdentifier.getInstance();
|
||||||
|
|
||||||
|
CompletableFuture<VerificationResult> future = CompletableFuture.supplyAsync(() ->
|
||||||
|
verificationClient.verifyKey(key, hardwareId.getHardwareId())
|
||||||
|
);
|
||||||
|
|
||||||
|
future.thenAccept(result -> MinecraftClient.getInstance().execute(() -> {
|
||||||
|
isVerifying = false;
|
||||||
|
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
ModConfig.BetaConfig betaConfig = ConfigManager.getBetaConfig();
|
||||||
|
betaConfig.betaKey = key;
|
||||||
|
betaConfig.verificationToken = result.getToken();
|
||||||
|
betaConfig.tokenExpiresAt = System.currentTimeMillis() +
|
||||||
|
(result.getExpiresIn() * 1000);
|
||||||
|
betaConfig.firstLaunch = false;
|
||||||
|
ConfigManager.save();
|
||||||
|
|
||||||
|
linkedUsername = result.getLinkedUsername();
|
||||||
|
if (linkedUsername != null && !linkedUsername.isEmpty()) {
|
||||||
|
statusMessage = "✓ Verified! Linked to: " + linkedUsername;
|
||||||
|
} else {
|
||||||
|
statusMessage = "✓ Verification successful!";
|
||||||
|
}
|
||||||
|
statusColor = 0x55FF55;
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}).thenRun(() -> MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
|
if (client.currentScreen == this) {
|
||||||
|
client.setScreen(
|
||||||
|
MuiModApi.get().createScreen(new MainMenuScreen(), null, null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
statusMessage = "✗ " + result.getMessage();
|
||||||
|
statusColor = 0xFF5555;
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
})).exceptionally(throwable -> {
|
||||||
|
Client.logger.error("Verification error", throwable);
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
isVerifying = false;
|
||||||
|
statusMessage = "✗ Connection error. Please try again.";
|
||||||
|
statusColor = 0xFF5555;
|
||||||
|
updateButtonState();
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||||
|
renderBackground(context, 0, 0, 0);
|
||||||
|
|
||||||
|
int centerX = width / 2;
|
||||||
|
int centerY = height / 2;
|
||||||
|
|
||||||
|
context.drawCenteredTextWithShadow(
|
||||||
|
textRenderer, Text.literal("Citrus Beta Access"),
|
||||||
|
centerX, 50, 0xFFA500
|
||||||
|
);
|
||||||
|
|
||||||
|
context.drawCenteredTextWithShadow(
|
||||||
|
textRenderer, "This version of Citrus requires a beta key",
|
||||||
|
centerX, 80, 0xAAAAAA
|
||||||
|
);
|
||||||
|
|
||||||
|
context.drawCenteredTextWithShadow(
|
||||||
|
textRenderer, "Enter your key below or visit our Discord",
|
||||||
|
centerX, 95, 0xAAAAAA
|
||||||
|
);
|
||||||
|
|
||||||
|
context.drawCenteredTextWithShadow(
|
||||||
|
textRenderer, "Format: CITRUS-XXXXXXXXXXXX",
|
||||||
|
centerX, centerY - 50, 0x888888
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!statusMessage.isEmpty()) {
|
||||||
|
context.drawCenteredTextWithShadow(
|
||||||
|
textRenderer, statusMessage,
|
||||||
|
centerX, centerY + 75, statusColor
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.render(context, mouseX, mouseY, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldCloseOnEsc() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package de.winniepat.citrus.beta;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.managers.ConfigManager;
|
||||||
|
import de.winniepat.citrus.ModConfig;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class BetaManager {
|
||||||
|
private static BetaManager instance;
|
||||||
|
|
||||||
|
private final VerificationClient verificationClient;
|
||||||
|
private boolean isVerified = false;
|
||||||
|
private ScheduledExecutorService scheduler;
|
||||||
|
|
||||||
|
private BetaManager() {
|
||||||
|
this.verificationClient = VerificationClient.getInstance();
|
||||||
|
setupPeriodicValidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized BetaManager getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new BetaManager();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initialize() {
|
||||||
|
Client.debugLog("Initializing Beta Verification System");
|
||||||
|
|
||||||
|
ConfigManager.load();
|
||||||
|
|
||||||
|
checkVerificationOnStartup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkVerificationOnStartup() {
|
||||||
|
Executors.newSingleThreadExecutor().submit(() -> {
|
||||||
|
if (!validateSession()) {
|
||||||
|
Client.debugLog("No valid beta session found");
|
||||||
|
|
||||||
|
ModConfig.BetaConfig betaConfig = ConfigManager.getBetaConfig();
|
||||||
|
if (betaConfig.firstLaunch || (!hasBetaKey() && !hasValidToken())) {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.currentScreen == null) {
|
||||||
|
client.setScreen(new BetaKeyScreen());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Client.debugLog("Beta verification successful");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validateSession() {
|
||||||
|
ModConfig.BetaConfig betaConfig = ConfigManager.getBetaConfig();
|
||||||
|
|
||||||
|
if (!ConfigManager.hasValidToken()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VerificationResult result = verificationClient.validateSession(
|
||||||
|
betaConfig.verificationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isSuccess()) {
|
||||||
|
isVerified = true;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
isVerified = false;
|
||||||
|
betaConfig.verificationToken = "";
|
||||||
|
betaConfig.tokenExpiresAt = 0;
|
||||||
|
ConfigManager.save();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupPeriodicValidation() {
|
||||||
|
scheduler = Executors.newScheduledThreadPool(1);
|
||||||
|
scheduler.scheduleAtFixedRate(() -> {
|
||||||
|
if (isVerified) {
|
||||||
|
validateSession();
|
||||||
|
}
|
||||||
|
}, 5, 5, TimeUnit.MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
if (scheduler != null) {
|
||||||
|
scheduler.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVerified() {
|
||||||
|
return isVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasBetaKey() {
|
||||||
|
return ConfigManager.hasBetaKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasValidToken() {
|
||||||
|
return ConfigManager.hasValidToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package de.winniepat.citrus.beta;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class HardwareIdentifier {
|
||||||
|
private static HardwareIdentifier instance;
|
||||||
|
private String hardwareId;
|
||||||
|
|
||||||
|
private HardwareIdentifier() {
|
||||||
|
this.hardwareId = generateHardwareId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized HardwareIdentifier getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new HardwareIdentifier();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateHardwareId() {
|
||||||
|
try {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.append(System.getProperty("os.name"));
|
||||||
|
sb.append(System.getProperty("os.version"));
|
||||||
|
sb.append(System.getProperty("os.arch"));
|
||||||
|
sb.append(System.getProperty("user.name"));
|
||||||
|
sb.append(System.getProperty("java.version"));
|
||||||
|
sb.append(System.getProperty("user.country"));
|
||||||
|
sb.append(Runtime.getRuntime().availableProcessors());
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (MinecraftClient.getInstance() != null) {
|
||||||
|
String uuid = String.valueOf(MinecraftClient.getInstance().getSession().getUuidOrNull());
|
||||||
|
sb.append(uuid);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.warn("Failed to generate hardware id", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
String combined = sb.toString();
|
||||||
|
String hash = Base64.getEncoder().encodeToString(
|
||||||
|
combined.getBytes(StandardCharsets.UTF_8)
|
||||||
|
);
|
||||||
|
|
||||||
|
return hash.substring(0, Math.min(32, hash.length()));
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.warn("Failed to generate hardware ID, using UUID fallback", e);
|
||||||
|
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHardwareId() {
|
||||||
|
return hardwareId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package de.winniepat.citrus.beta;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.managers.ConfigManager;
|
||||||
|
import de.winniepat.citrus.ModConfig;
|
||||||
|
import de.winniepat.citrus.utils.SecurityUtils;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class VerificationClient {
|
||||||
|
private static VerificationClient instance;
|
||||||
|
private static final Gson GSON = new Gson();
|
||||||
|
|
||||||
|
private String getApiKey() {
|
||||||
|
ModConfig.BetaConfig betaConfig = ConfigManager.getBetaConfig();
|
||||||
|
if (betaConfig == null) {
|
||||||
|
Client.logger.error("Beta config is null, using fallback API key");
|
||||||
|
return "ClientF8CjVGn7VTOAhROa65Ot";
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiKey = betaConfig.getActiveApiKey();
|
||||||
|
|
||||||
|
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||||
|
Client.logger.error("API key is empty in config, using fallback");
|
||||||
|
return "ClientF8CjVGn7VTOAhROa65Ot";
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VerificationClient() {}
|
||||||
|
|
||||||
|
public static synchronized VerificationClient getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new VerificationClient();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VerificationResult verifyKey(String key, String hardwareId) {
|
||||||
|
try {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
String playerUUID = String.valueOf(client.getSession().getUuidOrNull());
|
||||||
|
String playerName = client.getSession().getUsername();
|
||||||
|
|
||||||
|
String apiUrl = Client.BETA_URL + "/api/verify";
|
||||||
|
String apiKey = getApiKey();
|
||||||
|
|
||||||
|
Client.debugLog("Verifying key at: " + apiUrl);
|
||||||
|
Client.debugLog("Using API key: " + (apiKey.length() > 8 ?
|
||||||
|
apiKey.substring(0, 8) + "..." : apiKey));
|
||||||
|
|
||||||
|
URL url = new URL(apiUrl);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
setupConnection(conn, apiKey);
|
||||||
|
|
||||||
|
JsonObject request = new JsonObject();
|
||||||
|
request.addProperty("key", key);
|
||||||
|
request.addProperty("playerUUID", playerUUID);
|
||||||
|
request.addProperty("playerName", playerName);
|
||||||
|
request.addProperty("hardwareFingerprint", hardwareId);
|
||||||
|
request.addProperty("ipAddress", "127.0.0.1");
|
||||||
|
|
||||||
|
Client.debugLog("Sending verification request for key: " + key);
|
||||||
|
Client.debugLog("Player: " + playerName + " (" + playerUUID + ")");
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
byte[] input = request.toString().getBytes(StandardCharsets.UTF_8);
|
||||||
|
os.write(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
Client.debugLog("Server response code: " + responseCode);
|
||||||
|
|
||||||
|
if (responseCode == 200) {
|
||||||
|
try (BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
String responseText = br.readLine();
|
||||||
|
JsonObject response = GSON.fromJson(responseText, JsonObject.class);
|
||||||
|
|
||||||
|
Client.debugLog("Response: " + responseText);
|
||||||
|
|
||||||
|
if (response.get("valid").getAsBoolean()) {
|
||||||
|
Client.debugLog("Key verification successful!");
|
||||||
|
String linkedUsername = response.has("linkedUsername") && !response.get("linkedUsername").isJsonNull()
|
||||||
|
? response.get("linkedUsername").getAsString() : null;
|
||||||
|
|
||||||
|
// Validate that the linked username matches the current player
|
||||||
|
if (linkedUsername != null && !linkedUsername.isEmpty() && !linkedUsername.equalsIgnoreCase(playerName)) {
|
||||||
|
Client.logger.warn("Username mismatch! Key is linked to: {}, but current player is: {}", linkedUsername, playerName);
|
||||||
|
return VerificationResult.failure("This key is linked to " + linkedUsername + ", not " + playerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return VerificationResult.success(
|
||||||
|
response.get("token").getAsString(),
|
||||||
|
response.get("expiresIn").getAsLong(),
|
||||||
|
key,
|
||||||
|
playerUUID,
|
||||||
|
linkedUsername
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
String reason = response.has("reason") ?
|
||||||
|
response.get("reason").getAsString() : "Unknown error";
|
||||||
|
Client.logger.warn("Key verification failed: {}", reason);
|
||||||
|
return VerificationResult.failure(reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (responseCode == 401) {
|
||||||
|
Client.logger.error("API key rejected by server. Check your API key in config.");
|
||||||
|
return VerificationResult.failure("Invalid API key");
|
||||||
|
} else if (responseCode == 404) {
|
||||||
|
Client.logger.error("Server endpoint not found. Check API URL in config. URL: {}", url);
|
||||||
|
return VerificationResult.failure("Server endpoint not found");
|
||||||
|
} else {
|
||||||
|
Client.logger.error("Verification failed with HTTP code: {}", responseCode);
|
||||||
|
return VerificationResult.failure("Server error: " + responseCode);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error verifying key: {}", e.getMessage(), e);
|
||||||
|
return VerificationResult.failure("Connection error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public VerificationResult validateSession(String token) {
|
||||||
|
try {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
String currentPlayerName = client.getSession().getUsername();
|
||||||
|
|
||||||
|
String apiUrl = Client.BETA_URL + "/validate-session";
|
||||||
|
String apiKey = getApiKey();
|
||||||
|
|
||||||
|
URL url = new URL(apiUrl);
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
setupConnection(conn, apiKey);
|
||||||
|
|
||||||
|
JsonObject request = new JsonObject();
|
||||||
|
request.addProperty("token", token);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
byte[] input = request.toString().getBytes(StandardCharsets.UTF_8);
|
||||||
|
os.write(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
if (responseCode == 200) {
|
||||||
|
try (BufferedReader br = new BufferedReader(
|
||||||
|
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
|
JsonObject response = GSON.fromJson(br, JsonObject.class);
|
||||||
|
|
||||||
|
if (response.get("valid").getAsBoolean()) {
|
||||||
|
String linkedUsername = response.has("playerName") && !response.get("playerName").isJsonNull()
|
||||||
|
? response.get("playerName").getAsString() : null;
|
||||||
|
|
||||||
|
// Validate that the linked username matches the current player
|
||||||
|
if (linkedUsername != null && !linkedUsername.isEmpty() && !linkedUsername.equalsIgnoreCase(currentPlayerName)) {
|
||||||
|
Client.logger.warn("Session username mismatch! Session is linked to: {}, but current player is: {}", linkedUsername, currentPlayerName);
|
||||||
|
return VerificationResult.failure("Session is linked to " + linkedUsername + ", not " + currentPlayerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return VerificationResult.success(
|
||||||
|
token,
|
||||||
|
(response.get("expiresAt").getAsLong() - System.currentTimeMillis()) / 1000,
|
||||||
|
"SESSION",
|
||||||
|
response.get("playerUUID").getAsString(),
|
||||||
|
linkedUsername
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return VerificationResult.failure("Invalid session");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return VerificationResult.failure("Server error: " + responseCode);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error validating session", e);
|
||||||
|
return VerificationResult.failure("Connection error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupConnection(HttpURLConnection conn, String apiKey) throws Exception {
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("x-api-key", apiKey);
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(10000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package de.winniepat.citrus.beta;
|
||||||
|
|
||||||
|
public class VerificationResult {
|
||||||
|
private final boolean success;
|
||||||
|
private final String message;
|
||||||
|
private final String token;
|
||||||
|
private final long expiresIn;
|
||||||
|
private final String betaKey;
|
||||||
|
private final String playerUUID;
|
||||||
|
private final String linkedUsername;
|
||||||
|
|
||||||
|
private VerificationResult(boolean success, String message, String token,
|
||||||
|
long expiresIn, String betaKey, String playerUUID, String linkedUsername) {
|
||||||
|
this.success = success;
|
||||||
|
this.message = message;
|
||||||
|
this.token = token;
|
||||||
|
this.expiresIn = expiresIn;
|
||||||
|
this.betaKey = betaKey;
|
||||||
|
this.playerUUID = playerUUID;
|
||||||
|
this.linkedUsername = linkedUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VerificationResult success(String token, long expiresIn, String betaKey, String playerUUID, String linkedUsername) {
|
||||||
|
return new VerificationResult(true, "Verification successful", token, expiresIn, betaKey, playerUUID, linkedUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VerificationResult failure(String message) {
|
||||||
|
return new VerificationResult(false, message, null, 0, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getExpiresIn() {
|
||||||
|
return expiresIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBetaKey() {
|
||||||
|
return betaKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlayerUUID() {
|
||||||
|
return playerUUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLinkedUsername() {
|
||||||
|
return linkedUsername;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
package de.winniepat.citrus.client;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import com.mojang.brigadier.builder.*;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.*;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.sync.CapeSyncManager;
|
||||||
|
import de.winniepat.citrus.cosmetics.wings.ClientWingManager;
|
||||||
|
import de.winniepat.citrus.managers.ClientRegistrationManager;
|
||||||
|
import de.winniepat.citrus.gui.*;
|
||||||
|
import icyllis.modernui.mc.MuiModApi;
|
||||||
|
import net.fabricmc.fabric.api.client.command.v2.*;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ClientCommands {
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
|
||||||
|
|
||||||
|
LiteralArgumentBuilder<FabricClientCommandSource> citrusCommand =
|
||||||
|
LiteralArgumentBuilder.<FabricClientCommandSource>literal("citrus")
|
||||||
|
.executes(context -> {
|
||||||
|
context.getSource().sendFeedback(Text.literal("§f#### §aCitrus §7- by WinniePatGG §f####"));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§7Version: §7" + Client.CLIENT_BUILD));
|
||||||
|
context.getSource().sendFeedback(Text.literal(""));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§7Available commands:"));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§e/citrus cape set <cape_name> §7- Set your cape"));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§e/citrus cape reload §7- Reload available capes from cdn"));
|
||||||
|
context.getSource().sendFeedback(Text.literal(""));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§e/citrus capesync clearcache §7- Clear cape sync cache"));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§e/citrus capesync forcefetch §7- Force fetch capes for all players"));
|
||||||
|
context.getSource().sendFeedback(Text.literal(""));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§e/citrus wings toggle §7- Toggle wings visibility"));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§e/citrus wings set <wing_type> §7- Set your wing type"));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§e/citrus wings list §7- List available wing types"));
|
||||||
|
context.getSource().sendFeedback(Text.literal(""));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§e/citrus status §7- Show Citrus status"));
|
||||||
|
context.getSource().sendFeedback(Text.literal("§e/citrus screen <name> §7- Open a client screen"));
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
citrusCommand.then(LiteralArgumentBuilder.<FabricClientCommandSource>literal("screen")
|
||||||
|
.then(RequiredArgumentBuilder
|
||||||
|
.<FabricClientCommandSource, String>argument("name", StringArgumentType.word())
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
builder.suggest("showcase");
|
||||||
|
builder.suggest("friends");
|
||||||
|
builder.suggest("profiles");
|
||||||
|
builder.suggest("hud");
|
||||||
|
builder.suggest("menu");
|
||||||
|
return builder.buildFuture();
|
||||||
|
})
|
||||||
|
.executes(context -> {
|
||||||
|
String name = StringArgumentType.getString(context, "name").toLowerCase();
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
|
client.execute(() -> {
|
||||||
|
switch (name) {
|
||||||
|
case "friends" -> client.setScreen(MuiModApi.get().createScreen(new FriendsFragment(), null, client.currentScreen));
|
||||||
|
case "profiles" -> client.setScreen(MuiModApi.get().createScreen(new ProfileScreen(), null, client.currentScreen));
|
||||||
|
case "hud" -> client.setScreen(MuiModApi.get().createScreen(new HudEditorFragment(), null, client.currentScreen));
|
||||||
|
case "menu" -> client.setScreen(MuiModApi.get().createScreen(new MainMenuScreen(), null, null));
|
||||||
|
case "showcase" -> client.setScreen(MuiModApi.get().createScreen(new TestScreen(), null, client.currentScreen));
|
||||||
|
default -> context.getSource().sendError(Text.literal("§cUnknown screen: §e" + name));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
citrusCommand.then(LiteralArgumentBuilder.<FabricClientCommandSource>literal("cape")
|
||||||
|
.then(LiteralArgumentBuilder.<FabricClientCommandSource>literal("set")
|
||||||
|
.then(RequiredArgumentBuilder
|
||||||
|
.<FabricClientCommandSource, String>argument("cape_name", StringArgumentType.string())
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
builder.suggest("none");
|
||||||
|
builder.suggest("auto");
|
||||||
|
for (String cape : CapeHandler.getAvailableCapes()) {
|
||||||
|
builder.suggest(cape);
|
||||||
|
}
|
||||||
|
return builder.buildFuture();
|
||||||
|
})
|
||||||
|
.executes(context -> {
|
||||||
|
String capeName = StringArgumentType.getString(context, "cape_name");
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
|
if (client.player == null) {
|
||||||
|
context.getSource().sendError(Text.literal("§cYou must be in-game!"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CapeHandler.capeExists(capeName)) {
|
||||||
|
CapeHandler.setLocalPlayerCape(capeName, false);
|
||||||
|
context.getSource().sendFeedback(Text.literal("§aCape set to: §e" + capeName));
|
||||||
|
} else {
|
||||||
|
context.getSource().sendError(Text.literal("§cUnknown cape: " + capeName));
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(LiteralArgumentBuilder.<FabricClientCommandSource>literal("reload")
|
||||||
|
.executes(context -> {
|
||||||
|
context.getSource().sendFeedback(Text.literal("§aReloading capes..."));
|
||||||
|
CapeHandler.reloadCapes();
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
citrusCommand.then(LiteralArgumentBuilder.<FabricClientCommandSource>literal("capesync")
|
||||||
|
.then(LiteralArgumentBuilder.<FabricClientCommandSource>literal("clearcache")
|
||||||
|
.executes(context -> {
|
||||||
|
CapeSyncManager.clearAllCache();
|
||||||
|
context.getSource().sendFeedback(Text.literal("§aCache cleared."));
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(LiteralArgumentBuilder.<FabricClientCommandSource>literal("forcefetch")
|
||||||
|
.executes(context -> {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
|
if (client.player == null || client.world == null) {
|
||||||
|
context.getSource().sendError(Text.literal("§cYou must be in-game!"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UUID> playerUUIDs = client.world.getPlayers().stream()
|
||||||
|
.filter(p -> p != client.player)
|
||||||
|
.map(PlayerEntity::getUuid)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
CapeSyncManager.batchFetchCapes(playerUUIDs);
|
||||||
|
context.getSource().sendFeedback(Text.literal(
|
||||||
|
"§aFetching capes for §e" + playerUUIDs.size() + "§a players..."
|
||||||
|
));
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
citrusCommand.then(LiteralArgumentBuilder.<FabricClientCommandSource>literal("status")
|
||||||
|
.executes(context -> {
|
||||||
|
context.getSource().sendFeedback(Text.literal("--- §aCitrus Status §7---"));
|
||||||
|
context.getSource().sendFeedback(Text.literal(""));
|
||||||
|
|
||||||
|
boolean healthy = CapeSyncManager.isIsHealthy();
|
||||||
|
String serverHealth = healthy
|
||||||
|
? "§ahealthy"
|
||||||
|
: "§cexperiencing issues or is offline";
|
||||||
|
context.getSource().sendFeedback(Text.literal("CapeSync Server: " + serverHealth));
|
||||||
|
|
||||||
|
int capesAvailable = CapeHandler.getAvailableCapes().size();
|
||||||
|
context.getSource().sendFeedback(Text.literal("§aCapes available: §e" + capesAvailable));
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) {
|
||||||
|
String activeWings = de.winniepat.citrus.cosmetics.wings.ClientWingManager.getActiveWings(client.player.getUuid());
|
||||||
|
boolean wingsVisible = ClientWingManager.areWingsVisible(client.player.getUuid());
|
||||||
|
String wingStatus = "§aWing type: §e" + activeWings + "§a, " + (wingsVisible ? "§avisible" : "§chidden");
|
||||||
|
context.getSource().sendFeedback(Text.literal(wingStatus));
|
||||||
|
}
|
||||||
|
|
||||||
|
String registrationStatus = getRegistrationStatus(healthy);
|
||||||
|
context.getSource().sendFeedback(Text.literal("Registration: " + registrationStatus));
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatcher.register(citrusCommand);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull String getRegistrationStatus(boolean healthy) {
|
||||||
|
boolean registered = ClientRegistrationManager.isRegistered();
|
||||||
|
|
||||||
|
String registrationStatus = registered
|
||||||
|
? "§aYou are registered with the Citrus API"
|
||||||
|
: "§cYou are not registered with the Citrus API";
|
||||||
|
|
||||||
|
if (!healthy && registered) {
|
||||||
|
registrationStatus += " §7(§eStatus may be inaccurate because Backend has issues or is offline§7)";
|
||||||
|
}
|
||||||
|
return registrationStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package de.winniepat.citrus.client;
|
||||||
|
|
||||||
|
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
|
||||||
|
import net.minecraft.client.option.KeyBinding;
|
||||||
|
import net.minecraft.client.util.InputUtil;
|
||||||
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
|
||||||
|
public class ClientKeybinds {
|
||||||
|
|
||||||
|
private static final String CATEGORY = "category.citrus.general";
|
||||||
|
|
||||||
|
public static KeyBinding OPEN_DRAGGUI;
|
||||||
|
public static KeyBinding OPEN_CAPE_SCREEN;
|
||||||
|
public static KeyBinding REFRESH_CLIENT;
|
||||||
|
public static KeyBinding TOGGLE_FULLBRIGHT;
|
||||||
|
public static KeyBinding ZOOM_KEY;
|
||||||
|
public static KeyBinding OPEN_MODULETOGGLE;
|
||||||
|
public static KeyBinding OPEN_FRIENDS_SCREEN;
|
||||||
|
public static KeyBinding OPEN_COSMETICS_SCREEN;
|
||||||
|
|
||||||
|
public static KeyBinding OPEN_BETA_KEY_SCREEN;
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
OPEN_DRAGGUI = new KeyBinding("key.citrus.overlay_editor", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_RIGHT_SHIFT, CATEGORY);
|
||||||
|
OPEN_CAPE_SCREEN = new KeyBinding("key.citrus.open_cape_screen", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_K, CATEGORY);
|
||||||
|
REFRESH_CLIENT = new KeyBinding("key.citrus.refresh_client", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_J, CATEGORY);
|
||||||
|
TOGGLE_FULLBRIGHT = new KeyBinding("key.citrus.toggle_fullbright", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_G, CATEGORY);
|
||||||
|
ZOOM_KEY = new KeyBinding("key.citrus.zoom", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_C, CATEGORY);
|
||||||
|
OPEN_MODULETOGGLE = new KeyBinding("key.citrus.module_toggle", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_RIGHT_ALT, CATEGORY);
|
||||||
|
OPEN_FRIENDS_SCREEN = new KeyBinding("key.citrus.open_friends_screen", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_H, CATEGORY);
|
||||||
|
OPEN_COSMETICS_SCREEN = new KeyBinding("key.citrus.open_cosmetics_screen", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_L, CATEGORY);
|
||||||
|
OPEN_BETA_KEY_SCREEN = new KeyBinding("key.citrus.open_beta_key_screen", InputUtil.Type.KEYSYM, GLFW.GLFW_KEY_B, CATEGORY);
|
||||||
|
|
||||||
|
KeyBindingHelper.registerKeyBinding(OPEN_DRAGGUI);
|
||||||
|
KeyBindingHelper.registerKeyBinding(OPEN_CAPE_SCREEN);
|
||||||
|
KeyBindingHelper.registerKeyBinding(REFRESH_CLIENT);
|
||||||
|
KeyBindingHelper.registerKeyBinding(TOGGLE_FULLBRIGHT);
|
||||||
|
KeyBindingHelper.registerKeyBinding(ZOOM_KEY);
|
||||||
|
KeyBindingHelper.registerKeyBinding(OPEN_MODULETOGGLE);
|
||||||
|
KeyBindingHelper.registerKeyBinding(OPEN_FRIENDS_SCREEN);
|
||||||
|
KeyBindingHelper.registerKeyBinding(OPEN_COSMETICS_SCREEN);
|
||||||
|
KeyBindingHelper.registerKeyBinding(OPEN_BETA_KEY_SCREEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package de.winniepat.citrus.client;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.Command;
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import de.winniepat.citrus.cosmetics.ClientCosmeticManager;
|
||||||
|
import de.winniepat.citrus.cosmetics.CosmeticScreen;
|
||||||
|
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
|
||||||
|
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class CosmeticCommands {
|
||||||
|
public static void register() {
|
||||||
|
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("cosmetic")
|
||||||
|
.then(ClientCommandManager.literal("wings")
|
||||||
|
.then(ClientCommandManager.literal("toggle")
|
||||||
|
.executes(context -> {
|
||||||
|
UUID uuid = context.getSource().getPlayer().getUuid();
|
||||||
|
ClientCosmeticManager.toggleWings(uuid);
|
||||||
|
context.getSource().sendFeedback(Text.of("Toggled wings visibility"));
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(ClientCommandManager.literal("set")
|
||||||
|
.then(ClientCommandManager.argument("type", StringArgumentType.string())
|
||||||
|
.executes(context -> {
|
||||||
|
UUID uuid = context.getSource().getPlayer().getUuid();
|
||||||
|
String type = StringArgumentType.getString(context, "type");
|
||||||
|
ClientCosmeticManager.setActiveWings(uuid, type);
|
||||||
|
context.getSource().sendFeedback(Text.of("Set wings to: " + type));
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(ClientCommandManager.literal("hat")
|
||||||
|
.then(ClientCommandManager.literal("toggle")
|
||||||
|
.executes(context -> {
|
||||||
|
UUID uuid = context.getSource().getPlayer().getUuid();
|
||||||
|
ClientCosmeticManager.toggleHat(uuid);
|
||||||
|
context.getSource().sendFeedback(Text.of("Toggled hat visibility"));
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(ClientCommandManager.literal("set")
|
||||||
|
.then(ClientCommandManager.argument("type", StringArgumentType.string())
|
||||||
|
.executes(context -> {
|
||||||
|
UUID uuid = context.getSource().getPlayer().getUuid();
|
||||||
|
String type = StringArgumentType.getString(context, "type");
|
||||||
|
ClientCosmeticManager.setActiveHat(uuid, type);
|
||||||
|
context.getSource().sendFeedback(Text.of("Set hat to: " + type));
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(ClientCommandManager.literal("halo")
|
||||||
|
.then(ClientCommandManager.literal("toggle")
|
||||||
|
.executes(context -> {
|
||||||
|
UUID uuid = context.getSource().getPlayer().getUuid();
|
||||||
|
ClientCosmeticManager.toggleHalo(uuid);
|
||||||
|
context.getSource().sendFeedback(Text.of("Toggled halo visibility"));
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(ClientCommandManager.literal("set")
|
||||||
|
.then(ClientCommandManager.argument("type", StringArgumentType.string())
|
||||||
|
.executes(context -> {
|
||||||
|
UUID uuid = context.getSource().getPlayer().getUuid();
|
||||||
|
String type = StringArgumentType.getString(context, "type");
|
||||||
|
ClientCosmeticManager.setActiveHalo(uuid, type);
|
||||||
|
context.getSource().sendFeedback(Text.of("Set halo to: " + type));
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(ClientCommandManager.literal("gui")
|
||||||
|
.executes(context -> {
|
||||||
|
context.getSource().getClient().setScreen(new CosmeticScreen());
|
||||||
|
return Command.SINGLE_SUCCESS;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ClientCosmeticManager {
|
||||||
|
private static final File CONFIG_FILE = new File(Client.CONFIG_DIR, "citrus_cosmetics.json");
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
private static Map<UUID, PlayerCosmeticData> cosmeticDataMap = new HashMap<>();
|
||||||
|
|
||||||
|
public static class PlayerCosmeticData {
|
||||||
|
public String activeWings = "none";
|
||||||
|
public boolean wingsVisible = true;
|
||||||
|
public String activeHat = "none";
|
||||||
|
public boolean hatVisible = true;
|
||||||
|
public String activeHalo = "none";
|
||||||
|
public boolean haloVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void load() {
|
||||||
|
if (CONFIG_FILE.exists()) {
|
||||||
|
try (FileReader reader = new FileReader(CONFIG_FILE)) {
|
||||||
|
JsonObject json = JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
cosmeticDataMap.clear();
|
||||||
|
|
||||||
|
for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
|
||||||
|
UUID uuid = UUID.fromString(entry.getKey());
|
||||||
|
JsonObject data = entry.getValue().getAsJsonObject();
|
||||||
|
PlayerCosmeticData cosmeticData = new PlayerCosmeticData();
|
||||||
|
|
||||||
|
if (data.has("activeWings")) {
|
||||||
|
cosmeticData.activeWings = data.get("activeWings").getAsString();
|
||||||
|
}
|
||||||
|
if (data.has("wingsVisible")) {
|
||||||
|
cosmeticData.wingsVisible = data.get("wingsVisible").getAsBoolean();
|
||||||
|
}
|
||||||
|
if (data.has("activeHat")) {
|
||||||
|
cosmeticData.activeHat = data.get("activeHat").getAsString();
|
||||||
|
}
|
||||||
|
if (data.has("hatVisible")) {
|
||||||
|
cosmeticData.hatVisible = data.get("hatVisible").getAsBoolean();
|
||||||
|
}
|
||||||
|
if (data.has("activeHalo")) {
|
||||||
|
cosmeticData.activeHalo = data.get("activeHalo").getAsString();
|
||||||
|
}
|
||||||
|
if (data.has("haloVisible")) {
|
||||||
|
cosmeticData.haloVisible = data.get("haloVisible").getAsBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
cosmeticDataMap.put(uuid, cosmeticData);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to load cosmetic data: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save() {
|
||||||
|
try {
|
||||||
|
if (!CONFIG_FILE.getParentFile().exists()) {
|
||||||
|
CONFIG_FILE.getParentFile().mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileWriter writer = new FileWriter(CONFIG_FILE)) {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
for (Map.Entry<UUID, PlayerCosmeticData> entry : cosmeticDataMap.entrySet()) {
|
||||||
|
JsonObject data = new JsonObject();
|
||||||
|
data.addProperty("activeWings", entry.getValue().activeWings);
|
||||||
|
data.addProperty("wingsVisible", entry.getValue().wingsVisible);
|
||||||
|
data.addProperty("activeHat", entry.getValue().activeHat);
|
||||||
|
data.addProperty("hatVisible", entry.getValue().hatVisible);
|
||||||
|
data.addProperty("activeHalo", entry.getValue().activeHalo);
|
||||||
|
data.addProperty("haloVisible", entry.getValue().haloVisible);
|
||||||
|
json.add(entry.getKey().toString(), data);
|
||||||
|
}
|
||||||
|
GSON.toJson(json, writer);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to save cosmetic data: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlayerCosmeticData getData(UUID playerUUID) {
|
||||||
|
return cosmeticDataMap.computeIfAbsent(playerUUID, uuid -> new PlayerCosmeticData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getActiveWings(UUID playerUUID) {
|
||||||
|
return getData(playerUUID).activeWings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean areWingsVisible(UUID playerUUID) {
|
||||||
|
return getData(playerUUID).wingsVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setActiveWings(UUID playerUUID, String wingId) {
|
||||||
|
PlayerCosmeticData data = getData(playerUUID);
|
||||||
|
data.activeWings = wingId.toLowerCase();
|
||||||
|
save();
|
||||||
|
CosmeticSyncManager.sendCosmeticsToServer(playerUUID, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void toggleWings(UUID playerUUID) {
|
||||||
|
PlayerCosmeticData data = getData(playerUUID);
|
||||||
|
data.wingsVisible = !data.wingsVisible;
|
||||||
|
save();
|
||||||
|
CosmeticSyncManager.sendCosmeticsToServer(playerUUID, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getActiveHat(UUID playerUUID) {
|
||||||
|
return getData(playerUUID).activeHat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isHatVisible(UUID playerUUID) {
|
||||||
|
return getData(playerUUID).hatVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setActiveHat(UUID playerUUID, String hatId) {
|
||||||
|
PlayerCosmeticData data = getData(playerUUID);
|
||||||
|
data.activeHat = hatId.toLowerCase();
|
||||||
|
save();
|
||||||
|
CosmeticSyncManager.sendCosmeticsToServer(playerUUID, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void toggleHat(UUID playerUUID) {
|
||||||
|
PlayerCosmeticData data = getData(playerUUID);
|
||||||
|
data.hatVisible = !data.hatVisible;
|
||||||
|
save();
|
||||||
|
CosmeticSyncManager.sendCosmeticsToServer(playerUUID, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getActiveHalo(UUID playerUUID) {
|
||||||
|
return getData(playerUUID).activeHalo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isHaloVisible(UUID playerUUID) {
|
||||||
|
return getData(playerUUID).haloVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setActiveHalo(UUID playerUUID, String haloId) {
|
||||||
|
PlayerCosmeticData data = getData(playerUUID);
|
||||||
|
data.activeHalo = haloId.toLowerCase();
|
||||||
|
save();
|
||||||
|
CosmeticSyncManager.sendCosmeticsToServer(playerUUID, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void toggleHalo(UUID playerUUID) {
|
||||||
|
PlayerCosmeticData data = getData(playerUUID);
|
||||||
|
data.haloVisible = !data.haloVisible;
|
||||||
|
save();
|
||||||
|
CosmeticSyncManager.sendCosmeticsToServer(playerUUID, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics;
|
||||||
|
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import net.minecraft.nbt.NbtCompound;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class CosmeticManager {
|
||||||
|
private static final Map<UUID, PlayerCosmeticData> playerData = new HashMap<>();
|
||||||
|
|
||||||
|
public static class PlayerCosmeticData {
|
||||||
|
private String activeWings = "none";
|
||||||
|
private boolean wingsVisible = true;
|
||||||
|
private String activeHat = "none";
|
||||||
|
private boolean hatVisible = true;
|
||||||
|
private String activeHalo = "none";
|
||||||
|
private boolean haloVisible = true;
|
||||||
|
|
||||||
|
public void setActiveWings(String wingId) {
|
||||||
|
this.activeWings = wingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getActiveWings() {
|
||||||
|
return activeWings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWingsVisible(boolean visible) {
|
||||||
|
this.wingsVisible = visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean areWingsVisible() {
|
||||||
|
return wingsVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActiveHat(String hatId) {
|
||||||
|
this.activeHat = hatId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getActiveHat() {
|
||||||
|
return activeHat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHatVisible(boolean visible) {
|
||||||
|
this.hatVisible = visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHatVisible() {
|
||||||
|
return hatVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActiveHalo(String haloId) {
|
||||||
|
this.activeHalo = haloId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getActiveHalo() {
|
||||||
|
return activeHalo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHaloVisible(boolean visible) {
|
||||||
|
this.haloVisible = visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHaloVisible() {
|
||||||
|
return haloVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NbtCompound toNbt() {
|
||||||
|
NbtCompound nbt = new NbtCompound();
|
||||||
|
nbt.putString("activeWings", activeWings);
|
||||||
|
nbt.putBoolean("wingsVisible", wingsVisible);
|
||||||
|
nbt.putString("activeHat", activeHat);
|
||||||
|
nbt.putBoolean("hatVisible", hatVisible);
|
||||||
|
nbt.putString("activeHalo", activeHalo);
|
||||||
|
nbt.putBoolean("haloVisible", haloVisible);
|
||||||
|
return nbt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlayerCosmeticData fromNbt(NbtCompound nbt) {
|
||||||
|
PlayerCosmeticData data = new PlayerCosmeticData();
|
||||||
|
if (nbt.contains("activeWings")) {
|
||||||
|
data.activeWings = nbt.getString("activeWings");
|
||||||
|
}
|
||||||
|
if (nbt.contains("wingsVisible")) {
|
||||||
|
data.wingsVisible = nbt.getBoolean("wingsVisible");
|
||||||
|
}
|
||||||
|
if (nbt.contains("activeHat")) {
|
||||||
|
data.activeHat = nbt.getString("activeHat");
|
||||||
|
}
|
||||||
|
if (nbt.contains("hatVisible")) {
|
||||||
|
data.hatVisible = nbt.getBoolean("hatVisible");
|
||||||
|
}
|
||||||
|
if (nbt.contains("activeHalo")) {
|
||||||
|
data.activeHalo = nbt.getString("activeHalo");
|
||||||
|
}
|
||||||
|
if (nbt.contains("haloVisible")) {
|
||||||
|
data.haloVisible = nbt.getBoolean("haloVisible");
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PlayerCosmeticData getData(PlayerEntity player) {
|
||||||
|
return playerData.computeIfAbsent(player.getUuid(), uuid -> new PlayerCosmeticData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setWings(PlayerEntity player, String wingId) {
|
||||||
|
getData(player).setActiveWings(wingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void toggleWings(PlayerEntity player) {
|
||||||
|
PlayerCosmeticData data = getData(player);
|
||||||
|
data.setWingsVisible(!data.areWingsVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setHat(PlayerEntity player, String hatId) {
|
||||||
|
getData(player).setActiveHat(hatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void toggleHat(PlayerEntity player) {
|
||||||
|
PlayerCosmeticData data = getData(player);
|
||||||
|
data.setHatVisible(!data.isHatVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setHalo(PlayerEntity player, String haloId) {
|
||||||
|
getData(player).setActiveHalo(haloId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void toggleHalo(PlayerEntity player) {
|
||||||
|
PlayerCosmeticData data = getData(player);
|
||||||
|
data.setHaloVisible(!data.isHaloVisible());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics;
|
||||||
|
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.Screen;
|
||||||
|
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.Formatting;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class CosmeticScreen extends Screen {
|
||||||
|
private static final int BUTTON_WIDTH = 200;
|
||||||
|
private static final int BUTTON_HEIGHT = 20;
|
||||||
|
private static final int PADDING = 5;
|
||||||
|
|
||||||
|
private UUID playerUUID;
|
||||||
|
|
||||||
|
public CosmeticScreen() {
|
||||||
|
super(Text.of("Cosmetics"));
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
this.playerUUID = client.player != null ? client.player.getUuid() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
if (playerUUID == null) {
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int centerX = this.width / 2;
|
||||||
|
int y = this.height / 4;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("§6§lCOSMETICS").copy().formatted(Formatting.BOLD),
|
||||||
|
button -> {}
|
||||||
|
).dimensions(centerX - 100, y - 30, 200, 20).build());
|
||||||
|
|
||||||
|
y += 40;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("§e§lWINGS").copy().formatted(Formatting.BOLD),
|
||||||
|
button -> {}
|
||||||
|
).dimensions(centerX - 100, y, 200, 20).build());
|
||||||
|
y += 25;
|
||||||
|
|
||||||
|
String wingsStatus = ClientCosmeticManager.areWingsVisible(playerUUID) ? "§aVisible" : "§cHidden";
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("Toggle Wings: " + wingsStatus),
|
||||||
|
button -> {
|
||||||
|
ClientCosmeticManager.toggleWings(playerUUID);
|
||||||
|
this.clearAndInit();
|
||||||
|
}
|
||||||
|
).dimensions(centerX - 100, y, 200, BUTTON_HEIGHT).build());
|
||||||
|
y += 25;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("Select Wing Type"),
|
||||||
|
button -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(new WingSelectionScreen());
|
||||||
|
}
|
||||||
|
).dimensions(centerX - 100, y, 200, BUTTON_HEIGHT).build());
|
||||||
|
|
||||||
|
y += 40;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("§e§lHAT").copy().formatted(Formatting.BOLD),
|
||||||
|
button -> {}
|
||||||
|
).dimensions(centerX - 100, y, 200, 20).build());
|
||||||
|
y += 25;
|
||||||
|
|
||||||
|
String hatStatus = ClientCosmeticManager.isHatVisible(playerUUID) ? "§aVisible" : "§cHidden";
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("Toggle Hat: " + hatStatus),
|
||||||
|
button -> {
|
||||||
|
ClientCosmeticManager.toggleHat(playerUUID);
|
||||||
|
this.clearAndInit();
|
||||||
|
}
|
||||||
|
).dimensions(centerX - 100, y, 200, BUTTON_HEIGHT).build());
|
||||||
|
y += 25;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("Select Hat Type"),
|
||||||
|
button -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(new HatSelectionScreen());
|
||||||
|
}
|
||||||
|
).dimensions(centerX - 100, y, 200, BUTTON_HEIGHT).build());
|
||||||
|
|
||||||
|
y += 40;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("§e§lHALO").copy().formatted(Formatting.BOLD),
|
||||||
|
button -> {}
|
||||||
|
).dimensions(centerX - 100, y, 200, 20).build());
|
||||||
|
y += 25;
|
||||||
|
|
||||||
|
String haloStatus = ClientCosmeticManager.isHaloVisible(playerUUID) ? "§aVisible" : "§cHidden";
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("Toggle Halo: " + haloStatus),
|
||||||
|
button -> {
|
||||||
|
ClientCosmeticManager.toggleHalo(playerUUID);
|
||||||
|
this.clearAndInit();
|
||||||
|
}
|
||||||
|
).dimensions(centerX - 100, y, 200, BUTTON_HEIGHT).build());
|
||||||
|
y += 25;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("Select Halo Type"),
|
||||||
|
button -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(new HaloSelectionScreen());
|
||||||
|
}
|
||||||
|
).dimensions(centerX - 100, y, 200, BUTTON_HEIGHT).build());
|
||||||
|
|
||||||
|
y += 50;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("§cClose"),
|
||||||
|
button -> this.close()
|
||||||
|
).dimensions(centerX - 50, y, 100, BUTTON_HEIGHT).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||||
|
this.renderBackground(context, mouseX, mouseY, delta);
|
||||||
|
super.render(context, mouseX, mouseY, delta);
|
||||||
|
|
||||||
|
context.drawCenteredTextWithShadow(
|
||||||
|
this.textRenderer,
|
||||||
|
Text.of("§6§lCosmetics Menu").copy().formatted(Formatting.BOLD),
|
||||||
|
this.width / 2,
|
||||||
|
30,
|
||||||
|
0xFFFFFF
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldPause() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WingSelectionScreen extends Screen {
|
||||||
|
private final de.winniepat.citrus.cosmetics.wings.WingType[] wingTypes;
|
||||||
|
|
||||||
|
public WingSelectionScreen() {
|
||||||
|
super(Text.of("Select Wings"));
|
||||||
|
wingTypes = de.winniepat.citrus.cosmetics.wings.WingType.getAvailableTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
int centerX = this.width / 2;
|
||||||
|
int y = 60;
|
||||||
|
|
||||||
|
for (de.winniepat.citrus.cosmetics.wings.WingType type : wingTypes) {
|
||||||
|
if (type == de.winniepat.citrus.cosmetics.wings.WingType.NONE) continue;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("Select: " + type.getId()),
|
||||||
|
button -> {
|
||||||
|
UUID uuid = MinecraftClient.getInstance().player.getUuid();
|
||||||
|
ClientCosmeticManager.setActiveWings(uuid, type.getId());
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
).dimensions(centerX - 100, y, 200, 20).build());
|
||||||
|
y += 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
y += 20;
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("§cBack"),
|
||||||
|
button -> this.close()
|
||||||
|
).dimensions(centerX - 50, y, 100, 20).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HatSelectionScreen extends Screen {
|
||||||
|
private final de.winniepat.citrus.cosmetics.hat.HatType[] hatTypes;
|
||||||
|
|
||||||
|
public HatSelectionScreen() {
|
||||||
|
super(Text.of("Select Hat"));
|
||||||
|
hatTypes = de.winniepat.citrus.cosmetics.hat.HatType.getAvailableTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
int centerX = this.width / 2;
|
||||||
|
int y = 60;
|
||||||
|
|
||||||
|
for (de.winniepat.citrus.cosmetics.hat.HatType type : hatTypes) {
|
||||||
|
if (type == de.winniepat.citrus.cosmetics.hat.HatType.NONE) continue;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("Select: " + type.getId()),
|
||||||
|
button -> {
|
||||||
|
UUID uuid = MinecraftClient.getInstance().player.getUuid();
|
||||||
|
ClientCosmeticManager.setActiveHat(uuid, type.getId());
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
).dimensions(centerX - 100, y, 200, 20).build());
|
||||||
|
y += 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
y += 20;
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("§cBack"),
|
||||||
|
button -> this.close()
|
||||||
|
).dimensions(centerX - 50, y, 100, 20).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HaloSelectionScreen extends Screen {
|
||||||
|
private final de.winniepat.citrus.cosmetics.halo.HaloType[] haloTypes;
|
||||||
|
|
||||||
|
public HaloSelectionScreen() {
|
||||||
|
super(Text.of("Select Halo"));
|
||||||
|
haloTypes = de.winniepat.citrus.cosmetics.halo.HaloType.getAvailableTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
int centerX = this.width / 2;
|
||||||
|
int y = 60;
|
||||||
|
|
||||||
|
for (de.winniepat.citrus.cosmetics.halo.HaloType type : haloTypes) {
|
||||||
|
if (type == de.winniepat.citrus.cosmetics.halo.HaloType.NONE) continue;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("Select: " + type.getId()),
|
||||||
|
button -> {
|
||||||
|
UUID uuid = MinecraftClient.getInstance().player.getUuid();
|
||||||
|
ClientCosmeticManager.setActiveHalo(uuid, type.getId());
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
).dimensions(centerX - 100, y, 200, 20).build());
|
||||||
|
y += 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
y += 20;
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(
|
||||||
|
Text.of("§cBack"),
|
||||||
|
button -> this.close()
|
||||||
|
).dimensions(centerX - 50, y, 100, 20).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.managers.ClientRegistrationManager;
|
||||||
|
import de.winniepat.citrus.utils.SecurityUtils;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class CosmeticSyncManager {
|
||||||
|
private static final String API_BASE_URL = Client.API_URL;
|
||||||
|
private static final Gson GSON = new GsonBuilder().create();
|
||||||
|
|
||||||
|
public static final Map<UUID, SyncedCosmeticData> SYNCED_COSMETIC_CACHE = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<UUID, Long> LAST_SYNC_TIME = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<UUID, SyncedCosmeticData> PENDING_UPDATES = new ConcurrentHashMap<>();
|
||||||
|
private static final Set<UUID> PENDING_FETCHES = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private static boolean hasSentInitialCosmetics = false;
|
||||||
|
private static boolean isInitializing = false;
|
||||||
|
private static boolean autoSyncEnabled = true;
|
||||||
|
private static Timer syncTimer;
|
||||||
|
|
||||||
|
public static class SyncedCosmeticData {
|
||||||
|
public String wingType = "none";
|
||||||
|
public boolean wingsVisible = true;
|
||||||
|
public String hatType = "none";
|
||||||
|
public boolean hatVisible = true;
|
||||||
|
public String haloType = "none";
|
||||||
|
public boolean haloVisible = true;
|
||||||
|
|
||||||
|
public JsonObject toJson() {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("wingType", wingType);
|
||||||
|
json.addProperty("wingsVisible", wingsVisible);
|
||||||
|
json.addProperty("hatType", hatType);
|
||||||
|
json.addProperty("hatVisible", hatVisible);
|
||||||
|
json.addProperty("haloType", haloType);
|
||||||
|
json.addProperty("haloVisible", haloVisible);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SyncedCosmeticData fromJson(JsonObject json) {
|
||||||
|
SyncedCosmeticData data = new SyncedCosmeticData();
|
||||||
|
if (json.has("wingType")) data.wingType = json.get("wingType").getAsString();
|
||||||
|
if (json.has("wingsVisible")) data.wingsVisible = json.get("wingsVisible").getAsBoolean();
|
||||||
|
if (json.has("hatType")) data.hatType = json.get("hatType").getAsString();
|
||||||
|
if (json.has("hatVisible")) data.hatVisible = json.get("hatVisible").getAsBoolean();
|
||||||
|
if (json.has("haloType")) data.haloType = json.get("haloType").getAsString();
|
||||||
|
if (json.has("haloVisible")) data.haloVisible = json.get("haloVisible").getAsBoolean();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SyncedCosmeticData fromClientCosmeticData(ClientCosmeticManager.PlayerCosmeticData clientData) {
|
||||||
|
SyncedCosmeticData data = new SyncedCosmeticData();
|
||||||
|
data.wingType = clientData.activeWings;
|
||||||
|
data.wingsVisible = clientData.wingsVisible;
|
||||||
|
data.hatType = clientData.activeHat;
|
||||||
|
data.hatVisible = clientData.hatVisible;
|
||||||
|
data.haloType = clientData.activeHalo;
|
||||||
|
data.haloVisible = clientData.haloVisible;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientCosmeticManager.PlayerCosmeticData toClientCosmeticData() {
|
||||||
|
ClientCosmeticManager.PlayerCosmeticData data = new ClientCosmeticManager.PlayerCosmeticData();
|
||||||
|
data.activeWings = this.wingType;
|
||||||
|
data.wingsVisible = this.wingsVisible;
|
||||||
|
data.activeHat = this.hatType;
|
||||||
|
data.hatVisible = this.hatVisible;
|
||||||
|
data.activeHalo = this.haloType;
|
||||||
|
data.haloVisible = this.haloVisible;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
Client.debugLog("Initializing cosmetic sync...");
|
||||||
|
isInitializing = true;
|
||||||
|
startAutoSync();
|
||||||
|
scheduleInitialSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void scheduleInitialSync() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) {
|
||||||
|
performInitialSync();
|
||||||
|
} else {
|
||||||
|
Util.getMainWorkerExecutor().execute(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
performInitialSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void performInitialSync() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) {
|
||||||
|
isInitializing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID uuid = client.player.getUuid();
|
||||||
|
String playerName = client.player.getName().getString();
|
||||||
|
|
||||||
|
Client.debugLog("Performing initial cosmetic sync for " + playerName);
|
||||||
|
fetchCosmeticsFromServer(uuid, playerName, true).thenAccept(serverData -> {
|
||||||
|
if (serverData != null) {
|
||||||
|
applyServerCosmeticsToLocal(uuid, serverData);
|
||||||
|
} else {
|
||||||
|
ClientCosmeticManager.PlayerCosmeticData localData = ClientCosmeticManager.getData(uuid);
|
||||||
|
if (localData != null) {
|
||||||
|
SyncedCosmeticData syncData = SyncedCosmeticData.fromClientCosmeticData(localData);
|
||||||
|
sendCosmeticsToServer(uuid, syncData, playerName, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasSentInitialCosmetics = true;
|
||||||
|
isInitializing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SyncedCosmeticData getSyncedCosmetics(UUID uuid, String playerName) {
|
||||||
|
if (!ClientRegistrationManager.isPlayerOnlineWithCitrus(uuid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInitializing && isLocalPlayer(uuid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SYNCED_COSMETIC_CACHE.containsKey(uuid)) {
|
||||||
|
long lastSync = LAST_SYNC_TIME.getOrDefault(uuid, 0L);
|
||||||
|
if (System.currentTimeMillis() - lastSync < 300000) {
|
||||||
|
return SYNCED_COSMETIC_CACHE.get(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocalPlayer(uuid) && !hasSentInitialCosmetics && !isInitializing) {
|
||||||
|
performInitialSync();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PENDING_FETCHES.contains(uuid)) {
|
||||||
|
fetchCosmeticsFromServer(uuid, playerName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<SyncedCosmeticData> fetchCosmeticsFromServer(UUID uuid, String playerName, boolean isInitial) {
|
||||||
|
PENDING_FETCHES.add(uuid);
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
String urlString = API_BASE_URL + "/cosmetics/" + uuid.toString();
|
||||||
|
URL url = new URI(urlString).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(10000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
try (InputStream is = conn.getInputStream()) {
|
||||||
|
String responseBody = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
JsonObject json = GSON.fromJson(responseBody, JsonObject.class);
|
||||||
|
SyncedCosmeticData cosmeticData = SyncedCosmeticData.fromJson(json);
|
||||||
|
|
||||||
|
SYNCED_COSMETIC_CACHE.put(uuid, cosmeticData);
|
||||||
|
LAST_SYNC_TIME.put(uuid, System.currentTimeMillis());
|
||||||
|
return cosmeticData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Exception fetching cosmetics: {}", e.getMessage());
|
||||||
|
} finally {
|
||||||
|
PENDING_FETCHES.remove(uuid);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendCosmeticsToServer(UUID uuid, SyncedCosmeticData cosmeticData, String playerName, boolean isInitial) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/cosmetics").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(3000);
|
||||||
|
conn.setReadTimeout(3000);
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", uuid.toString());
|
||||||
|
json.addProperty("playerName", playerName);
|
||||||
|
json.add("cosmetics", cosmeticData.toJson());
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
if (isInitial) hasSentInitialCosmetics = true;
|
||||||
|
SYNCED_COSMETIC_CACHE.put(uuid, cosmeticData);
|
||||||
|
LAST_SYNC_TIME.put(uuid, System.currentTimeMillis());
|
||||||
|
PENDING_UPDATES.remove(uuid);
|
||||||
|
Client.debugLog("Successfully synced cosmetics to server for " + playerName);
|
||||||
|
} else {
|
||||||
|
PENDING_UPDATES.put(uuid, cosmeticData);
|
||||||
|
Client.logger.error("Failed to sync cosmetics. HTTP {}", conn.getResponseCode());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to send cosmetics to server", e);
|
||||||
|
PENDING_UPDATES.put(uuid, cosmeticData);
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendCosmeticsToServer(UUID uuid, ClientCosmeticManager.PlayerCosmeticData clientData) {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) {
|
||||||
|
sendCosmeticsToServer(uuid, clientData, client.player.getName().getString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendCosmeticsToServer(UUID uuid, ClientCosmeticManager.PlayerCosmeticData clientData, String playerName) {
|
||||||
|
SyncedCosmeticData syncData = SyncedCosmeticData.fromClientCosmeticData(clientData);
|
||||||
|
sendCosmeticsToServer(uuid, syncData, playerName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyServerCosmeticsToLocal(UUID uuid, SyncedCosmeticData cosmeticData) {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
ClientCosmeticManager.PlayerCosmeticData localData = ClientCosmeticManager.getData(uuid);
|
||||||
|
ClientCosmeticManager.PlayerCosmeticData serverData = cosmeticData.toClientCosmeticData();
|
||||||
|
|
||||||
|
localData.activeWings = serverData.activeWings;
|
||||||
|
localData.wingsVisible = serverData.wingsVisible;
|
||||||
|
localData.activeHat = serverData.activeHat;
|
||||||
|
localData.hatVisible = serverData.hatVisible;
|
||||||
|
localData.activeHalo = serverData.activeHalo;
|
||||||
|
localData.haloVisible = serverData.haloVisible;
|
||||||
|
|
||||||
|
ClientCosmeticManager.save();
|
||||||
|
Client.debugLog("Applied server cosmetics for " + uuid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void batchFetchCosmetics(List<UUID> uuids) {
|
||||||
|
if (uuids.isEmpty()) return;
|
||||||
|
|
||||||
|
List<UUID> citrusUsers = uuids.stream()
|
||||||
|
.filter(ClientRegistrationManager::isPlayerOnlineWithCitrus)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (citrusUsers.isEmpty()) return;
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/cosmetics/batch").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
JsonArray uuidArray = new JsonArray();
|
||||||
|
for (UUID uuid : citrusUsers) {
|
||||||
|
uuidArray.add(uuid.toString());
|
||||||
|
}
|
||||||
|
json.add("uuids", uuidArray);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
try (InputStream is = conn.getInputStream()) {
|
||||||
|
String response = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
JsonObject result = GSON.fromJson(response, JsonObject.class);
|
||||||
|
|
||||||
|
for (String uuidStr : result.keySet()) {
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(uuidStr);
|
||||||
|
JsonObject cosmeticDataJson = result.getAsJsonObject(uuidStr);
|
||||||
|
SyncedCosmeticData cosmeticData = SyncedCosmeticData.fromJson(cosmeticDataJson);
|
||||||
|
|
||||||
|
SYNCED_COSMETIC_CACHE.put(uuid, cosmeticData);
|
||||||
|
LAST_SYNC_TIME.put(uuid, System.currentTimeMillis());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Invalid UUID in cosmetics response: {}", uuidStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Batch fetched cosmetics for " + result.size() + " users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Batch cosmetics fetch failed", e);
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startAutoSync() {
|
||||||
|
if (syncTimer != null) {
|
||||||
|
syncTimer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
syncTimer = new Timer("CosmeticAutoSync", true);
|
||||||
|
syncTimer.scheduleAtFixedRate(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (autoSyncEnabled && MinecraftClient.getInstance().player != null) {
|
||||||
|
performAutoSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void performAutoSync() {
|
||||||
|
syncLocalPlayerCosmetics();
|
||||||
|
retryPendingUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void syncLocalPlayerCosmetics() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) return;
|
||||||
|
|
||||||
|
UUID uuid = client.player.getUuid();
|
||||||
|
ClientCosmeticManager.PlayerCosmeticData localData = ClientCosmeticManager.getData(uuid);
|
||||||
|
|
||||||
|
if (localData != null) {
|
||||||
|
SyncedCosmeticData cachedData = SYNCED_COSMETIC_CACHE.get(uuid);
|
||||||
|
if (cachedData == null ||
|
||||||
|
!cachedData.wingType.equals(localData.activeWings) ||
|
||||||
|
cachedData.wingsVisible != localData.wingsVisible ||
|
||||||
|
!cachedData.hatType.equals(localData.activeHat) ||
|
||||||
|
cachedData.hatVisible != localData.hatVisible ||
|
||||||
|
!cachedData.haloType.equals(localData.activeHalo) ||
|
||||||
|
cachedData.haloVisible != localData.haloVisible) {
|
||||||
|
|
||||||
|
sendCosmeticsToServer(uuid, localData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void retryPendingUpdates() {
|
||||||
|
if (PENDING_UPDATES.isEmpty()) return;
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) return;
|
||||||
|
|
||||||
|
for (Map.Entry<UUID, SyncedCosmeticData> entry : PENDING_UPDATES.entrySet()) {
|
||||||
|
String playerName = entry.getKey().equals(client.player.getUuid())
|
||||||
|
? client.player.getName().getString()
|
||||||
|
: entry.getKey().toString();
|
||||||
|
|
||||||
|
sendCosmeticsToServer(entry.getKey(), entry.getValue(), playerName, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLocalPlayer(UUID uuid) {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
return client.player != null && client.player.getUuid().equals(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearCache(UUID uuid) {
|
||||||
|
SYNCED_COSMETIC_CACHE.remove(uuid);
|
||||||
|
LAST_SYNC_TIME.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearAllCache() {
|
||||||
|
SYNCED_COSMETIC_CACHE.clear();
|
||||||
|
LAST_SYNC_TIME.clear();
|
||||||
|
PENDING_UPDATES.clear();
|
||||||
|
PENDING_FETCHES.clear();
|
||||||
|
hasSentInitialCosmetics = false;
|
||||||
|
isInitializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasInitialSyncCompleted() {
|
||||||
|
return hasSentInitialCosmetics && !isInitializing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isInitializing() {
|
||||||
|
return isInitializing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void forceInitialSync() {
|
||||||
|
if (!hasSentInitialCosmetics) {
|
||||||
|
isInitializing = true;
|
||||||
|
performInitialSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAutoSyncEnabled() {
|
||||||
|
return autoSyncEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setAutoSyncEnabled(boolean enabled) {
|
||||||
|
autoSyncEnabled = enabled;
|
||||||
|
if (enabled) {
|
||||||
|
startAutoSync();
|
||||||
|
} else if (syncTimer != null) {
|
||||||
|
syncTimer.cancel();
|
||||||
|
syncTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getSyncStatus() {
|
||||||
|
if (!autoSyncEnabled) return "Disabled";
|
||||||
|
if (isInitializing) return "Initializing...";
|
||||||
|
if (hasSentInitialCosmetics) return "Synced";
|
||||||
|
return "Not synced";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics;
|
||||||
|
|
||||||
|
public enum CosmeticType {
|
||||||
|
WINGS,
|
||||||
|
HAT,
|
||||||
|
HALO
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.managers.ClientRegistrationManager;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class PlayerCosmeticTracker {
|
||||||
|
private static final Map<UUID, Long> LAST_SEEN_PLAYERS = new ConcurrentHashMap<>();
|
||||||
|
private static final long SYNC_INTERVAL = 30000;
|
||||||
|
|
||||||
|
public static void tick() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.world == null || client.player == null) return;
|
||||||
|
|
||||||
|
List<UUID> citrusUsersToSync = new ArrayList<>();
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (PlayerEntity player : client.world.getPlayers()) {
|
||||||
|
UUID uuid = player.getUuid();
|
||||||
|
|
||||||
|
if (player == client.player) continue;
|
||||||
|
|
||||||
|
if (!ClientRegistrationManager.isPlayerOnlineWithCitrus(uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long lastSeen = LAST_SEEN_PLAYERS.get(uuid);
|
||||||
|
if (lastSeen == null || currentTime - lastSeen > SYNC_INTERVAL) {
|
||||||
|
citrusUsersToSync.add(uuid);
|
||||||
|
LAST_SEEN_PLAYERS.put(uuid, currentTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!citrusUsersToSync.isEmpty()) {
|
||||||
|
CosmeticSyncManager.batchFetchCosmetics(citrusUsersToSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterator<Map.Entry<UUID, Long>> iterator = LAST_SEEN_PLAYERS.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<UUID, Long> entry = iterator.next();
|
||||||
|
UUID uuid = entry.getKey();
|
||||||
|
|
||||||
|
boolean stillPresent = client.world.getPlayers().stream()
|
||||||
|
.anyMatch(p -> p.getUuid().equals(uuid) && p != client.player);
|
||||||
|
|
||||||
|
if (!stillPresent && currentTime - entry.getValue() > 60000) {
|
||||||
|
iterator.remove();
|
||||||
|
CosmeticSyncManager.clearCache(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTime % 60000 < 50) {
|
||||||
|
CosmeticSyncManager.retryPendingUpdates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,543 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.capes;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.sync.CapeSyncManager;
|
||||||
|
import de.winniepat.citrus.managers.ClientRegistrationManager;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.network.AbstractClientPlayerEntity;
|
||||||
|
import net.minecraft.client.texture.*;
|
||||||
|
import net.minecraft.client.util.SkinTextures;
|
||||||
|
import net.minecraft.util.*;
|
||||||
|
import org.w3c.dom.*;
|
||||||
|
|
||||||
|
import javax.xml.parsers.*;
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
public class CapeHandler {
|
||||||
|
private static final Map<String, CapeEntry> CAPE_CACHE = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<UUID, String> PLAYER_CAPES = new ConcurrentHashMap<>();
|
||||||
|
private static final Set<String> LOADING_CAPES = ConcurrentHashMap.newKeySet();
|
||||||
|
private static final Set<String> FAILED_CAPES = ConcurrentHashMap.newKeySet();
|
||||||
|
private static String localCapeOverride = null;
|
||||||
|
private static boolean overrideFromSync = false;
|
||||||
|
|
||||||
|
private static final String CDN_BASE_URL = Client.CDN_URL + "/capes/";
|
||||||
|
private static List<String> AVAILABLE_CAPES = new ArrayList<>();
|
||||||
|
private static boolean capesListFetched = false;
|
||||||
|
private static boolean isInitialized = false;
|
||||||
|
|
||||||
|
private static final String MOD_ID = "citrus";
|
||||||
|
|
||||||
|
private static class CapeEntry {
|
||||||
|
final Identifier textureId;
|
||||||
|
final NativeImageBackedTexture texture;
|
||||||
|
final long loadTime;
|
||||||
|
|
||||||
|
CapeEntry(Identifier textureId, NativeImageBackedTexture texture) {
|
||||||
|
this.textureId = textureId;
|
||||||
|
this.texture = texture;
|
||||||
|
this.loadTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void fetchCapeListFromCDN() {
|
||||||
|
Client.debugLog("Fetching cape list from CDN...");
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(CDN_BASE_URL).toURL();
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setConnectTimeout(10000);
|
||||||
|
connection.setReadTimeout(10000);
|
||||||
|
|
||||||
|
if (connection.getResponseCode() == 200) {
|
||||||
|
try (InputStream stream = connection.getInputStream()) {
|
||||||
|
byte[] xmlData = stream.readAllBytes();
|
||||||
|
parseCapeListFromXML(xmlData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Client.logger.error("Failed to fetch cape list. HTTP {}", connection.getResponseCode());
|
||||||
|
loadDefaultCapes();
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.disconnect();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error fetching cape list", e);
|
||||||
|
loadDefaultCapes();
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseCapeListFromXML(byte[] xmlData) {
|
||||||
|
try {
|
||||||
|
List<String> discoveredCapes = new ArrayList<>();
|
||||||
|
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
Document doc = builder.parse(new ByteArrayInputStream(xmlData));
|
||||||
|
|
||||||
|
NodeList keyNodes = doc.getElementsByTagName("Key");
|
||||||
|
|
||||||
|
for (int i = 0; i < keyNodes.getLength(); i++) {
|
||||||
|
Node node = keyNodes.item(i);
|
||||||
|
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||||
|
String keyValue = node.getTextContent().trim();
|
||||||
|
|
||||||
|
if (keyValue.toLowerCase().endsWith(".png")) {
|
||||||
|
String capeName = keyValue.substring(0, keyValue.length() - 4);
|
||||||
|
discoveredCapes.add(capeName);
|
||||||
|
|
||||||
|
Client.debugLog("Found cape: " + capeName + " (file: " + keyValue + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discoveredCapes.isEmpty()) {
|
||||||
|
Client.logger.error("No PNG files found in CDN response");
|
||||||
|
loadDefaultCapes();
|
||||||
|
} else {
|
||||||
|
AVAILABLE_CAPES = discoveredCapes;
|
||||||
|
capesListFetched = true;
|
||||||
|
|
||||||
|
Client.debugLog("Successfully loaded " + discoveredCapes.size() + " capes from CDN");
|
||||||
|
|
||||||
|
preloadLocalPlayerCape();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error parsing cape list from XML", e);
|
||||||
|
loadDefaultCapes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void loadDefaultCapes() {
|
||||||
|
Client.debugLog("Using default cape list");
|
||||||
|
|
||||||
|
AVAILABLE_CAPES = Arrays.asList("strick", "cat");
|
||||||
|
capesListFetched = true;
|
||||||
|
preloadLocalPlayerCape();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ensureCapeListFetched() {
|
||||||
|
if (!capesListFetched && !isInitialized) {
|
||||||
|
fetchCapeListFromCDN();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SkinTextures getCapes(SkinTextures original, AbstractClientPlayerEntity player) {
|
||||||
|
if (player == null) return original;
|
||||||
|
|
||||||
|
if (!ClientRegistrationManager.isPlayerOnlineWithCitrus(player.getUuid())) {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID playerUUID = player.getUuid();
|
||||||
|
String playerName = player.getName().getString();
|
||||||
|
|
||||||
|
String capeType = getCapeForPlayer(playerUUID, playerName);
|
||||||
|
|
||||||
|
if (capeType == null || "none".equals(capeType) || FAILED_CAPES.contains(capeType)) {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
CapeEntry capeEntry = CAPE_CACHE.get(capeType);
|
||||||
|
if (capeEntry != null) {
|
||||||
|
return new SkinTextures(
|
||||||
|
original.texture(),
|
||||||
|
original.textureUrl(),
|
||||||
|
capeEntry.textureId,
|
||||||
|
capeEntry.textureId,
|
||||||
|
original.model(),
|
||||||
|
original.secure()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (!LOADING_CAPES.contains(capeType) && !FAILED_CAPES.contains(capeType)) {
|
||||||
|
startCapeLoading(capeType);
|
||||||
|
}
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getCapeForPlayer(UUID uuid, String name) {
|
||||||
|
if (!ClientRegistrationManager.isPlayerOnlineWithCitrus(uuid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null && client.player.getUuid().equals(uuid) && localCapeOverride != null) {
|
||||||
|
return localCapeOverride.equals("none") ? null : localCapeOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PLAYER_CAPES.containsKey(uuid)) {
|
||||||
|
return PLAYER_CAPES.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
String syncedCape = CapeSyncManager.getSyncedCape(uuid, name);
|
||||||
|
if (syncedCape != null) {
|
||||||
|
PLAYER_CAPES.put(uuid, syncedCape);
|
||||||
|
return syncedCape;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AVAILABLE_CAPES.isEmpty()) {
|
||||||
|
ensureCapeListFetched();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hash = Math.abs((uuid + name).hashCode());
|
||||||
|
int index = hash % AVAILABLE_CAPES.size();
|
||||||
|
String capeType = AVAILABLE_CAPES.get(index);
|
||||||
|
|
||||||
|
PLAYER_CAPES.put(uuid, capeType);
|
||||||
|
return capeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startCapeLoading(String capeType) {
|
||||||
|
if (LOADING_CAPES.contains(capeType)) return;
|
||||||
|
|
||||||
|
LOADING_CAPES.add(capeType);
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
byte[] imageData = downloadCapeImage(capeType);
|
||||||
|
|
||||||
|
if (imageData.length == 0) {
|
||||||
|
Client.logger.error("Empty image data for cape {}", capeType);
|
||||||
|
markCapeAsFailed(capeType, "Empty data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValidPng(imageData)) {
|
||||||
|
Client.logger.error("Invalid PNG signature for cape {}", capeType);
|
||||||
|
Client.logger.error("First 8 bytes: {}", bytesToHex(imageData, 8));
|
||||||
|
markCapeAsFailed(capeType, "Invalid PNG signature");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeImage nativeImage = null;
|
||||||
|
try {
|
||||||
|
nativeImage = NativeImage.read(new ByteArrayInputStream(imageData));
|
||||||
|
|
||||||
|
if (nativeImage.getWidth() > 512 || nativeImage.getHeight() > 512) {
|
||||||
|
|
||||||
|
Client.logger.error("Cape image too large: {} ({}x{})", capeType, nativeImage.getWidth(), nativeImage.getHeight());
|
||||||
|
nativeImage.close();
|
||||||
|
markCapeAsFailed(capeType, "Image too large");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleTextureCreation(capeType, nativeImage);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to parse PNG for cape {}", capeType, e);
|
||||||
|
if (nativeImage != null) {
|
||||||
|
nativeImage.close();
|
||||||
|
}
|
||||||
|
markCapeAsFailed(capeType, "PNG parsing failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to load cape {}", capeType, e);
|
||||||
|
markCapeAsFailed(capeType, "Download failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] downloadCapeImage(String capeType) throws Exception {
|
||||||
|
String capeUrl = CDN_BASE_URL + capeType + ".png";
|
||||||
|
|
||||||
|
Client.debugLog("Downloading cape from: " + capeUrl);
|
||||||
|
|
||||||
|
URL url = new URI(capeUrl).toURL();
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestProperty("User-Agent", "Minecraft Mod");
|
||||||
|
connection.setConnectTimeout(10000);
|
||||||
|
connection.setReadTimeout(10000);
|
||||||
|
connection.setInstanceFollowRedirects(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
|
||||||
|
Client.debugLog("Response code for " + capeType + ": " + responseCode);
|
||||||
|
|
||||||
|
if (responseCode != 200) {
|
||||||
|
try (InputStream errorStream = connection.getErrorStream()) {
|
||||||
|
if (errorStream != null) {
|
||||||
|
byte[] errorBytes = errorStream.readAllBytes();
|
||||||
|
Client.logger.error("Error response {}", new String(errorBytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IOException("HTTP " + responseCode + " for " + capeUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream stream = connection.getInputStream()) {
|
||||||
|
return stream.readAllBytes();
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValidPng(byte[] data) {
|
||||||
|
if (data.length < 8) return false;
|
||||||
|
|
||||||
|
return (data[0] & 0xFF) == 0x89 &&
|
||||||
|
data[1] == 0x50 &&
|
||||||
|
data[2] == 0x4E &&
|
||||||
|
data[3] == 0x47 &&
|
||||||
|
data[4] == 0x0D &&
|
||||||
|
data[5] == 0x0A &&
|
||||||
|
data[6] == 0x1A &&
|
||||||
|
data[7] == 0x0A;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHex(byte[] bytes, int length) {
|
||||||
|
StringBuilder hex = new StringBuilder();
|
||||||
|
for (int i = 0; i < Math.min(bytes.length, length); i++) {
|
||||||
|
hex.append(String.format("%02X ", bytes[i] & 0xFF));
|
||||||
|
}
|
||||||
|
return hex.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void markCapeAsFailed(String capeType, String reason) {
|
||||||
|
FAILED_CAPES.add(capeType);
|
||||||
|
LOADING_CAPES.remove(capeType);
|
||||||
|
Client.logger.error("Marked cape '{}' as failed: {}", capeType, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void scheduleTextureCreation(String capeType, NativeImage nativeImage) {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
try {
|
||||||
|
if (CAPE_CACHE.containsKey(capeType)) {
|
||||||
|
nativeImage.close();
|
||||||
|
LOADING_CAPES.remove(capeType);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Identifier textureId = Identifier.of(MOD_ID, "capes/" + capeType);
|
||||||
|
NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage);
|
||||||
|
MinecraftClient.getInstance().getTextureManager().registerTexture(textureId, texture);
|
||||||
|
|
||||||
|
CAPE_CACHE.put(capeType, new CapeEntry(textureId, texture));
|
||||||
|
FAILED_CAPES.remove(capeType);
|
||||||
|
|
||||||
|
Client.debugLog("Successfully loaded cape: " + capeType + " (" + nativeImage.getWidth() + "x" + nativeImage.getHeight() + ")");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to create texture for cape {}", capeType, e);
|
||||||
|
nativeImage.close();
|
||||||
|
markCapeAsFailed(capeType, "Texture creation failed: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
LOADING_CAPES.remove(capeType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void preloadLocalPlayerCape() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) {
|
||||||
|
String capeType = getCapeForPlayer(client.player.getUuid(), client.player.getName().getString());
|
||||||
|
if (capeType != null && !capeType.equals("none")) {
|
||||||
|
startCapeLoading(capeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> getAvailableCapes() {
|
||||||
|
ensureCapeListFetched();
|
||||||
|
return new ArrayList<>(AVAILABLE_CAPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCapeLoaded(String capeType) {
|
||||||
|
return CAPE_CACHE.containsKey(capeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setLocalPlayerCape(String capeType, boolean fromSync) {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) return;
|
||||||
|
|
||||||
|
UUID uuid = client.player.getUuid();
|
||||||
|
String playerName = client.player.getName().getString();
|
||||||
|
|
||||||
|
if (fromSync) {
|
||||||
|
overrideFromSync = true;
|
||||||
|
localCapeOverride = capeType;
|
||||||
|
PLAYER_CAPES.put(uuid, capeType);
|
||||||
|
} else {
|
||||||
|
overrideFromSync = false;
|
||||||
|
localCapeOverride = capeType;
|
||||||
|
|
||||||
|
CapeSyncManager.sendCapeToServer(uuid, capeType, playerName);
|
||||||
|
|
||||||
|
PLAYER_CAPES.put(uuid, capeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capeType != null && !capeType.equals("none") && !CAPE_CACHE.containsKey(capeType)) {
|
||||||
|
startCapeLoading(capeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.player.getSkinTextures();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearLocalPlayerOverride() {
|
||||||
|
localCapeOverride = null;
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) {
|
||||||
|
PLAYER_CAPES.remove(client.player.getUuid());
|
||||||
|
client.player.getSkinTextures();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCurrentCapeForLocalPlayer() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) return null;
|
||||||
|
|
||||||
|
if (localCapeOverride != null) {
|
||||||
|
return localCapeOverride.equals("none") ? null : localCapeOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PLAYER_CAPES.get(client.player.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getAssignedCapeForLocalPlayer() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) return null;
|
||||||
|
|
||||||
|
UUID uuid = client.player.getUuid();
|
||||||
|
if (PLAYER_CAPES.containsKey(uuid)) {
|
||||||
|
return PLAYER_CAPES.get(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureCapeListFetched();
|
||||||
|
if (AVAILABLE_CAPES.isEmpty()) return null;
|
||||||
|
|
||||||
|
String name = client.player.getName().getString();
|
||||||
|
int hash = Math.abs((uuid.toString() + name).hashCode());
|
||||||
|
int index = hash % AVAILABLE_CAPES.size();
|
||||||
|
return AVAILABLE_CAPES.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean capeExists(String capeName) {
|
||||||
|
ensureCapeListFetched();
|
||||||
|
return AVAILABLE_CAPES.contains(capeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void preloadSpecificCapes(Collection<String> capeTypes) {
|
||||||
|
for (String capeType : capeTypes) {
|
||||||
|
if (capeType != null &&
|
||||||
|
!capeType.equals("none") &&
|
||||||
|
!CAPE_CACHE.containsKey(capeType) &&
|
||||||
|
!LOADING_CAPES.contains(capeType) &&
|
||||||
|
!FAILED_CAPES.contains(capeType)) {
|
||||||
|
startCapeLoading(capeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void reloadCapes() {
|
||||||
|
clearCache();
|
||||||
|
capesListFetched = false;
|
||||||
|
fetchCapeListFromCDN();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void retryFailedCapes() {
|
||||||
|
List<String> failed = new ArrayList<>(FAILED_CAPES);
|
||||||
|
FAILED_CAPES.clear();
|
||||||
|
for (String capeType : failed) {
|
||||||
|
startCapeLoading(capeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearCache() {
|
||||||
|
for (CapeEntry entry : CAPE_CACHE.values()) {
|
||||||
|
entry.texture.close();
|
||||||
|
}
|
||||||
|
CAPE_CACHE.clear();
|
||||||
|
PLAYER_CAPES.clear();
|
||||||
|
LOADING_CAPES.clear();
|
||||||
|
FAILED_CAPES.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCapeStatus(String capeType) {
|
||||||
|
if (CAPE_CACHE.containsKey(capeType)) {
|
||||||
|
return "loaded";
|
||||||
|
} else if (LOADING_CAPES.contains(capeType)) {
|
||||||
|
return "loading";
|
||||||
|
} else if (FAILED_CAPES.contains(capeType)) {
|
||||||
|
return "failed";
|
||||||
|
} else {
|
||||||
|
return "not_loaded";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
if (isInitialized) return;
|
||||||
|
|
||||||
|
Client.debugLog("Initializing dynamic cape system...");
|
||||||
|
isInitialized = true;
|
||||||
|
fetchCapeListFromCDN();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void refreshCapeList() {
|
||||||
|
capesListFetched = false;
|
||||||
|
fetchCapeListFromCDN();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getLocalCapeOverride() {
|
||||||
|
return localCapeOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getSyncedCapeForPlayer(UUID uuid, String name) {
|
||||||
|
String syncedCape = CapeSyncManager.getSyncedCape(uuid, name);
|
||||||
|
if (syncedCape != null) {
|
||||||
|
return syncedCape;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null && client.player.getUuid().equals(uuid) && localCapeOverride != null) {
|
||||||
|
return localCapeOverride.equals("none") ? null : localCapeOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AVAILABLE_CAPES.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int hash = Math.abs((uuid.toString() + name).hashCode());
|
||||||
|
int index = hash % AVAILABLE_CAPES.size();
|
||||||
|
String capeType = AVAILABLE_CAPES.get(index);
|
||||||
|
|
||||||
|
PLAYER_CAPES.put(uuid, capeType);
|
||||||
|
return capeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void forceCacheCape(UUID uuid, String capeType) {
|
||||||
|
if (capeType == null || "none".equals(capeType)) {
|
||||||
|
PLAYER_CAPES.remove(uuid);
|
||||||
|
} else {
|
||||||
|
PLAYER_CAPES.put(uuid, capeType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void cachePlayerCape(UUID playerUUID, String capeType) {
|
||||||
|
if (capeType == null || "none".equals(capeType)) {
|
||||||
|
PLAYER_CAPES.put(playerUUID, null);
|
||||||
|
} else {
|
||||||
|
PLAYER_CAPES.put(playerUUID, capeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null && client.player.getUuid().equals(playerUUID)) {
|
||||||
|
localCapeOverride = capeType;
|
||||||
|
overrideFromSync = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Cached cape for " + playerUUID + ": " + (capeType == null ? "none" : capeType));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,542 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.capes.sync;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.CapeHandler;
|
||||||
|
import de.winniepat.citrus.managers.ClientRegistrationManager;
|
||||||
|
import de.winniepat.citrus.utils.SecurityUtils;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
public class CapeSyncManager {
|
||||||
|
private static final String API_BASE_URL = Client.API_URL;
|
||||||
|
private static final Gson GSON = new GsonBuilder().create();
|
||||||
|
|
||||||
|
public static final Map<UUID, String> SYNCED_CAPE_CACHE = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<UUID, Long> LAST_SYNC_TIME = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final Map<UUID, String> PENDING_UPDATES = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final Set<UUID> PENDING_FETCHES = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private static boolean hasSentInitialCape = false;
|
||||||
|
private static boolean isInitializing = false;
|
||||||
|
|
||||||
|
private static boolean serverReachable = true;
|
||||||
|
private static long lastServerCheck = 0;
|
||||||
|
private static final long SERVER_CHECK_INTERVAL = 30000;
|
||||||
|
|
||||||
|
private static boolean autoSyncEnabled = true;
|
||||||
|
private static long lastAutoSyncTime = 0;
|
||||||
|
private static final long AUTO_SYNC_INTERVAL = 5000;
|
||||||
|
|
||||||
|
private static boolean serverHealthy = false;
|
||||||
|
|
||||||
|
private static Timer syncTimer;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
Client.debugLog("Initializing cape sync...");
|
||||||
|
isInitializing = true;
|
||||||
|
|
||||||
|
startAutoSync();
|
||||||
|
|
||||||
|
checkServerHealth().thenAccept(healthy -> {
|
||||||
|
serverReachable = healthy;
|
||||||
|
if (healthy) {
|
||||||
|
Client.debugLog("Cape sync server is online");
|
||||||
|
scheduleInitialSync();
|
||||||
|
} else {
|
||||||
|
Client.logger.error("Cape sync server is offline");
|
||||||
|
isInitializing = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void scheduleInitialSync() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) {
|
||||||
|
performInitialSync();
|
||||||
|
} else {
|
||||||
|
Util.getMainWorkerExecutor().execute(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
performInitialSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void performInitialSync() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) {
|
||||||
|
isInitializing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID uuid = client.player.getUuid();
|
||||||
|
String playerName = client.player.getName().getString();
|
||||||
|
|
||||||
|
Client.debugLog("Performing initial cape sync for " + playerName + " (" + uuid + ")");
|
||||||
|
|
||||||
|
fetchCapeFromServer(uuid, playerName, true).thenAccept(serverCape -> {
|
||||||
|
if (serverCape != null) {
|
||||||
|
Client.debugLog("Server has cape: " + serverCape);
|
||||||
|
applyServerCapeToLocal(uuid, serverCape);
|
||||||
|
} else {
|
||||||
|
String localCape = CapeHandler.getCurrentCapeForLocalPlayer();
|
||||||
|
if (localCape != null && !"none".equals(localCape)) {
|
||||||
|
Client.debugLog("Sending local cape to server: " + localCape);
|
||||||
|
sendCapeToServer(uuid, localCape, playerName, true).thenAccept(success -> {
|
||||||
|
if (success) {
|
||||||
|
Client.debugLog("Initial cape sync completed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Client.debugLog("No local cape to sync");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasSentInitialCape = true;
|
||||||
|
isInitializing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getSyncedCape(UUID uuid, String playerName) {
|
||||||
|
|
||||||
|
if (!ClientRegistrationManager.isPlayerOnlineWithCitrus(uuid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInitializing && isLocalPlayer(uuid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SYNCED_CAPE_CACHE.containsKey(uuid)) {
|
||||||
|
long lastSync = LAST_SYNC_TIME.getOrDefault(uuid, 0L);
|
||||||
|
if (System.currentTimeMillis() - lastSync < 300000) {
|
||||||
|
return SYNCED_CAPE_CACHE.get(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocalPlayer(uuid) && !hasSentInitialCape && !isInitializing) {
|
||||||
|
performInitialSync();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PENDING_FETCHES.contains(uuid)) {
|
||||||
|
fetchCapeFromServer(uuid, playerName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<String> fetchCapeFromServer(UUID uuid, String playerName, boolean isInitial) {
|
||||||
|
PENDING_FETCHES.add(uuid);
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
String urlString = API_BASE_URL + "/cape/" + uuid.toString();
|
||||||
|
Client.debugLog("Fetching from: " + urlString);
|
||||||
|
|
||||||
|
URL url = new URI(urlString).toURL();
|
||||||
|
conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(10000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
|
||||||
|
conn.setRequestProperty("User-Agent", "Minecraft/Citrus");
|
||||||
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
|
conn.setRequestProperty("Accept-Charset", "UTF-8");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
|
||||||
|
conn.setRequestProperty("Connection", "close");
|
||||||
|
|
||||||
|
Client.debugLog("Opening connection with headers: " + conn.getRequestProperties());
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
Client.debugLog("Response code: " + responseCode);
|
||||||
|
|
||||||
|
if (responseCode == 200) {
|
||||||
|
serverReachable = true;
|
||||||
|
try (InputStream is = conn.getInputStream()) {
|
||||||
|
byte[] response = is.readAllBytes();
|
||||||
|
String responseBody = new String(response, StandardCharsets.UTF_8);
|
||||||
|
Client.debugLog("Response: " + responseBody);
|
||||||
|
|
||||||
|
JsonObject json = GSON.fromJson(responseBody, JsonObject.class);
|
||||||
|
|
||||||
|
if (json.has("cape")) {
|
||||||
|
String cape = json.get("cape").getAsString();
|
||||||
|
Client.debugLog("Successfully got cape: " + cape);
|
||||||
|
|
||||||
|
if (!"none".equals(cape)) {
|
||||||
|
SYNCED_CAPE_CACHE.put(uuid, cape);
|
||||||
|
LAST_SYNC_TIME.put(uuid, System.currentTimeMillis());
|
||||||
|
return cape;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try (InputStream es = conn.getErrorStream()) {
|
||||||
|
if (es != null) {
|
||||||
|
String errorBody = new String(es.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
Client.logger.error("Error response {}", errorBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverReachable = false;
|
||||||
|
Client.logger.error("Server returned error code {}", responseCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
serverReachable = false;
|
||||||
|
Client.logger.error("Exception type {}: {}", e.getClass().getName(), e);
|
||||||
|
|
||||||
|
switch (e) {
|
||||||
|
case ConnectException connectException ->
|
||||||
|
Client.logger.error("Connection refused. Is the server running?");
|
||||||
|
case SocketTimeoutException socketTimeoutException ->
|
||||||
|
Client.logger.error("Connection timeout. Check firewall/network.");
|
||||||
|
case UnknownHostException unknownHostException ->
|
||||||
|
Client.logger.error("Unknown host. Check DNS/network.");
|
||||||
|
default -> Client.logger.error("Error Conneccting to Cape Server {}", String.valueOf(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
if (conn != null) {
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
PENDING_FETCHES.remove(uuid);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<Boolean> sendCapeToServer(UUID uuid, String cape, String playerName, boolean isInitial) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/cape").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("User-Agent", "Citrus/1.0");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(3000);
|
||||||
|
conn.setReadTimeout(3000);
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", uuid.toString());
|
||||||
|
json.addProperty("cape", cape == null ? "none" : cape);
|
||||||
|
json.addProperty("playerName", playerName);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
|
||||||
|
if (responseCode == 200) {
|
||||||
|
serverReachable = true;
|
||||||
|
if (isInitial) {
|
||||||
|
hasSentInitialCape = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SYNCED_CAPE_CACHE.put(uuid, cape);
|
||||||
|
LAST_SYNC_TIME.put(uuid, System.currentTimeMillis());
|
||||||
|
|
||||||
|
PENDING_UPDATES.remove(uuid);
|
||||||
|
|
||||||
|
if (!isInitial) {
|
||||||
|
Client.debugLog("Successfully synced cape to server: " + cape);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
serverReachable = false;
|
||||||
|
if (!isInitial) {
|
||||||
|
Client.logger.error("Failed to sync cape to server. HTTP {}", responseCode);
|
||||||
|
}
|
||||||
|
PENDING_UPDATES.put(uuid, cape);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
serverReachable = false;
|
||||||
|
if (!isInitial) {
|
||||||
|
Client.logger.error("Failed to send cape to server", e);
|
||||||
|
}
|
||||||
|
PENDING_UPDATES.put(uuid, cape);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendCapeToServer(UUID uuid, String cape, String playerName) {
|
||||||
|
sendCapeToServer(uuid, cape, playerName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyServerCapeToLocal(UUID uuid, String cape) {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
CapeHandler.setLocalPlayerCape(cape, true);
|
||||||
|
Client.debugLog("Applied server cape: " + cape);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Boolean> checkServerHealth() {
|
||||||
|
lastServerCheck = System.currentTimeMillis();
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/health").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setConnectTimeout(3000);
|
||||||
|
|
||||||
|
boolean healthy = conn.getResponseCode() == 200;
|
||||||
|
serverHealthy = conn.getResponseCode() == 200;
|
||||||
|
conn.disconnect();
|
||||||
|
return healthy;
|
||||||
|
} catch (Exception e) {
|
||||||
|
serverHealthy = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldTryServer() {
|
||||||
|
if (!serverReachable) {
|
||||||
|
long timeSinceLastCheck = System.currentTimeMillis() - lastServerCheck;
|
||||||
|
return timeSinceLastCheck > SERVER_CHECK_INTERVAL;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLocalPlayer(UUID uuid) {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
return client.player != null && client.player.getUuid().equals(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void batchFetchCapes(List<UUID> uuids) {
|
||||||
|
if (uuids.isEmpty()) return;
|
||||||
|
|
||||||
|
List<UUID> citrusUsers = uuids.stream()
|
||||||
|
.filter(ClientRegistrationManager::isPlayerOnlineWithCitrus)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (citrusUsers.isEmpty()) return;
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/capes/batch").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
List<String> uuidStrings = new ArrayList<>();
|
||||||
|
for (UUID uuid : uuids) {
|
||||||
|
uuidStrings.add(uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.add("uuids", GSON.toJsonTree(uuidStrings));
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
serverReachable = true;
|
||||||
|
try (InputStream is = conn.getInputStream()) {
|
||||||
|
byte[] response = is.readAllBytes();
|
||||||
|
JsonObject result = GSON.fromJson(new String(response), JsonObject.class);
|
||||||
|
|
||||||
|
for (String uuidStr : result.keySet()) {
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(uuidStr);
|
||||||
|
JsonObject capeData = result.getAsJsonObject(uuidStr);
|
||||||
|
String cape = capeData.get("cape").getAsString();
|
||||||
|
|
||||||
|
if (!"none".equals(cape)) {
|
||||||
|
CapeHandler.cachePlayerCape(uuid, cape);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Invalid UUID in response: {}", uuidStr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Fetched capes for " + result.size() + " Citrus users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Batch fetch failed", e);
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void retryPendingUpdates() {
|
||||||
|
if (PENDING_UPDATES.isEmpty()) return;
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) return;
|
||||||
|
|
||||||
|
for (Map.Entry<UUID, String> entry : PENDING_UPDATES.entrySet()) {
|
||||||
|
String playerName = entry.getKey().equals(client.player.getUuid())
|
||||||
|
? client.player.getName().getString()
|
||||||
|
: entry.getKey().toString();
|
||||||
|
|
||||||
|
sendCapeToServer(entry.getKey(), entry.getValue(), playerName, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearCache(UUID uuid) {
|
||||||
|
SYNCED_CAPE_CACHE.remove(uuid);
|
||||||
|
LAST_SYNC_TIME.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearAllCache() {
|
||||||
|
SYNCED_CAPE_CACHE.clear();
|
||||||
|
LAST_SYNC_TIME.clear();
|
||||||
|
PENDING_UPDATES.clear();
|
||||||
|
PENDING_FETCHES.clear();
|
||||||
|
hasSentInitialCape = false;
|
||||||
|
isInitializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<UUID, String> getAllCapeAssignments() {
|
||||||
|
return new HashMap<>(SYNCED_CAPE_CACHE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasInitialSyncCompleted() {
|
||||||
|
return hasSentInitialCape && !isInitializing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void forceInitialSync() {
|
||||||
|
if (!hasSentInitialCape) {
|
||||||
|
isInitializing = true;
|
||||||
|
performInitialSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startAutoSync() {
|
||||||
|
if (syncTimer != null) {
|
||||||
|
syncTimer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
syncTimer = new Timer("CapeAutoSync", true);
|
||||||
|
syncTimer.scheduleAtFixedRate(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (autoSyncEnabled && System.currentTimeMillis() - lastAutoSyncTime >= AUTO_SYNC_INTERVAL) {
|
||||||
|
performAutoSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000, AUTO_SYNC_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void performAutoSync() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null || client.world == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastAutoSyncTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
syncLocalPlayerCape();
|
||||||
|
|
||||||
|
syncNearbyPlayers();
|
||||||
|
|
||||||
|
retryPendingUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void syncLocalPlayerCape() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) return;
|
||||||
|
|
||||||
|
UUID uuid = client.player.getUuid();
|
||||||
|
String playerName = client.player.getName().getString();
|
||||||
|
String currentCape = CapeHandler.getCurrentCapeForLocalPlayer();
|
||||||
|
|
||||||
|
if (currentCape != null && !"none".equals(currentCape)) {
|
||||||
|
Long lastSync = LAST_SYNC_TIME.get(uuid);
|
||||||
|
if (lastSync == null || System.currentTimeMillis() - lastSync > 30000) {
|
||||||
|
sendCapeToServer(uuid, currentCape, playerName, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void syncNearbyPlayers() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.world == null || client.player == null) return;
|
||||||
|
|
||||||
|
List<UUID> nearbyCitrusUsers = new ArrayList<>();
|
||||||
|
|
||||||
|
for (net.minecraft.entity.player.PlayerEntity player : client.world.getPlayers()) {
|
||||||
|
if (player == client.player) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ClientRegistrationManager.isPlayerOnlineWithCitrus(player.getUuid())) {
|
||||||
|
nearbyCitrusUsers.add(player.getUuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nearbyCitrusUsers.isEmpty()) {
|
||||||
|
batchFetchCapes(nearbyCitrusUsers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setAutoSyncEnabled(boolean enabled) {
|
||||||
|
autoSyncEnabled = enabled;
|
||||||
|
if (enabled) {
|
||||||
|
startAutoSync();
|
||||||
|
Client.debugLog("Auto-sync enabled (every 5 seconds)");
|
||||||
|
} else {
|
||||||
|
if (syncTimer != null) {
|
||||||
|
syncTimer.cancel();
|
||||||
|
syncTimer = null;
|
||||||
|
}
|
||||||
|
Client.debugLog("Auto-sync disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAutoSyncEnabled() {
|
||||||
|
return autoSyncEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void forceSyncNow() {
|
||||||
|
lastAutoSyncTime = System.currentTimeMillis() - AUTO_SYNC_INTERVAL;
|
||||||
|
performAutoSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getAutoSyncStatus() {
|
||||||
|
if (!autoSyncEnabled) return "disabled";
|
||||||
|
|
||||||
|
long timeSinceLastSync = System.currentTimeMillis() - lastAutoSyncTime;
|
||||||
|
long timeUntilNextSync = Math.max(0, AUTO_SYNC_INTERVAL - timeSinceLastSync);
|
||||||
|
|
||||||
|
if (timeSinceLastSync < 1000) {
|
||||||
|
return "just synced";
|
||||||
|
} else {
|
||||||
|
return String.format("next sync in %.1fs", timeUntilNextSync / 1000.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isIsHealthy() {
|
||||||
|
return serverHealthy;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.capes.sync;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.managers.ClientRegistrationManager;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class PlayerCapeTracker {
|
||||||
|
private static final Map<UUID, Long> LAST_SEEN_PLAYERS = new ConcurrentHashMap<>();
|
||||||
|
private static final long SYNC_INTERVAL = 30000;
|
||||||
|
|
||||||
|
public static void tick() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.world == null || client.player == null) return;
|
||||||
|
|
||||||
|
List<UUID> citrusUsersToSync = new ArrayList<>();
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (PlayerEntity player : client.world.getPlayers()) {
|
||||||
|
UUID uuid = player.getUuid();
|
||||||
|
|
||||||
|
if (player == client.player) continue;
|
||||||
|
|
||||||
|
if (!ClientRegistrationManager.isPlayerOnlineWithCitrus(uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long lastSeen = LAST_SEEN_PLAYERS.get(uuid);
|
||||||
|
if (lastSeen == null || currentTime - lastSeen > SYNC_INTERVAL) {
|
||||||
|
citrusUsersToSync.add(uuid);
|
||||||
|
LAST_SEEN_PLAYERS.put(uuid, currentTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!citrusUsersToSync.isEmpty()) {
|
||||||
|
CapeSyncManager.batchFetchCapes(citrusUsersToSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterator<Map.Entry<UUID, Long>> iterator = LAST_SEEN_PLAYERS.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<UUID, Long> entry = iterator.next();
|
||||||
|
UUID uuid = entry.getKey();
|
||||||
|
|
||||||
|
boolean stillPresent = client.world.getPlayers().stream()
|
||||||
|
.anyMatch(p -> p.getUuid().equals(uuid) && p != client.player);
|
||||||
|
|
||||||
|
if (!stillPresent && currentTime - entry.getValue() > 60000) {
|
||||||
|
iterator.remove();
|
||||||
|
CapeSyncManager.clearCache(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTime % 60000 < 50) {
|
||||||
|
CapeSyncManager.retryPendingUpdates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.halo;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.cosmetics.CosmeticSyncManager;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import software.bernie.geckolib.animatable.GeoAnimatable;
|
||||||
|
import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache;
|
||||||
|
import software.bernie.geckolib.animation.*;
|
||||||
|
import software.bernie.geckolib.util.GeckoLibUtil;
|
||||||
|
|
||||||
|
public class CosmeticHalo implements GeoAnimatable {
|
||||||
|
private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this);
|
||||||
|
private final PlayerEntity player;
|
||||||
|
|
||||||
|
public CosmeticHalo(PlayerEntity player) {
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerEntity getPlayer() {
|
||||||
|
return this.player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HaloType getHaloType() {
|
||||||
|
CosmeticSyncManager.SyncedCosmeticData syncedData =
|
||||||
|
CosmeticSyncManager.getSyncedCosmetics(
|
||||||
|
player.getUuid(),
|
||||||
|
player.getName().getString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (syncedData != null && CosmeticSyncManager.hasInitialSyncCompleted()) {
|
||||||
|
return HaloType.fromId(syncedData.haloType);
|
||||||
|
}
|
||||||
|
|
||||||
|
String haloId = de.winniepat.citrus.cosmetics.ClientCosmeticManager.getActiveHalo(player.getUuid());
|
||||||
|
return HaloType.fromId(haloId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldRender() {
|
||||||
|
HaloType type = getHaloType();
|
||||||
|
if (type == HaloType.NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CosmeticSyncManager.SyncedCosmeticData syncedData =
|
||||||
|
CosmeticSyncManager.getSyncedCosmetics(
|
||||||
|
player.getUuid(),
|
||||||
|
player.getName().getString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (syncedData != null && CosmeticSyncManager.hasInitialSyncCompleted()) {
|
||||||
|
return syncedData.haloVisible && !player.isInvisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
return de.winniepat.citrus.cosmetics.ClientCosmeticManager.isHaloVisible(player.getUuid()) && !player.isInvisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
|
||||||
|
controllers.add(new AnimationController<>(this, "controller", 0, state -> {
|
||||||
|
HaloType type = state.getAnimatable().getHaloType();
|
||||||
|
if (type == HaloType.NONE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.setAndContinue(RawAnimation.begin().thenLoop("animation.halo.float"));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimatableInstanceCache getAnimatableInstanceCache() {
|
||||||
|
return this.cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getTick(Object object) {
|
||||||
|
return this.player.age;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.halo;
|
||||||
|
|
||||||
|
import net.minecraft.client.network.AbstractClientPlayerEntity;
|
||||||
|
import net.minecraft.client.render.RenderLayer;
|
||||||
|
import net.minecraft.client.render.VertexConsumerProvider;
|
||||||
|
import net.minecraft.client.render.entity.feature.FeatureRenderer;
|
||||||
|
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||||
|
import net.minecraft.client.render.entity.model.PlayerEntityModel;
|
||||||
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import net.minecraft.util.math.RotationAxis;
|
||||||
|
import software.bernie.geckolib.renderer.GeoObjectRenderer;
|
||||||
|
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
public class HaloFeatureRenderer extends FeatureRenderer<AbstractClientPlayerEntity, PlayerEntityModel<AbstractClientPlayerEntity>> {
|
||||||
|
private final GeoObjectRenderer<CosmeticHalo> haloRenderer;
|
||||||
|
private static final WeakHashMap<PlayerEntity, CosmeticHalo> HALO_CACHE = new WeakHashMap<>();
|
||||||
|
|
||||||
|
public HaloFeatureRenderer(FeatureRendererContext<AbstractClientPlayerEntity, PlayerEntityModel<AbstractClientPlayerEntity>> context) {
|
||||||
|
super(context);
|
||||||
|
this.haloRenderer = new GeoObjectRenderer<>(new HaloModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, AbstractClientPlayerEntity player, float limbAngle, float limbDistance, float tickDelta, float animationProgress, float headYaw, float headPitch) {
|
||||||
|
|
||||||
|
CosmeticHalo halo = HALO_CACHE.computeIfAbsent(player, CosmeticHalo::new);
|
||||||
|
boolean shouldRender = halo.shouldRender();
|
||||||
|
HaloType type = halo.getHaloType();
|
||||||
|
if (!shouldRender) return;
|
||||||
|
|
||||||
|
Identifier texture = this.haloRenderer.getGeoModel().getTextureResource(halo);
|
||||||
|
if (texture == null) return;
|
||||||
|
|
||||||
|
matrices.push();
|
||||||
|
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(headYaw * 0.3f));
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(headPitch * 0.3f));
|
||||||
|
|
||||||
|
matrices.translate(0, -2.0, 0);
|
||||||
|
|
||||||
|
matrices.translate(type.getPosX(), type.getPosY(), type.getPosZ());
|
||||||
|
|
||||||
|
float scale = type.getScale();
|
||||||
|
matrices.scale(scale, scale, scale);
|
||||||
|
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f));
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.haloRenderer.render(
|
||||||
|
matrices, halo, vertexConsumers,
|
||||||
|
RenderLayer.getEntityTranslucent(texture),
|
||||||
|
vertexConsumers.getBuffer(RenderLayer.getEntityTranslucent(texture)),
|
||||||
|
0xF000F0,
|
||||||
|
tickDelta
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to render halo: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
matrices.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void invalidateHaloCache(PlayerEntity player) {
|
||||||
|
HALO_CACHE.remove(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.halo;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import software.bernie.geckolib.model.GeoModel;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
public class HaloModel extends GeoModel<CosmeticHalo> {
|
||||||
|
private static final Identifier DEFAULT_HALO_ANIMATION = Identifier.of("citrus", "animations/halo/default_halo.animation.json");
|
||||||
|
private static final Identifier DEFAULT_HALO_MODEL = Identifier.of("citrus", "geo/halo/default_halo.geo.json");
|
||||||
|
private static final Identifier DEFAULT_HALO_TEXTURE = Identifier.of("citrus", "textures/cosmetics/halo/default_halo.png");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Identifier getModelResource(CosmeticHalo halo) {
|
||||||
|
HaloType type = halo.getHaloType();
|
||||||
|
if (type == HaloType.NONE) return null;
|
||||||
|
|
||||||
|
Identifier modelId = type.getModelId();
|
||||||
|
if (modelId == null) {
|
||||||
|
Client.debugLog("Halo model is null for type: " + type.getId());
|
||||||
|
return DEFAULT_HALO_MODEL;
|
||||||
|
}
|
||||||
|
return modelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Identifier getTextureResource(CosmeticHalo halo) {
|
||||||
|
HaloType type = halo.getHaloType();
|
||||||
|
if (type == HaloType.NONE) return null;
|
||||||
|
|
||||||
|
Identifier textureId = type.getTextureId();
|
||||||
|
if (textureId == null) {
|
||||||
|
Client.debugLog("Halo texture is null for type: " + type.getId());
|
||||||
|
return DEFAULT_HALO_TEXTURE;
|
||||||
|
}
|
||||||
|
return textureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Identifier getAnimationResource(CosmeticHalo halo) {
|
||||||
|
HaloType type = halo.getHaloType();
|
||||||
|
if (type == HaloType.NONE) {
|
||||||
|
Client.debugLog("Animation resource is null for NONE halo type");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Identifier anim = type.getAnimationResource();
|
||||||
|
if (anim == null) {
|
||||||
|
Client.debugLog("Animation resource is null for halo type: " + type.getId());
|
||||||
|
Client.debugLog("Falling back to default halo animation");
|
||||||
|
return DEFAULT_HALO_ANIMATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Halo animation resource: " + anim.toString() + " for type: " + type.getId());
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.halo;
|
||||||
|
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
public enum HaloType {
|
||||||
|
NONE("none", null, null, null, 1.0f, 0.0, 0.0, 0.0),
|
||||||
|
ANGEL_HALO("angel_halo", "angel_halo", "angel_halo.png", "angel_halo", 10f, 0.0, 1.2, 0.1);
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final String modelName;
|
||||||
|
private final String textureName;
|
||||||
|
private final String animationName;
|
||||||
|
private final float scale;
|
||||||
|
private final double posX, posY, posZ;
|
||||||
|
|
||||||
|
HaloType(String id, String modelName, String textureName, String animationName,
|
||||||
|
float scale, double posX, double posY, double posZ) {
|
||||||
|
this.id = id;
|
||||||
|
this.modelName = modelName;
|
||||||
|
this.textureName = textureName;
|
||||||
|
this.animationName = animationName;
|
||||||
|
this.scale = scale;
|
||||||
|
this.posX = posX;
|
||||||
|
this.posY = posY;
|
||||||
|
this.posZ = posZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getModelName() {
|
||||||
|
return modelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getModelId() {
|
||||||
|
if (modelName == null || NONE.id.equals(id)) return null;
|
||||||
|
return Identifier.of("citrus", "geo/halo/" + modelName + ".geo.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getTextureId() {
|
||||||
|
if (textureName == null || NONE.id.equals(id)) return null;
|
||||||
|
return Identifier.of("citrus", "textures/cosmetics/halo/" + textureName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getAnimationResource() {
|
||||||
|
if (animationName == null || NONE.id.equals(id)) return null;
|
||||||
|
return Identifier.of("citrus", "animations/halo/" + animationName + ".animation.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getScale() {
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPosX() {
|
||||||
|
return posX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPosY() {
|
||||||
|
return posY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPosZ() {
|
||||||
|
return posZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HaloType fromId(String id) {
|
||||||
|
if (id == null || id.equals("none") || id.isEmpty()) return NONE;
|
||||||
|
|
||||||
|
for (HaloType type : values()) {
|
||||||
|
if (type.id.equalsIgnoreCase(id)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HaloType[] getAvailableTypes() {
|
||||||
|
return values();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.hat;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.cosmetics.CosmeticSyncManager;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import software.bernie.geckolib.animatable.GeoAnimatable;
|
||||||
|
import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache;
|
||||||
|
import software.bernie.geckolib.animation.*;
|
||||||
|
import software.bernie.geckolib.util.GeckoLibUtil;
|
||||||
|
|
||||||
|
public class CosmeticHat implements GeoAnimatable {
|
||||||
|
private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this);
|
||||||
|
private final PlayerEntity player;
|
||||||
|
|
||||||
|
public CosmeticHat(PlayerEntity player) {
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerEntity getPlayer() {
|
||||||
|
return this.player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HatType getHatType() {
|
||||||
|
CosmeticSyncManager.SyncedCosmeticData syncedData =
|
||||||
|
CosmeticSyncManager.getSyncedCosmetics(
|
||||||
|
player.getUuid(),
|
||||||
|
player.getName().getString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (syncedData != null && CosmeticSyncManager.hasInitialSyncCompleted()) {
|
||||||
|
return HatType.fromId(syncedData.hatType);
|
||||||
|
}
|
||||||
|
|
||||||
|
String hatId = de.winniepat.citrus.cosmetics.ClientCosmeticManager.getActiveHat(player.getUuid());
|
||||||
|
return HatType.fromId(hatId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldRender() {
|
||||||
|
HatType type = getHatType();
|
||||||
|
if (type == HatType.NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CosmeticSyncManager.SyncedCosmeticData syncedData =
|
||||||
|
CosmeticSyncManager.getSyncedCosmetics(
|
||||||
|
player.getUuid(),
|
||||||
|
player.getName().getString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (syncedData != null && CosmeticSyncManager.hasInitialSyncCompleted()) {
|
||||||
|
return syncedData.hatVisible && !player.isInvisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
return de.winniepat.citrus.cosmetics.ClientCosmeticManager.isHatVisible(player.getUuid()) && !player.isInvisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
|
||||||
|
controllers.add(new AnimationController<>(this, "controller", 0, state -> {
|
||||||
|
HatType type = state.getAnimatable().getHatType();
|
||||||
|
if (type == HatType.NONE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.setAndContinue(RawAnimation.begin().thenLoop("animation.hat.idle"));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimatableInstanceCache getAnimatableInstanceCache() {
|
||||||
|
return this.cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getTick(Object object) {
|
||||||
|
return this.player.age;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.hat;
|
||||||
|
|
||||||
|
import net.minecraft.client.network.AbstractClientPlayerEntity;
|
||||||
|
import net.minecraft.client.render.RenderLayer;
|
||||||
|
import net.minecraft.client.render.VertexConsumerProvider;
|
||||||
|
import net.minecraft.client.render.entity.feature.FeatureRenderer;
|
||||||
|
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||||
|
import net.minecraft.client.render.entity.model.PlayerEntityModel;
|
||||||
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import net.minecraft.util.math.RotationAxis;
|
||||||
|
import software.bernie.geckolib.renderer.GeoObjectRenderer;
|
||||||
|
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
public class HatFeatureRenderer extends FeatureRenderer<AbstractClientPlayerEntity, PlayerEntityModel<AbstractClientPlayerEntity>> {
|
||||||
|
private final GeoObjectRenderer<CosmeticHat> hatRenderer;
|
||||||
|
private static final WeakHashMap<PlayerEntity, CosmeticHat> HAT_CACHE = new WeakHashMap<>();
|
||||||
|
|
||||||
|
public HatFeatureRenderer(FeatureRendererContext<AbstractClientPlayerEntity, PlayerEntityModel<AbstractClientPlayerEntity>> context) {
|
||||||
|
super(context);
|
||||||
|
this.hatRenderer = new GeoObjectRenderer<>(new HatModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, AbstractClientPlayerEntity player, float limbAngle, float limbDistance, float tickDelta, float animationProgress, float headYaw, float headPitch) {
|
||||||
|
|
||||||
|
CosmeticHat hat = HAT_CACHE.computeIfAbsent(player, CosmeticHat::new);
|
||||||
|
boolean shouldRender = hat.shouldRender();
|
||||||
|
HatType type = hat.getHatType();
|
||||||
|
if (!shouldRender) return;
|
||||||
|
|
||||||
|
Identifier texture = this.hatRenderer.getGeoModel().getTextureResource(hat);
|
||||||
|
if (texture == null) return;
|
||||||
|
|
||||||
|
matrices.push();
|
||||||
|
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(headYaw * 0.3f));
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(headPitch * 0.3f));
|
||||||
|
|
||||||
|
matrices.translate(0, -1.6, 0);
|
||||||
|
|
||||||
|
matrices.translate(type.getPosX(), type.getPosY(), type.getPosZ());
|
||||||
|
|
||||||
|
float scale = type.getScale();
|
||||||
|
matrices.scale(scale, scale, scale);
|
||||||
|
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(180f));
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.hatRenderer.render(
|
||||||
|
matrices, hat, vertexConsumers,
|
||||||
|
RenderLayer.getEntityTranslucent(texture),
|
||||||
|
vertexConsumers.getBuffer(RenderLayer.getEntityTranslucent(texture)),
|
||||||
|
0xF000F0,
|
||||||
|
tickDelta
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to render hat: " + e.getMessage());
|
||||||
|
} finally {
|
||||||
|
matrices.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void invalidateHatCache(PlayerEntity player) {
|
||||||
|
HAT_CACHE.remove(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.hat;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import software.bernie.geckolib.model.GeoModel;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
public class HatModel extends GeoModel<CosmeticHat> {
|
||||||
|
private static final Identifier DEFAULT_HAT_ANIMATION = Identifier.of("citrus", "animations/hat/default_hat.animation.json");
|
||||||
|
private static final Identifier DEFAULT_HAT_MODEL = Identifier.of("citrus", "geo/hat/default_hat.geo.json");
|
||||||
|
private static final Identifier DEFAULT_HAT_TEXTURE = Identifier.of("citrus", "textures/cosmetics/hat/default_hat.png");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Identifier getModelResource(CosmeticHat hat) {
|
||||||
|
HatType type = hat.getHatType();
|
||||||
|
if (type == HatType.NONE) return null;
|
||||||
|
|
||||||
|
Identifier modelId = type.getModelId();
|
||||||
|
if (modelId == null) {
|
||||||
|
Client.debugLog("Hat model is null for type: " + type.getId());
|
||||||
|
return DEFAULT_HAT_MODEL;
|
||||||
|
}
|
||||||
|
return modelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Identifier getTextureResource(CosmeticHat hat) {
|
||||||
|
HatType type = hat.getHatType();
|
||||||
|
if (type == HatType.NONE) return null;
|
||||||
|
|
||||||
|
Identifier textureId = type.getTextureId();
|
||||||
|
if (textureId == null) {
|
||||||
|
Client.debugLog("Hat texture is null for type: " + type.getId());
|
||||||
|
return DEFAULT_HAT_TEXTURE;
|
||||||
|
}
|
||||||
|
return textureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Identifier getAnimationResource(CosmeticHat hat) {
|
||||||
|
HatType type = hat.getHatType();
|
||||||
|
if (type == HatType.NONE) {
|
||||||
|
Client.debugLog("Animation resource is null for NONE hat type");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Identifier anim = type.getAnimationResource();
|
||||||
|
if (anim == null) {
|
||||||
|
Client.debugLog("Animation resource is null for hat type: " + type.getId());
|
||||||
|
Client.debugLog("Falling back to default hat animation");
|
||||||
|
return DEFAULT_HAT_ANIMATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Hat animation resource: " + anim.toString() + " for type: " + type.getId());
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.hat;
|
||||||
|
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
public enum HatType {
|
||||||
|
NONE("none", null, null, null, 1.0f, 0.0, 0.0, 0.0),
|
||||||
|
TEST("test_hat", "test_hat", "test_hat.png", "test_hat", 1.0f, 2.0, -5.0, -2.0);
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final String modelName;
|
||||||
|
private final String textureName;
|
||||||
|
private final String animationName;
|
||||||
|
private final float scale;
|
||||||
|
private final double posX, posY, posZ;
|
||||||
|
|
||||||
|
HatType(String id, String modelName, String textureName, String animationName,
|
||||||
|
float scale, double posX, double posY, double posZ) {
|
||||||
|
this.id = id;
|
||||||
|
this.modelName = modelName;
|
||||||
|
this.textureName = textureName;
|
||||||
|
this.animationName = animationName;
|
||||||
|
this.scale = scale;
|
||||||
|
this.posX = posX;
|
||||||
|
this.posY = posY;
|
||||||
|
this.posZ = posZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getModelName() {
|
||||||
|
return modelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getModelId() {
|
||||||
|
if (modelName == null || NONE.id.equals(id)) return null;
|
||||||
|
return Identifier.of("citrus", "geo/hat/" + modelName + ".geo.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getTextureId() {
|
||||||
|
if (textureName == null || NONE.id.equals(id)) return null;
|
||||||
|
return Identifier.of("citrus", "textures/cosmetics/hat/" + textureName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getAnimationResource() {
|
||||||
|
if (animationName == null || NONE.id.equals(id)) return null;
|
||||||
|
return Identifier.of("citrus", "animations/hat/" + animationName + ".animation.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getScale() {
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPosX() {
|
||||||
|
return posX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPosY() {
|
||||||
|
return posY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPosZ() {
|
||||||
|
return posZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HatType fromId(String id) {
|
||||||
|
if (id == null || id.equals("none") || id.isEmpty()) return NONE;
|
||||||
|
|
||||||
|
for (HatType type : values()) {
|
||||||
|
if (type.id.equalsIgnoreCase(id)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HatType[] getAvailableTypes() {
|
||||||
|
return values();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.wings;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.cosmetics.wings.sync.WingSyncManager;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ClientWingManager {
|
||||||
|
private static final File CONFIG_FILE = new File(Client.CONFIG_DIR, "citrus_wings.json");
|
||||||
|
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
private static Map<UUID, WingData> wingDataMap = new HashMap<>();
|
||||||
|
|
||||||
|
public static class WingData {
|
||||||
|
public String activeWings = "none";
|
||||||
|
public boolean wingsVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void load() {
|
||||||
|
if (CONFIG_FILE.exists()) {
|
||||||
|
try (FileReader reader = new FileReader(CONFIG_FILE)) {
|
||||||
|
JsonObject json = JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
wingDataMap.clear();
|
||||||
|
|
||||||
|
for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
|
||||||
|
UUID uuid = UUID.fromString(entry.getKey());
|
||||||
|
JsonObject data = entry.getValue().getAsJsonObject();
|
||||||
|
WingData wingData = new WingData();
|
||||||
|
|
||||||
|
if (data.has("activeWings")) {
|
||||||
|
wingData.activeWings = data.get("activeWings").getAsString();
|
||||||
|
}
|
||||||
|
if (data.has("wingsVisible")) {
|
||||||
|
wingData.wingsVisible = data.get("wingsVisible").getAsBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
wingDataMap.put(uuid, wingData);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to load wing data: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save() {
|
||||||
|
try {
|
||||||
|
if (!CONFIG_FILE.getParentFile().exists()) {
|
||||||
|
CONFIG_FILE.getParentFile().mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileWriter writer = new FileWriter(CONFIG_FILE)) {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
for (Map.Entry<UUID, WingData> entry : wingDataMap.entrySet()) {
|
||||||
|
JsonObject data = new JsonObject();
|
||||||
|
data.addProperty("activeWings", entry.getValue().activeWings);
|
||||||
|
data.addProperty("wingsVisible", entry.getValue().wingsVisible);
|
||||||
|
json.add(entry.getKey().toString(), data);
|
||||||
|
}
|
||||||
|
GSON.toJson(json, writer);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to save wing data: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WingData getData(UUID playerUUID) {
|
||||||
|
return wingDataMap.computeIfAbsent(playerUUID, uuid -> new WingData());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getActiveWings(UUID playerUUID) {
|
||||||
|
return getData(playerUUID).activeWings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean areWingsVisible(UUID playerUUID) {
|
||||||
|
return getData(playerUUID).wingsVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setActiveWings(UUID playerUUID, String wingId) {
|
||||||
|
WingData data = getData(playerUUID);
|
||||||
|
data.activeWings = wingId.toLowerCase();
|
||||||
|
save();
|
||||||
|
|
||||||
|
if (MinecraftClient.getInstance().player != null) {
|
||||||
|
String playerName = MinecraftClient.getInstance().player.getName().getString();
|
||||||
|
WingSyncManager.sendWingsToServer(playerUUID, data, playerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void toggleWings(UUID playerUUID) {
|
||||||
|
WingData data = getData(playerUUID);
|
||||||
|
data.wingsVisible = !data.wingsVisible;
|
||||||
|
save();
|
||||||
|
|
||||||
|
if (MinecraftClient.getInstance().player != null) {
|
||||||
|
String playerName = MinecraftClient.getInstance().player.getName().getString();
|
||||||
|
de.winniepat.citrus.cosmetics.wings.sync.WingSyncManager.sendWingsToServer(playerUUID, data, playerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setWingsVisible(UUID playerUUID, boolean visible) {
|
||||||
|
WingData data = getData(playerUUID);
|
||||||
|
data.wingsVisible = visible;
|
||||||
|
save();
|
||||||
|
|
||||||
|
if (MinecraftClient.getInstance().player != null) {
|
||||||
|
String playerName = MinecraftClient.getInstance().player.getName().getString();
|
||||||
|
WingSyncManager.sendWingsToServer(playerUUID, data, playerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.wings;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.cosmetics.wings.sync.WingSyncManager;
|
||||||
|
import de.winniepat.citrus.cosmetics.wings.sync.WingSyncManager.SyncedWingData;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import software.bernie.geckolib.animatable.GeoAnimatable;
|
||||||
|
import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache;
|
||||||
|
import software.bernie.geckolib.animation.*;
|
||||||
|
import software.bernie.geckolib.util.GeckoLibUtil;
|
||||||
|
|
||||||
|
public class CosmeticWings implements GeoAnimatable {
|
||||||
|
private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this);
|
||||||
|
private final PlayerEntity player;
|
||||||
|
|
||||||
|
public CosmeticWings(PlayerEntity player) {
|
||||||
|
this.player = player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerEntity getPlayer() {
|
||||||
|
return this.player;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WingType getWingType() {
|
||||||
|
SyncedWingData syncedData =
|
||||||
|
WingSyncManager.getSyncedWings(
|
||||||
|
player.getUuid(),
|
||||||
|
player.getName().getString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (syncedData != null && WingSyncManager.hasInitialSyncCompleted()) {
|
||||||
|
return WingType.fromId(syncedData.wingType);
|
||||||
|
}
|
||||||
|
|
||||||
|
String wingId = ClientWingManager.getActiveWings(player.getUuid());
|
||||||
|
return WingType.fromId(wingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldRender() {
|
||||||
|
WingType type = getWingType();
|
||||||
|
if (type == WingType.NONE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncedWingData syncedData =
|
||||||
|
WingSyncManager.getSyncedWings(
|
||||||
|
player.getUuid(),
|
||||||
|
player.getName().getString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (syncedData != null && WingSyncManager.hasInitialSyncCompleted()) {
|
||||||
|
return syncedData.wingsVisible && !player.isInvisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClientWingManager.areWingsVisible(player.getUuid()) && !player.isInvisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
|
||||||
|
controllers.add(new AnimationController<>(this, "controller", 0, state -> {
|
||||||
|
WingType type = state.getAnimatable().getWingType();
|
||||||
|
if (type == WingType.NONE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerEntity p = state.getAnimatable().getPlayer();
|
||||||
|
|
||||||
|
return state.setAndContinue(RawAnimation.begin().thenLoop("animation.wings.idle"));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimatableInstanceCache getAnimatableInstanceCache() {
|
||||||
|
return this.cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public double getTick(Object object) {
|
||||||
|
return this.player.age;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.wings;
|
||||||
|
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
public enum WingType {
|
||||||
|
/*
|
||||||
|
Weil ich Alzheimer hab: ~Winnie
|
||||||
|
|
||||||
|
posX: Größer = weiter links
|
||||||
|
posY: Größer = höher
|
||||||
|
posZ: Größer = weiter vorn
|
||||||
|
*/
|
||||||
|
|
||||||
|
NONE("none", null, null, null, 1f, 0, 0, 0),
|
||||||
|
|
||||||
|
TEST1("wings", "wings", "wings.png", "wings",
|
||||||
|
1f,
|
||||||
|
0.2,
|
||||||
|
0.9,
|
||||||
|
0
|
||||||
|
),
|
||||||
|
|
||||||
|
TEST2("wings2", "wings2", "wings2.png", "wings2",
|
||||||
|
1.8f,
|
||||||
|
-0.2,
|
||||||
|
-0.4,
|
||||||
|
-0.1
|
||||||
|
),
|
||||||
|
|
||||||
|
TEST3("wings3", "angel_wings", "angel_wings.png", "angel_wings",
|
||||||
|
1.8f,
|
||||||
|
-0.2,
|
||||||
|
-0.4,
|
||||||
|
-0.1
|
||||||
|
);
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final String modelName;
|
||||||
|
private final String textureName;
|
||||||
|
private final String animationName;
|
||||||
|
private final float scale;
|
||||||
|
private final double posX, posY, posZ;
|
||||||
|
|
||||||
|
WingType(String id, String modelName, String textureName, String animationName, float scale, double posX, double posY, double posZ) {
|
||||||
|
this.id = id;
|
||||||
|
this.modelName = modelName;
|
||||||
|
this.textureName = textureName;
|
||||||
|
this.animationName = animationName;
|
||||||
|
this.scale = scale;
|
||||||
|
this.posX = posX;
|
||||||
|
this.posY = posY;
|
||||||
|
this.posZ = posZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getModelName() {
|
||||||
|
return modelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getModelId() {
|
||||||
|
if (modelName == null) return null;
|
||||||
|
return Identifier.of("citrus", "geo/" + modelName + ".geo.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getTextureId() {
|
||||||
|
if (textureName == null) return null;
|
||||||
|
return Identifier.of("citrus", "textures/cosmetics/" + textureName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getAnimationResource() {
|
||||||
|
if (animationName == null || NONE.id.equals(id)) return null;
|
||||||
|
return Identifier.of("citrus", "animations/" + animationName + ".animation.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getScale() {
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPosX() {
|
||||||
|
return posX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPosY() {
|
||||||
|
return posY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPosZ() {
|
||||||
|
return posZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WingType fromId(String id) {
|
||||||
|
if (id == null || id.equals("none") || id.isEmpty()) {
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (WingType type : values()) {
|
||||||
|
if (type.id.equalsIgnoreCase(id)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WingType[] getAvailableTypes() {
|
||||||
|
return values();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.wings;
|
||||||
|
|
||||||
|
import net.minecraft.client.network.AbstractClientPlayerEntity;
|
||||||
|
import net.minecraft.client.render.RenderLayer;
|
||||||
|
import net.minecraft.client.render.VertexConsumerProvider;
|
||||||
|
import net.minecraft.client.render.entity.feature.FeatureRenderer;
|
||||||
|
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||||
|
import net.minecraft.client.render.entity.model.PlayerEntityModel;
|
||||||
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import net.minecraft.util.math.RotationAxis;
|
||||||
|
import software.bernie.geckolib.renderer.GeoObjectRenderer;
|
||||||
|
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
public class WingsFeatureRenderer extends FeatureRenderer<AbstractClientPlayerEntity, PlayerEntityModel<AbstractClientPlayerEntity>> {
|
||||||
|
private final GeoObjectRenderer<CosmeticWings> wingRenderer;
|
||||||
|
private static final WeakHashMap<PlayerEntity, CosmeticWings> WING_CACHE = new WeakHashMap<>();
|
||||||
|
|
||||||
|
public WingsFeatureRenderer(FeatureRendererContext<AbstractClientPlayerEntity, PlayerEntityModel<AbstractClientPlayerEntity>> context) {
|
||||||
|
super(context);
|
||||||
|
this.wingRenderer = new GeoObjectRenderer<>(new WingsModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, AbstractClientPlayerEntity player, float limbAngle, float limbDistance, float tickDelta, float animationProgress, float headYaw, float headPitch) {
|
||||||
|
if (player.isInvisible()) return;
|
||||||
|
|
||||||
|
CosmeticWings wings = WING_CACHE.computeIfAbsent(player, CosmeticWings::new);
|
||||||
|
if (!wings.shouldRender()) return;
|
||||||
|
|
||||||
|
WingType type = wings.getWingType();
|
||||||
|
if (type == WingType.NONE) return;
|
||||||
|
|
||||||
|
Identifier texture = this.wingRenderer.getGeoModel().getTextureResource(wings);
|
||||||
|
if (texture == null) {
|
||||||
|
System.err.println("Wing texture is null for type: " + type.getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
matrices.push();
|
||||||
|
|
||||||
|
this.getContextModel().body.rotate(matrices);
|
||||||
|
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(180f));
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180f));
|
||||||
|
|
||||||
|
matrices.translate(-0.7, -2.8, 0.5);
|
||||||
|
|
||||||
|
matrices.translate(type.getPosX(), type.getPosY(), type.getPosZ());
|
||||||
|
|
||||||
|
matrices.scale(type.getScale(), type.getScale(), -1.0f);
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.wingRenderer.render(
|
||||||
|
matrices, wings, vertexConsumers,
|
||||||
|
RenderLayer.getEntityCutoutNoCull(texture),
|
||||||
|
vertexConsumers.getBuffer(RenderLayer.getEntityCutoutNoCull(texture)),
|
||||||
|
light, tickDelta
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to render wings: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
matrices.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void invalidateWingCache(PlayerEntity player) {
|
||||||
|
WING_CACHE.remove(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.wings;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import software.bernie.geckolib.model.GeoModel;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
public class WingsModel extends GeoModel<CosmeticWings> {
|
||||||
|
private static final Identifier DEFAULT_ANIMATION = Identifier.of("citrus", "animations/wings.animation.json");
|
||||||
|
private static final Identifier DEFAULT_MODEL = Identifier.of("citrus", "geo/wings.geo.json");
|
||||||
|
private static final Identifier DEFAULT_TEXTURE = Identifier.of("citrus", "textures/cosmetics/wings.png");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Identifier getModelResource(CosmeticWings wings) {
|
||||||
|
WingType type = wings.getWingType();
|
||||||
|
if (type == WingType.NONE) return null;
|
||||||
|
|
||||||
|
Identifier modelId = type.getModelId();
|
||||||
|
if (modelId == null) {
|
||||||
|
Client.debugLog("Wing model is null for type: " + type.getId());
|
||||||
|
return DEFAULT_MODEL;
|
||||||
|
}
|
||||||
|
return modelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Identifier getTextureResource(CosmeticWings wings) {
|
||||||
|
WingType type = wings.getWingType();
|
||||||
|
if (type == WingType.NONE) return null;
|
||||||
|
|
||||||
|
Identifier textureId = type.getTextureId();
|
||||||
|
if (textureId == null) {
|
||||||
|
Client.debugLog("Wing texture is null for type: " + type.getId());
|
||||||
|
return DEFAULT_TEXTURE;
|
||||||
|
}
|
||||||
|
return textureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Identifier getAnimationResource(CosmeticWings wings) {
|
||||||
|
WingType type = wings.getWingType();
|
||||||
|
if (type == WingType.NONE) {
|
||||||
|
Client.debugLog("Animation resource is null for NONE wing type");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Identifier anim = type.getAnimationResource();
|
||||||
|
if (anim == null) {
|
||||||
|
Client.debugLog("Animation resource is null for wing type: " + type.getId());
|
||||||
|
Client.debugLog("Falling back to default animation");
|
||||||
|
return DEFAULT_ANIMATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Wing animation resource: " + anim.toString() + " for type: " + type.getId());
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.wings.sync;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.managers.ClientRegistrationManager;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class PlayerWingTracker {
|
||||||
|
private static final Map<UUID, Long> LAST_SEEN_PLAYERS = new ConcurrentHashMap<>();
|
||||||
|
private static final long SYNC_INTERVAL = 30000;
|
||||||
|
|
||||||
|
public static void tick() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.world == null || client.player == null) return;
|
||||||
|
|
||||||
|
List<UUID> citrusUsersToSync = new ArrayList<>();
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (PlayerEntity player : client.world.getPlayers()) {
|
||||||
|
UUID uuid = player.getUuid();
|
||||||
|
|
||||||
|
if (player == client.player) continue;
|
||||||
|
|
||||||
|
if (!ClientRegistrationManager.isPlayerOnlineWithCitrus(uuid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long lastSeen = LAST_SEEN_PLAYERS.get(uuid);
|
||||||
|
if (lastSeen == null || currentTime - lastSeen > SYNC_INTERVAL) {
|
||||||
|
citrusUsersToSync.add(uuid);
|
||||||
|
LAST_SEEN_PLAYERS.put(uuid, currentTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!citrusUsersToSync.isEmpty()) {
|
||||||
|
WingSyncManager.batchFetchWings(citrusUsersToSync);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterator<Map.Entry<UUID, Long>> iterator = LAST_SEEN_PLAYERS.entrySet().iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
Map.Entry<UUID, Long> entry = iterator.next();
|
||||||
|
UUID uuid = entry.getKey();
|
||||||
|
|
||||||
|
boolean stillPresent = client.world.getPlayers().stream()
|
||||||
|
.anyMatch(p -> p.getUuid().equals(uuid) && p != client.player);
|
||||||
|
|
||||||
|
if (!stillPresent && currentTime - entry.getValue() > 60000) {
|
||||||
|
iterator.remove();
|
||||||
|
WingSyncManager.clearCache(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTime % 60000 < 50) {
|
||||||
|
WingSyncManager.retryPendingUpdates();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,547 @@
|
|||||||
|
package de.winniepat.citrus.cosmetics.wings.sync;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.managers.ClientRegistrationManager;
|
||||||
|
import de.winniepat.citrus.utils.SecurityUtils;
|
||||||
|
import de.winniepat.citrus.cosmetics.wings.ClientWingManager;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
public class WingSyncManager {
|
||||||
|
private static final String API_BASE_URL = Client.API_URL;
|
||||||
|
private static final Gson GSON = new GsonBuilder().create();
|
||||||
|
public static final Map<UUID, SyncedWingData> SYNCED_WING_CACHE = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<UUID, Long> LAST_SYNC_TIME = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<UUID, SyncedWingData> PENDING_UPDATES = new ConcurrentHashMap<>();
|
||||||
|
private static final Set<UUID> PENDING_FETCHES = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private static boolean hasSentInitialWings = false;
|
||||||
|
private static boolean isInitializing = false;
|
||||||
|
private static boolean serverReachable = true;
|
||||||
|
private static final long SERVER_CHECK_INTERVAL = 30000;
|
||||||
|
|
||||||
|
private static boolean autoSyncEnabled = true;
|
||||||
|
private static long lastAutoSyncTime = 0;
|
||||||
|
private static final long AUTO_SYNC_INTERVAL = 5000;
|
||||||
|
|
||||||
|
private static boolean serverHealthy = false;
|
||||||
|
private static Timer syncTimer;
|
||||||
|
|
||||||
|
public static class SyncedWingData {
|
||||||
|
public String wingType = "none";
|
||||||
|
public boolean wingsVisible = true;
|
||||||
|
|
||||||
|
public JsonObject toJson() {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("wingType", wingType);
|
||||||
|
json.addProperty("wingsVisible", wingsVisible);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SyncedWingData fromJson(JsonObject json) {
|
||||||
|
SyncedWingData data = new SyncedWingData();
|
||||||
|
if (json.has("wingType")) {
|
||||||
|
data.wingType = json.get("wingType").getAsString();
|
||||||
|
}
|
||||||
|
if (json.has("wingsVisible")) {
|
||||||
|
data.wingsVisible = json.get("wingsVisible").getAsBoolean();
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SyncedWingData fromClientWingData(ClientWingManager.WingData clientData) {
|
||||||
|
SyncedWingData data = new SyncedWingData();
|
||||||
|
data.wingType = clientData.activeWings;
|
||||||
|
data.wingsVisible = clientData.wingsVisible;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientWingManager.WingData toClientWingData() {
|
||||||
|
ClientWingManager.WingData data = new ClientWingManager.WingData();
|
||||||
|
data.activeWings = this.wingType;
|
||||||
|
data.wingsVisible = this.wingsVisible;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
Client.debugLog("Initializing wing sync...");
|
||||||
|
isInitializing = true;
|
||||||
|
|
||||||
|
startAutoSync();
|
||||||
|
|
||||||
|
checkServerHealth().thenAccept(healthy -> {
|
||||||
|
serverReachable = healthy;
|
||||||
|
if (healthy) {
|
||||||
|
Client.debugLog("Wing sync server is online");
|
||||||
|
scheduleInitialSync();
|
||||||
|
} else {
|
||||||
|
Client.logger.error("Wing sync server is offline");
|
||||||
|
isInitializing = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void scheduleInitialSync() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) {
|
||||||
|
performInitialSync();
|
||||||
|
} else {
|
||||||
|
Util.getMainWorkerExecutor().execute(() -> {
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
performInitialSync();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void performInitialSync() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) {
|
||||||
|
isInitializing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID uuid = client.player.getUuid();
|
||||||
|
String playerName = client.player.getName().getString();
|
||||||
|
|
||||||
|
Client.debugLog("Performing initial wing sync for " + playerName + " (" + uuid + ")");
|
||||||
|
|
||||||
|
fetchWingsFromServer(uuid, playerName, true).thenAccept(serverData -> {
|
||||||
|
if (serverData != null) {
|
||||||
|
Client.debugLog("Server has wings: " + serverData.wingType + ", visible: " + serverData.wingsVisible);
|
||||||
|
applyServerWingsToLocal(uuid, serverData);
|
||||||
|
} else {
|
||||||
|
ClientWingManager.WingData localData = ClientWingManager.getData(uuid);
|
||||||
|
if (localData != null && (!"wings".equals(localData.activeWings) || !localData.wingsVisible)) {
|
||||||
|
Client.debugLog("Sending local wings to server: " + localData.activeWings + ", visible: " + localData.wingsVisible);
|
||||||
|
SyncedWingData syncData = SyncedWingData.fromClientWingData(localData);
|
||||||
|
sendWingsToServer(uuid, syncData, playerName, true).thenAccept(success -> {
|
||||||
|
if (success) {
|
||||||
|
Client.debugLog("Initial wing sync completed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Client.debugLog("No local wing changes to sync");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasSentInitialWings = true;
|
||||||
|
isInitializing = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SyncedWingData getSyncedWings(UUID uuid, String playerName) {
|
||||||
|
if (!ClientRegistrationManager.isPlayerOnlineWithCitrus(uuid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInitializing && isLocalPlayer(uuid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SYNCED_WING_CACHE.containsKey(uuid)) {
|
||||||
|
long lastSync = LAST_SYNC_TIME.getOrDefault(uuid, 0L);
|
||||||
|
if (System.currentTimeMillis() - lastSync < 300000) {
|
||||||
|
return SYNCED_WING_CACHE.get(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocalPlayer(uuid) && !hasSentInitialWings && !isInitializing) {
|
||||||
|
performInitialSync();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PENDING_FETCHES.contains(uuid)) {
|
||||||
|
fetchWingsFromServer(uuid, playerName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<SyncedWingData> fetchWingsFromServer(UUID uuid, String playerName, boolean isInitial) {
|
||||||
|
PENDING_FETCHES.add(uuid);
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
String urlString = API_BASE_URL + "/wings/" + uuid.toString();
|
||||||
|
Client.debugLog("Fetching wings from: " + urlString);
|
||||||
|
|
||||||
|
URL url = new URI(urlString).toURL();
|
||||||
|
conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setConnectTimeout(10000);
|
||||||
|
conn.setReadTimeout(10000);
|
||||||
|
|
||||||
|
conn.setRequestProperty("User-Agent", "Minecraft/Citrus");
|
||||||
|
conn.setRequestProperty("Accept", "application/json");
|
||||||
|
conn.setRequestProperty("Accept-Charset", "UTF-8");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
Client.debugLog("Wings response code: " + responseCode);
|
||||||
|
|
||||||
|
if (responseCode == 200) {
|
||||||
|
serverReachable = true;
|
||||||
|
try (InputStream is = conn.getInputStream()) {
|
||||||
|
byte[] response = is.readAllBytes();
|
||||||
|
String responseBody = new String(response, StandardCharsets.UTF_8);
|
||||||
|
Client.debugLog("Wings response: " + responseBody);
|
||||||
|
|
||||||
|
JsonObject json = GSON.fromJson(responseBody, JsonObject.class);
|
||||||
|
|
||||||
|
SyncedWingData wingData = new SyncedWingData();
|
||||||
|
wingData.wingType = json.get("wingType").getAsString();
|
||||||
|
wingData.wingsVisible = json.get("wingsVisible").getAsBoolean();
|
||||||
|
|
||||||
|
Client.debugLog("Successfully got wings: " + wingData.wingType + ", visible: " + wingData.wingsVisible);
|
||||||
|
|
||||||
|
SYNCED_WING_CACHE.put(uuid, wingData);
|
||||||
|
LAST_SYNC_TIME.put(uuid, System.currentTimeMillis());
|
||||||
|
return wingData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serverReachable = false;
|
||||||
|
Client.logger.error("Wing server returned error code {}", responseCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
serverReachable = false;
|
||||||
|
Client.logger.error("Exception fetching wings: {}", e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
if (conn != null) {
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
PENDING_FETCHES.remove(uuid);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<Boolean> sendWingsToServer(UUID uuid, SyncedWingData wingData, String playerName, boolean isInitial) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/wings").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("User-Agent", "Citrus/1.0");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(3000);
|
||||||
|
conn.setReadTimeout(3000);
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", uuid.toString());
|
||||||
|
json.addProperty("wingType", wingData.wingType);
|
||||||
|
json.addProperty("wingsVisible", wingData.wingsVisible);
|
||||||
|
json.addProperty("playerName", playerName);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
|
||||||
|
if (responseCode == 200) {
|
||||||
|
serverReachable = true;
|
||||||
|
if (isInitial) {
|
||||||
|
hasSentInitialWings = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SYNCED_WING_CACHE.put(uuid, wingData);
|
||||||
|
LAST_SYNC_TIME.put(uuid, System.currentTimeMillis());
|
||||||
|
|
||||||
|
PENDING_UPDATES.remove(uuid);
|
||||||
|
|
||||||
|
if (!isInitial) {
|
||||||
|
Client.debugLog("Successfully synced wings to server: " + wingData.wingType);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
serverReachable = false;
|
||||||
|
if (!isInitial) {
|
||||||
|
Client.logger.error("Failed to sync wings to server. HTTP {}", responseCode);
|
||||||
|
}
|
||||||
|
PENDING_UPDATES.put(uuid, wingData);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
serverReachable = false;
|
||||||
|
if (!isInitial) {
|
||||||
|
Client.logger.error("Failed to send wings to server", e);
|
||||||
|
}
|
||||||
|
PENDING_UPDATES.put(uuid, wingData);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendWingsToServer(UUID uuid, SyncedWingData wingData, String playerName) {
|
||||||
|
sendWingsToServer(uuid, wingData, playerName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendWingsToServer(UUID uuid, ClientWingManager.WingData clientWingData, String playerName) {
|
||||||
|
SyncedWingData wingData = SyncedWingData.fromClientWingData(clientWingData);
|
||||||
|
sendWingsToServer(uuid, wingData, playerName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendWingsToServer(UUID uuid, String wingType, boolean wingsVisible, String playerName) {
|
||||||
|
SyncedWingData wingData = new SyncedWingData();
|
||||||
|
wingData.wingType = wingType;
|
||||||
|
wingData.wingsVisible = wingsVisible;
|
||||||
|
sendWingsToServer(uuid, wingData, playerName, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyServerWingsToLocal(UUID uuid, SyncedWingData wingData) {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
ClientWingManager.WingData localData = ClientWingManager.getData(uuid);
|
||||||
|
localData.activeWings = wingData.wingType;
|
||||||
|
localData.wingsVisible = wingData.wingsVisible;
|
||||||
|
ClientWingManager.save();
|
||||||
|
Client.debugLog("Applied server wings: " + wingData.wingType + ", visible: " + wingData.wingsVisible);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void batchFetchWings(List<UUID> uuids) {
|
||||||
|
if (uuids.isEmpty()) return;
|
||||||
|
|
||||||
|
List<UUID> citrusUsers = uuids.stream()
|
||||||
|
.filter(ClientRegistrationManager::isPlayerOnlineWithCitrus)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (citrusUsers.isEmpty()) return;
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/wings/batch").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
List<String> uuidStrings = new ArrayList<>();
|
||||||
|
for (UUID uuid : uuids) {
|
||||||
|
uuidStrings.add(uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.add("uuids", GSON.toJsonTree(uuidStrings));
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
serverReachable = true;
|
||||||
|
try (InputStream is = conn.getInputStream()) {
|
||||||
|
byte[] response = is.readAllBytes();
|
||||||
|
JsonObject result = GSON.fromJson(new String(response), JsonObject.class);
|
||||||
|
|
||||||
|
for (String uuidStr : result.keySet()) {
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(uuidStr);
|
||||||
|
JsonObject wingDataJson = result.getAsJsonObject(uuidStr);
|
||||||
|
SyncedWingData wingData = SyncedWingData.fromJson(wingDataJson);
|
||||||
|
|
||||||
|
SYNCED_WING_CACHE.put(uuid, wingData);
|
||||||
|
LAST_SYNC_TIME.put(uuid, System.currentTimeMillis());
|
||||||
|
|
||||||
|
if (!isLocalPlayer(uuid)) {
|
||||||
|
ClientWingManager.WingData clientData = wingData.toClientWingData();
|
||||||
|
ClientWingManager.getData(uuid).activeWings = clientData.activeWings;
|
||||||
|
ClientWingManager.getData(uuid).wingsVisible = clientData.wingsVisible;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Invalid UUID in wings response: {}", uuidStr, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Fetched wings for " + result.size() + " Citrus users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Batch wings fetch failed", e);
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startAutoSync() {
|
||||||
|
if (syncTimer != null) {
|
||||||
|
syncTimer.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
syncTimer = new Timer("WingAutoSync", true);
|
||||||
|
syncTimer.scheduleAtFixedRate(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (autoSyncEnabled && System.currentTimeMillis() - lastAutoSyncTime >= AUTO_SYNC_INTERVAL) {
|
||||||
|
performAutoSync();
|
||||||
|
System.out.println("Synced wings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000, AUTO_SYNC_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void performAutoSync() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null || client.world == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastAutoSyncTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
syncLocalPlayerWings();
|
||||||
|
syncNearbyPlayersWings();
|
||||||
|
retryPendingUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void syncLocalPlayerWings() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) return;
|
||||||
|
|
||||||
|
UUID uuid = client.player.getUuid();
|
||||||
|
String playerName = client.player.getName().getString();
|
||||||
|
ClientWingManager.WingData localData = ClientWingManager.getData(uuid);
|
||||||
|
|
||||||
|
if (localData != null) {
|
||||||
|
Long lastSync = LAST_SYNC_TIME.get(uuid);
|
||||||
|
if (lastSync == null || System.currentTimeMillis() - lastSync > 30000) {
|
||||||
|
SyncedWingData syncData = SyncedWingData.fromClientWingData(localData);
|
||||||
|
sendWingsToServer(uuid, syncData, playerName, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void syncNearbyPlayersWings() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.world == null || client.player == null) return;
|
||||||
|
|
||||||
|
List<UUID> nearbyCitrusUsers = new ArrayList<>();
|
||||||
|
|
||||||
|
for (PlayerEntity player : client.world.getPlayers()) {
|
||||||
|
if (player == client.player) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ClientRegistrationManager.isPlayerOnlineWithCitrus(player.getUuid())) {
|
||||||
|
nearbyCitrusUsers.add(player.getUuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nearbyCitrusUsers.isEmpty()) {
|
||||||
|
batchFetchWings(nearbyCitrusUsers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void retryPendingUpdates() {
|
||||||
|
if (PENDING_UPDATES.isEmpty()) return;
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) return;
|
||||||
|
|
||||||
|
for (Map.Entry<UUID, SyncedWingData> entry : PENDING_UPDATES.entrySet()) {
|
||||||
|
String playerName = entry.getKey().equals(client.player.getUuid())
|
||||||
|
? client.player.getName().getString()
|
||||||
|
: entry.getKey().toString();
|
||||||
|
|
||||||
|
sendWingsToServer(entry.getKey(), entry.getValue(), playerName, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Boolean> checkServerHealth() {
|
||||||
|
long lastServerCheck = System.currentTimeMillis();
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/health").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setConnectTimeout(3000);
|
||||||
|
|
||||||
|
boolean healthy = conn.getResponseCode() == 200;
|
||||||
|
serverHealthy = conn.getResponseCode() == 200;
|
||||||
|
conn.disconnect();
|
||||||
|
return healthy;
|
||||||
|
} catch (Exception e) {
|
||||||
|
serverHealthy = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isLocalPlayer(UUID uuid) {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
return client.player != null && client.player.getUuid().equals(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearCache(UUID uuid) {
|
||||||
|
SYNCED_WING_CACHE.remove(uuid);
|
||||||
|
LAST_SYNC_TIME.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearAllCache() {
|
||||||
|
SYNCED_WING_CACHE.clear();
|
||||||
|
LAST_SYNC_TIME.clear();
|
||||||
|
PENDING_UPDATES.clear();
|
||||||
|
PENDING_FETCHES.clear();
|
||||||
|
hasSentInitialWings = false;
|
||||||
|
isInitializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasInitialSyncCompleted() {
|
||||||
|
return hasSentInitialWings && !isInitializing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isInitializing() {
|
||||||
|
return isInitializing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void forceInitialSync() {
|
||||||
|
if (!hasSentInitialWings) {
|
||||||
|
isInitializing = true;
|
||||||
|
performInitialSync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAutoSyncEnabled() {
|
||||||
|
return autoSyncEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setAutoSyncEnabled(boolean enabled) {
|
||||||
|
autoSyncEnabled = enabled;
|
||||||
|
if (enabled) {
|
||||||
|
startAutoSync();
|
||||||
|
} else if (syncTimer != null) {
|
||||||
|
syncTimer.cancel();
|
||||||
|
syncTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getSyncStatus() {
|
||||||
|
if (!autoSyncEnabled) return "Disabled";
|
||||||
|
if (isInitializing) return "Initializing...";
|
||||||
|
if (hasSentInitialWings) return "Synced";
|
||||||
|
return "Not synced";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package de.winniepat.citrus.draggui;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
|
||||||
|
public class HudConfig {
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
private static final File CUSTOM_DIR = new File(FabricLoader.getInstance().getGameDir().toFile(), "citrus");
|
||||||
|
private static final File FILE = new File(CUSTOM_DIR, "hud.json");
|
||||||
|
|
||||||
|
public int fpsX = 10, fpsY = 10;
|
||||||
|
public int pingX = 10, pingY = 25;
|
||||||
|
public int serverX = 10, serverY = 40;
|
||||||
|
|
||||||
|
public int keyX = 10, keyY = 60;
|
||||||
|
public int armorX = 10, armorY = 120;
|
||||||
|
public int potionX = 10, potionY = 180;
|
||||||
|
|
||||||
|
public int fpsColor = 0xFFFFFF;
|
||||||
|
public int pingColor = 0xFFFFFF;
|
||||||
|
public int serverColor = 0xFFFFFF;
|
||||||
|
public int keyColor = 0xFFFFFF;
|
||||||
|
public int armorColor = 0xFFFFFF;
|
||||||
|
public int potionColor = -1; // -1 ist einfach rot, wenn schlecht und grün, wenn gut
|
||||||
|
|
||||||
|
public float uiScale = 1.0f;
|
||||||
|
|
||||||
|
public boolean fpsEnabled = true;
|
||||||
|
public boolean pingEnabled = true;
|
||||||
|
public boolean serverEnabled = true;
|
||||||
|
public boolean keyEnabled = true;
|
||||||
|
public boolean armorEnabled = true;
|
||||||
|
public boolean potionEnabled = true;
|
||||||
|
|
||||||
|
public boolean isModuleEnabled(String label) {
|
||||||
|
return switch (label) {
|
||||||
|
case "FPS" -> fpsEnabled;
|
||||||
|
case "Ping" -> pingEnabled;
|
||||||
|
case "Server" -> serverEnabled;
|
||||||
|
case "Keys" -> keyEnabled;
|
||||||
|
case "Armor" -> armorEnabled;
|
||||||
|
case "Potion" -> potionEnabled;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HudConfig INSTANCE = new HudConfig();
|
||||||
|
|
||||||
|
public static void load() {
|
||||||
|
if (!CUSTOM_DIR.exists()) CUSTOM_DIR.mkdirs();
|
||||||
|
if (FILE.exists()) {
|
||||||
|
try (FileReader reader = new FileReader(FILE)) {
|
||||||
|
INSTANCE = GSON.fromJson(reader, HudConfig.class);
|
||||||
|
Client.debugLog("Loaded from " + FILE.getAbsolutePath());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error while loading config", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save() {
|
||||||
|
try {
|
||||||
|
if (!CUSTOM_DIR.exists()) CUSTOM_DIR.mkdirs();
|
||||||
|
try (FileWriter writer = new FileWriter(FILE)) {
|
||||||
|
GSON.toJson(INSTANCE, writer);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error while saving config", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
package de.winniepat.citrus.draggui;
|
||||||
|
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.Screen;
|
||||||
|
import net.minecraft.client.gui.widget.*;
|
||||||
|
import net.minecraft.client.network.PlayerListEntry;
|
||||||
|
import net.minecraft.entity.effect.StatusEffectInstance;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.math.MathHelper;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class HudEditorScreen extends Screen {
|
||||||
|
private enum SelectedModule {
|
||||||
|
NONE,
|
||||||
|
FPS,
|
||||||
|
PING,
|
||||||
|
SERVER,
|
||||||
|
KEYS,
|
||||||
|
ARMOR,
|
||||||
|
POTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
private SelectedModule dragging = SelectedModule.NONE;
|
||||||
|
private SelectedModule pickingColorFor = SelectedModule.NONE;
|
||||||
|
|
||||||
|
private int dragOffsetX, dragOffsetY;
|
||||||
|
private TextFieldWidget hexInput, rInput, gInput, bInput;
|
||||||
|
private ButtonWidget resetButton;
|
||||||
|
|
||||||
|
private float currentHue = 0f, currentSat = 0f, currentBri = 0f;
|
||||||
|
private boolean isDraggingHue = false, isDraggingSB = false;
|
||||||
|
private boolean ignoreUpdates = false;
|
||||||
|
|
||||||
|
private final int windowW = 185, windowH = 185;
|
||||||
|
private final int boxW = 120, boxH = 100, sliderW = 12;
|
||||||
|
|
||||||
|
public HudEditorScreen() { super(Text.literal("HUD Editor")); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
int cx = this.width / 2;
|
||||||
|
int cy = this.height / 2;
|
||||||
|
|
||||||
|
hexInput = new TextFieldWidget(textRenderer, cx - 80, cy + 55, 55, 16, Text.literal("Hex"));
|
||||||
|
hexInput.setMaxLength(7);
|
||||||
|
hexInput.setChangedListener(this::onHexChanged);
|
||||||
|
|
||||||
|
rInput = createRGBField(cx - 20, cy + 55, "R");
|
||||||
|
gInput = createRGBField(cx + 15, cy + 55, "G");
|
||||||
|
bInput = createRGBField(cx + 50, cy + 55, "B");
|
||||||
|
|
||||||
|
resetButton = ButtonWidget.builder(Text.literal("Reset Default"), button -> {
|
||||||
|
updateAll(0xFFFFFFFF, true);
|
||||||
|
}).dimensions(cx - 80, cy + 78, 160, 20).build();
|
||||||
|
|
||||||
|
this.addDrawableChild(hexInput);
|
||||||
|
this.addDrawableChild(rInput);
|
||||||
|
this.addDrawableChild(gInput);
|
||||||
|
this.addDrawableChild(bInput);
|
||||||
|
this.addDrawableChild(resetButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextFieldWidget createRGBField(int x, int y, String label) {
|
||||||
|
TextFieldWidget tf = new TextFieldWidget(textRenderer, x, y, 30, 16, Text.literal(label));
|
||||||
|
tf.setMaxLength(3);
|
||||||
|
tf.setChangedListener(s -> onRGBChanged());
|
||||||
|
return tf;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||||
|
this.renderBackground(context, mouseX, mouseY, delta);
|
||||||
|
HudConfig c = HudConfig.INSTANCE;
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
|
String fpsStr = client.getCurrentFps() + " FPS";
|
||||||
|
renderModule(context, c.fpsX, c.fpsY, textRenderer.getWidth(fpsStr), 10, fpsStr, c.fpsColor, dragging == SelectedModule.FPS);
|
||||||
|
|
||||||
|
int ping = 0;
|
||||||
|
if (client.getNetworkHandler() != null && client.player != null) {
|
||||||
|
PlayerListEntry entry = client.getNetworkHandler().getPlayerListEntry(client.player.getUuid());
|
||||||
|
if (entry != null) ping = entry.getLatency();
|
||||||
|
}
|
||||||
|
String pingStr = ping + "ms";
|
||||||
|
renderModule(context, c.pingX, c.pingY, Math.max(textRenderer.getWidth(pingStr), 30), 10, pingStr, c.pingColor, dragging == SelectedModule.PING);
|
||||||
|
|
||||||
|
String srv = client.getCurrentServerEntry() != null ? client.getCurrentServerEntry().address : "Singleplayer";
|
||||||
|
renderModule(context, c.serverX, c.serverY, textRenderer.getWidth(srv), 10, srv, c.serverColor, dragging == SelectedModule.SERVER);
|
||||||
|
|
||||||
|
drawLiveKeys(context, c.keyX, c.keyY, c.keyColor, dragging == SelectedModule.KEYS);
|
||||||
|
|
||||||
|
drawLiveArmor(context, c.armorX, c.armorY, dragging == SelectedModule.ARMOR);
|
||||||
|
|
||||||
|
drawLivePotions(context, c.potionX, c.potionY, c.potionColor, dragging == SelectedModule.POTIONS);
|
||||||
|
|
||||||
|
if (pickingColorFor != SelectedModule.NONE) {
|
||||||
|
renderColorPicker(context, mouseX, mouseY, delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLiveKeys(DrawContext ctx, int x, int y, int color, boolean drag) {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (drag) ctx.fill(x - 2, y - 2, x + 66, y + 88, 0x4400FF00);
|
||||||
|
|
||||||
|
drawKey(ctx, x + 22, y, 20, 20, "W", client.options.forwardKey.isPressed(), color);
|
||||||
|
drawKey(ctx, x, y + 22, 20, 20, "A", client.options.leftKey.isPressed(), color);
|
||||||
|
drawKey(ctx, x + 22, y + 22, 20, 20, "S", client.options.backKey.isPressed(), color);
|
||||||
|
drawKey(ctx, x + 44, y + 22, 20, 20, "D", client.options.rightKey.isPressed(), color);
|
||||||
|
drawKey(ctx, x, y + 44, 31, 20, "LMB", client.options.attackKey.isPressed(), color);
|
||||||
|
drawKey(ctx, x + 33, y + 44, 31, 20, "RMB", client.options.useKey.isPressed(), color);
|
||||||
|
drawKey(ctx, x, y + 66, 31, 20, "Shf", client.options.sneakKey.isPressed(), color);
|
||||||
|
drawKey(ctx, x + 33, y + 66, 31, 20, "Jmp", client.options.jumpKey.isPressed(), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawKey(DrawContext ctx, int x, int y, int w, int h, String label, boolean active, int color) {
|
||||||
|
ctx.fill(x, y, x + w, y + h, active ? 0x88FFFFFF : 0x44000000);
|
||||||
|
ctx.drawCenteredTextWithShadow(textRenderer, label, x + w / 2, y + (h / 2 - 4), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLivePotions(DrawContext ctx, int x, int y, int color, boolean drag) {
|
||||||
|
assert client != null;
|
||||||
|
assert client.player != null;
|
||||||
|
Collection<StatusEffectInstance> effects = client.player.getStatusEffects();
|
||||||
|
|
||||||
|
if (effects.isEmpty()) {
|
||||||
|
String placeholder = "[Potion Effects]";
|
||||||
|
ctx.fill(x - 2, y - 2, x + textRenderer.getWidth(placeholder) + 2, y + 10, drag ? 0x8800FF00 : 0x44FFFFFF);
|
||||||
|
ctx.drawText(textRenderer, placeholder, x, y, color, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
for (StatusEffectInstance effect : effects) {
|
||||||
|
int durationSeconds = effect.getDuration() / 20;
|
||||||
|
String time = String.format("%d:%02d", durationSeconds / 60, durationSeconds % 60);
|
||||||
|
String name = Text.translatable(effect.getTranslationKey()).getString();
|
||||||
|
String level = effect.getAmplifier() > 0 ? " " + (effect.getAmplifier() + 1) : "";
|
||||||
|
String fullText = name + level + " (" + time + ")";
|
||||||
|
|
||||||
|
if (drag && offset == 0) ctx.fill(x - 2, y - 2, x + textRenderer.getWidth(fullText) + 2, y + (effects.size() * 11), 0x4400FF00);
|
||||||
|
ctx.drawText(textRenderer, fullText, x, y + offset, color, true);
|
||||||
|
offset += 11;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawLiveArmor(DrawContext ctx, int x, int y, boolean drag) {
|
||||||
|
if (drag) ctx.fill(x - 2, y - 2, x + 20, y + 82, 0x4400FF00);
|
||||||
|
int i = 0;
|
||||||
|
boolean hasArmor = false;
|
||||||
|
assert client != null;
|
||||||
|
assert client.player != null;
|
||||||
|
for (ItemStack stack : client.player.getInventory().armor) {
|
||||||
|
if (!stack.isEmpty()) {
|
||||||
|
ctx.drawItem(stack, x, y + (3 - i) * 20);
|
||||||
|
hasArmor = true;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (!hasArmor) ctx.drawBorder(x, y, 18, 78, 0x88FFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderColorPicker(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||||
|
int x = (this.width - windowW) / 2;
|
||||||
|
int y = (this.height - windowH) / 2;
|
||||||
|
|
||||||
|
context.fill(x, y, x + windowW, y + windowH, 0xEE111111);
|
||||||
|
context.drawBorder(x, y, windowW, windowH, 0xFFFFFFFF);
|
||||||
|
|
||||||
|
int bx = x + 10, by = y + 10;
|
||||||
|
context.fillGradient(bx, by, bx + boxW, by + boxH, Color.HSBtoRGB(currentHue, 1f, 1f), 0xFF000000);
|
||||||
|
|
||||||
|
for (int i = 0; i < boxH; i++) {
|
||||||
|
context.fill(bx + boxW + 10, by + i, bx + boxW + 10 + sliderW, by + i + 1, 0xFF000000 | Color.HSBtoRGB(i / (float) boxH, 1f, 1f));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDraggingHue) {
|
||||||
|
currentHue = MathHelper.clamp((mouseY - by) / (float) boxH, 0f, 1f); syncFromHSB();
|
||||||
|
} else if (isDraggingSB) {
|
||||||
|
currentSat = MathHelper.clamp((mouseX - bx) / (float) boxW, 0f, 1f);
|
||||||
|
currentBri = MathHelper.clamp(1 - (mouseY - by) / (float) boxH, 0f, 1f);
|
||||||
|
syncFromHSB();
|
||||||
|
}
|
||||||
|
|
||||||
|
hexInput.render(context, mouseX, mouseY, delta);
|
||||||
|
rInput.render(context, mouseX, mouseY, delta);
|
||||||
|
gInput.render(context, mouseX, mouseY, delta);
|
||||||
|
bInput.render(context, mouseX, mouseY, delta);
|
||||||
|
resetButton.render(context, mouseX, mouseY, delta);
|
||||||
|
|
||||||
|
context.drawText(textRenderer, "Hex", hexInput.getX(), hexInput.getY() - 10, 0xFFAAAAAA, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderModule(DrawContext ctx, int x, int y, int w, int h, String text, int col, boolean drag) {
|
||||||
|
ctx.fill(x - 2, y - 2, x + w + 2, y + h + 2, drag ? 0x8800FF00 : 0x44FFFFFF);
|
||||||
|
ctx.drawText(textRenderer, text, x, y, col, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean mouseClicked(double mx, double my, int b) {
|
||||||
|
if (pickingColorFor != SelectedModule.NONE) {
|
||||||
|
if (super.mouseClicked(mx, my, b)) return true;
|
||||||
|
this.setFocused(null);
|
||||||
|
|
||||||
|
int x = (this.width - windowW) / 2 + 10;
|
||||||
|
int y = (this.height - windowH) / 2 + 10;
|
||||||
|
|
||||||
|
if (isHovering(mx, my, x + boxW + 10, y, sliderW, boxH)) {
|
||||||
|
isDraggingHue = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHovering(mx, my, x, y, boxW, boxH)) {
|
||||||
|
isDraggingSB = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHovering(mx, my, (this.width-windowW)/2, (this.height-windowH)/2, windowW, windowH)) {
|
||||||
|
pickingColorFor = SelectedModule.NONE;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
SelectedModule hovered = getHoveredModule(mx, my);
|
||||||
|
if (hovered != SelectedModule.NONE) {
|
||||||
|
if (b == 0) {
|
||||||
|
dragging = hovered;
|
||||||
|
dragOffsetX = (int)mx - getX(hovered);
|
||||||
|
dragOffsetY = (int)my - getY(hovered);
|
||||||
|
} else if (b == 1) {
|
||||||
|
pickingColorFor = hovered;
|
||||||
|
updateAll(getModuleColor(hovered), true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.mouseClicked(mx, my, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||||
|
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean charTyped(char chr, int modifiers) {
|
||||||
|
return super.charTyped(chr, modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SelectedModule getHoveredModule(double mx, double my) {
|
||||||
|
HudConfig c = HudConfig.INSTANCE;
|
||||||
|
if (isHovering(mx, my, c.fpsX, c.fpsY, 50, 10)) return SelectedModule.FPS;
|
||||||
|
if (isHovering(mx, my, c.pingX, c.pingY, 40, 10)) return SelectedModule.PING;
|
||||||
|
if (isHovering(mx, my, c.serverX, c.serverY, 80, 10)) return SelectedModule.SERVER;
|
||||||
|
if (isHovering(mx, my, c.keyX, c.keyY, 66, 88)) return SelectedModule.KEYS;
|
||||||
|
if (isHovering(mx, my, c.armorX, c.armorY, 20, 80)) return SelectedModule.ARMOR;
|
||||||
|
if (isHovering(mx, my, c.potionX, c.potionY, 100, 40)) return SelectedModule.POTIONS;
|
||||||
|
return SelectedModule.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onHexChanged(String hex) {
|
||||||
|
if (!ignoreUpdates && (hex.length() == 7 || hex.length() == 6)) {
|
||||||
|
try {
|
||||||
|
String h = hex.startsWith("#") ? hex.substring(1) : hex;
|
||||||
|
updateAll(Integer.parseInt(h, 16) | 0xFF000000, false);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onRGBChanged() {
|
||||||
|
if (!ignoreUpdates) {
|
||||||
|
try {
|
||||||
|
updateAll(new Color(Integer.parseInt(
|
||||||
|
rInput.getText()),
|
||||||
|
Integer.parseInt(gInput.getText()),
|
||||||
|
Integer.parseInt(bInput.getText())).getRGB(),
|
||||||
|
false);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncFromHSB() {
|
||||||
|
updateAll(Color.HSBtoRGB(currentHue, currentSat, currentBri), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAll(int rgb, boolean force) {
|
||||||
|
ignoreUpdates = true;
|
||||||
|
int color = rgb | 0xFF000000;
|
||||||
|
HudConfig c = HudConfig.INSTANCE;
|
||||||
|
switch (pickingColorFor) {
|
||||||
|
case FPS->c.fpsColor=color;
|
||||||
|
case PING->c.pingColor=color;
|
||||||
|
case SERVER->c.serverColor=color;
|
||||||
|
case KEYS->c.keyColor=color;
|
||||||
|
case ARMOR->c.armorColor=color;
|
||||||
|
case POTIONS->c.potionColor=color;
|
||||||
|
}
|
||||||
|
|
||||||
|
float[] hsb = Color.RGBtoHSB((color>>16)&0xFF, (color>>8)&0xFF, color&0xFF, null);
|
||||||
|
currentHue = hsb[0]; currentSat = hsb[1]; currentBri = hsb[2];
|
||||||
|
if (force || !hexInput.isFocused()) hexInput.setText(String.format("#%06X", (0xFFFFFF & color)));
|
||||||
|
if (force || !rInput.isFocused()) rInput.setText(String.valueOf((color>>16)&0xFF));
|
||||||
|
if (force || !gInput.isFocused()) gInput.setText(String.valueOf((color>>8)&0xFF));
|
||||||
|
if (force || !bInput.isFocused()) bInput.setText(String.valueOf(color&0xFF));
|
||||||
|
ignoreUpdates = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isHovering(double mx, double my, int x, int y, int w, int h) {
|
||||||
|
return mx >= x && mx <= x + w && my >= y && my <= y + h;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getX(SelectedModule m) {
|
||||||
|
HudConfig c = HudConfig.INSTANCE;
|
||||||
|
return switch(m) {
|
||||||
|
case FPS->c.fpsX;
|
||||||
|
case PING->c.pingX;
|
||||||
|
case SERVER->c.serverX;
|
||||||
|
case KEYS->c.keyX;
|
||||||
|
case ARMOR->c.armorX;
|
||||||
|
case POTIONS->c.potionX;
|
||||||
|
default->0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getY(SelectedModule m) {
|
||||||
|
HudConfig c = HudConfig.INSTANCE;
|
||||||
|
return switch(m) {
|
||||||
|
case FPS->c.fpsY;
|
||||||
|
case PING->c.pingY;
|
||||||
|
case SERVER->c.serverY;
|
||||||
|
case KEYS->c.keyY;
|
||||||
|
case ARMOR->c.armorY;
|
||||||
|
case POTIONS->c.potionY;
|
||||||
|
default->0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getModuleColor(SelectedModule m) {
|
||||||
|
HudConfig c = HudConfig.INSTANCE;
|
||||||
|
return switch(m) {
|
||||||
|
case FPS->c.fpsColor;
|
||||||
|
case PING->c.pingColor;
|
||||||
|
case SERVER->c.serverColor;
|
||||||
|
case KEYS->c.keyColor;
|
||||||
|
case ARMOR->c.armorColor;
|
||||||
|
case POTIONS->c.potionColor;
|
||||||
|
default->-1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean mouseDragged(double mx, double my, int b, double dx, double dy) {
|
||||||
|
if (dragging != SelectedModule.NONE) {
|
||||||
|
HudConfig c = HudConfig.INSTANCE; int nx = (int)mx - dragOffsetX, ny = (int)my - dragOffsetY;
|
||||||
|
switch(dragging) {
|
||||||
|
case FPS->{
|
||||||
|
c.fpsX=nx;
|
||||||
|
c.fpsY=ny;
|
||||||
|
}
|
||||||
|
case PING->{
|
||||||
|
c.pingX=nx;
|
||||||
|
c.pingY=ny;
|
||||||
|
}
|
||||||
|
case SERVER->{
|
||||||
|
c.serverX=nx;
|
||||||
|
c.serverY=ny;
|
||||||
|
}
|
||||||
|
case KEYS->{
|
||||||
|
c.keyX=nx;
|
||||||
|
c.keyY=ny;
|
||||||
|
}
|
||||||
|
case ARMOR->{
|
||||||
|
c.armorX=nx;
|
||||||
|
c.armorY=ny;
|
||||||
|
}
|
||||||
|
case POTIONS->{
|
||||||
|
c.potionX=nx;
|
||||||
|
c.potionY=ny;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.mouseDragged(mx, my, b, dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean mouseReleased(double mx, double my, int b) {
|
||||||
|
isDraggingHue = isDraggingSB = false;
|
||||||
|
dragging = SelectedModule.NONE;
|
||||||
|
return super.mouseReleased(mx, my, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void close() {
|
||||||
|
HudConfig.save();
|
||||||
|
super.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package de.winniepat.citrus.draggui;
|
||||||
|
|
||||||
|
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.entity.effect.*;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class HudRenderer {
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
HudRenderCallback.EVENT.register((context, tickCounter) -> {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null || client.options.hudHidden) return;
|
||||||
|
|
||||||
|
HudConfig c = HudConfig.INSTANCE;
|
||||||
|
float scale = c.uiScale <= 0 ? 1.0f : c.uiScale;
|
||||||
|
|
||||||
|
if (scale != 1.0f) {
|
||||||
|
context.getMatrices().push();
|
||||||
|
context.getMatrices().scale(scale, scale, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.fpsEnabled) {
|
||||||
|
renderFPS(client, context, c.fpsX, c.fpsY, c.fpsColor);
|
||||||
|
}
|
||||||
|
if (c.pingEnabled) {
|
||||||
|
renderPing(client, context, c.pingX, c.pingY, c.pingColor);
|
||||||
|
}
|
||||||
|
if (c.serverEnabled) {
|
||||||
|
renderServer(client, context, c.serverX, c.serverY, c.serverColor);
|
||||||
|
}
|
||||||
|
if (c.keyEnabled) {
|
||||||
|
renderKeystrokes(client, context, c.keyX, c.keyY, c.keyColor);
|
||||||
|
}
|
||||||
|
if (c.armorEnabled) {
|
||||||
|
renderArmorStatus(client, context, c.armorX, c.armorY, c.armorColor);
|
||||||
|
}
|
||||||
|
if (c.potionEnabled) {
|
||||||
|
renderPotionEffects(client, context, c.potionX, c.potionY, c.potionColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scale != 1.0f) {
|
||||||
|
context.getMatrices().pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renderKeystrokes(MinecraftClient client, DrawContext context, int x, int y, int color) {
|
||||||
|
drawKey(context, x + 22, y, "W", client.options.forwardKey.isPressed(), color);
|
||||||
|
drawKey(context, x, y + 22, "A", client.options.leftKey.isPressed(), color);
|
||||||
|
drawKey(context, x + 22, y + 22, "S", client.options.backKey.isPressed(), color);
|
||||||
|
drawKey(context, x + 44, y + 22, "D", client.options.rightKey.isPressed(), color);
|
||||||
|
|
||||||
|
drawKeyCustom(context, x, y + 44, 31, 20, "LMB", client.options.attackKey.isPressed(), color);
|
||||||
|
drawKeyCustom(context, x + 33, y + 44, 31, 20, "RMB", client.options.useKey.isPressed(), color);
|
||||||
|
|
||||||
|
drawKeyCustom(context, x, y + 66, 31, 20, "Shf", client.options.sneakKey.isPressed(), color);
|
||||||
|
drawKeyCustom(context, x + 33, y + 66, 31, 20, "Jmp", client.options.jumpKey.isPressed(), color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawKey(DrawContext context, int x, int y, String key, boolean pressed, int color) {
|
||||||
|
int bgColor = pressed ? 0x99FFFFFF : 0x44000000;
|
||||||
|
int textColor = pressed ? 0xFF000000 : color;
|
||||||
|
context.fill(x, y, x + 20, y + 20, bgColor);
|
||||||
|
context.drawText(MinecraftClient.getInstance().textRenderer, key, x + 7, y + 6, textColor, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawKeyCustom(DrawContext context, int x, int y, int w, int h, String name, boolean down, int color) {
|
||||||
|
int bgColor = down ? 0x99FFFFFF : 0x44000000;
|
||||||
|
int textColor = down ? 0xFF000000 : color;
|
||||||
|
context.fill(x, y, x + w, y + h, bgColor);
|
||||||
|
int labelWidth = MinecraftClient.getInstance().textRenderer.getWidth(name);
|
||||||
|
context.drawText(MinecraftClient.getInstance().textRenderer, name, x + (w / 2) - (labelWidth / 2), y + (h / 2) - 4, textColor, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renderFPS(MinecraftClient client, DrawContext context, int x, int y, int color) {
|
||||||
|
context.drawText(client.textRenderer, client.getCurrentFps() + " FPS", x, y, color, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renderPing(MinecraftClient client, DrawContext context, int x, int y, int color) {
|
||||||
|
int ping = 0;
|
||||||
|
if (client.getNetworkHandler() != null) {
|
||||||
|
var entry = client.getNetworkHandler().getPlayerListEntry(client.player.getUuid());
|
||||||
|
if (entry != null) ping = entry.getLatency();
|
||||||
|
}
|
||||||
|
context.drawText(client.textRenderer, ping + "ms", x, y, color, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renderServer(MinecraftClient client, DrawContext context, int x, int y, int color) {
|
||||||
|
String server = client.getCurrentServerEntry() != null ? client.getCurrentServerEntry().address : "Singleplayer";
|
||||||
|
context.drawText(client.textRenderer, server, x, y, color, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renderArmorStatus(MinecraftClient client, DrawContext context, int x, int y, int color) {
|
||||||
|
int offset = 0;
|
||||||
|
boolean hasArmor = false;
|
||||||
|
List<ItemStack> armor = new ArrayList<>();
|
||||||
|
client.player.getArmorItems().forEach(armor::add);
|
||||||
|
|
||||||
|
for (int i = 3; i >= 0; i--) {
|
||||||
|
ItemStack stack = armor.get(i);
|
||||||
|
if (stack.isEmpty()) continue;
|
||||||
|
hasArmor = true;
|
||||||
|
context.drawItem(stack, x, y + offset);
|
||||||
|
context.drawItemInSlot(client.textRenderer, stack, x, y + offset);
|
||||||
|
offset += 18;
|
||||||
|
}
|
||||||
|
if (!hasArmor) {
|
||||||
|
context.drawText(client.textRenderer, "[Armor Status]", x, y, color, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void renderPotionEffects(MinecraftClient client, DrawContext context, int x, int y, int configColor) {
|
||||||
|
assert client.player != null;
|
||||||
|
Collection<StatusEffectInstance> effects = client.player.getStatusEffects();
|
||||||
|
if (effects.isEmpty()) {
|
||||||
|
context.drawText(client.textRenderer, "[Potion Effects]", x, y, configColor, true);
|
||||||
|
} else {
|
||||||
|
int potionOffset = 0;
|
||||||
|
for (var instance : effects) {
|
||||||
|
int durationSeconds = instance.getDuration() / 20;
|
||||||
|
String time = String.format("%d:%02d", durationSeconds / 60, durationSeconds % 60);
|
||||||
|
String name = Text.translatable(instance.getTranslationKey()).getString();
|
||||||
|
String level = instance.getAmplifier() > 0 ? " " + (instance.getAmplifier() + 1) : "";
|
||||||
|
var effect = instance.getEffectType().value();
|
||||||
|
String fullText = name + level + " (" + time + ")";
|
||||||
|
|
||||||
|
int color = (configColor == -1 || configColor == 0xFFFFFF) ?
|
||||||
|
(effect.getCategory() == StatusEffectCategory.BENEFICIAL ? 0x55FF55 : 0xFF5555) : configColor;
|
||||||
|
|
||||||
|
context.drawText(client.textRenderer, fullText, x, y + potionOffset, color, true);
|
||||||
|
potionOffset += 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package de.winniepat.citrus.draggui;
|
||||||
|
|
||||||
|
import io.wispforest.owo.ui.component.Components;
|
||||||
|
import io.wispforest.owo.ui.container.Containers;
|
||||||
|
import io.wispforest.owo.ui.container.FlowLayout;
|
||||||
|
import io.wispforest.owo.ui.core.*;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class HudUtils {
|
||||||
|
public static Component createButton(String label, String texture, String tooltip, boolean initialState, Consumer<Boolean> onToggle) {
|
||||||
|
var textureComponent = Components.texture(Identifier.of("citrus", "textures/modules/" + texture + ".png"), 0, 0, 48, 48, 48, 48);
|
||||||
|
|
||||||
|
var wrapper = Containers.verticalFlow(Sizing.content(), Sizing.content());
|
||||||
|
wrapper.child(textureComponent);
|
||||||
|
wrapper.padding(Insets.of(2));
|
||||||
|
|
||||||
|
wrapper.mouseDown().subscribe((x, y, b) -> {
|
||||||
|
if (b != 0) return false;
|
||||||
|
boolean newState = !HudConfig.INSTANCE.isModuleEnabled(label);
|
||||||
|
onToggle.accept(newState);
|
||||||
|
updateWrapperVisuals(wrapper, newState);
|
||||||
|
HudConfig.save();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.tooltip(Text.literal(tooltip));
|
||||||
|
wrapper.cursorStyle(CursorStyle.HAND);
|
||||||
|
updateWrapperVisuals(wrapper, initialState);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateWrapperVisuals(FlowLayout wrapper, boolean enabled) {
|
||||||
|
int color = enabled ? Color.ofArgb(0xFF00FF00).argb() : Color.ofArgb(0xFFFF0000).argb();
|
||||||
|
wrapper.surface((context, component) -> {
|
||||||
|
int x = component.x();
|
||||||
|
int y = component.y();
|
||||||
|
int w = component.width();
|
||||||
|
int h = component.height();
|
||||||
|
|
||||||
|
context.fill(x + 4, y, x + w - 4, y + 1, color);
|
||||||
|
context.fill(x + 4, y + h - 1, x + w - 4, y + h, color);
|
||||||
|
context.fill(x, y + 4, x + 1, y + h - 4, color);
|
||||||
|
context.fill(x + w - 1, y + 4, x + w, y + h - 4, color);
|
||||||
|
|
||||||
|
context.fill(x + 2, y + 1, x + 4, y + 2, color);
|
||||||
|
context.fill(x + 1, y + 2, x + 2, y + 4, color);
|
||||||
|
|
||||||
|
context.fill(x + w - 4, y + 1, x + w - 2, y + 2, color);
|
||||||
|
context.fill(x + w - 2, y + 2, x + w - 1, y + 4, color);
|
||||||
|
|
||||||
|
context.fill(x + 2, y + h - 2, x + 4, y + h - 1, color);
|
||||||
|
context.fill(x + 1, y + h - 4, x + 2, y + h - 2, color);
|
||||||
|
|
||||||
|
context.fill(x + w - 4, y + h - 2, x + w - 2, y + h - 1, color);
|
||||||
|
context.fill(x + w - 2, y + h - 4, x + w - 1, y + h - 2, color);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package de.winniepat.citrus.gui;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.*;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.sync.CapeSyncManager;
|
||||||
|
import de.winniepat.citrus.utils.toasts.CustomToast;
|
||||||
|
import io.wispforest.owo.ui.base.BaseOwoScreen;
|
||||||
|
import io.wispforest.owo.ui.component.*;
|
||||||
|
import io.wispforest.owo.ui.container.*;
|
||||||
|
import io.wispforest.owo.ui.core.*;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.*;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.lwjgl.glfw.GLFW;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class CapeSelectionScreen extends BaseOwoScreen<FlowLayout> {
|
||||||
|
|
||||||
|
private final Map<String, ButtonComponent> capeButtons = new HashMap<>();
|
||||||
|
|
||||||
|
private LabelComponent statusLabel;
|
||||||
|
private LabelComponent currentCapeLabel;
|
||||||
|
private LabelComponent capeCountLabel;
|
||||||
|
private FlowLayout capeGrid;
|
||||||
|
private float updateTimer = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected @NotNull OwoUIAdapter<FlowLayout> createAdapter() {
|
||||||
|
return OwoUIAdapter.create(this, Containers::verticalFlow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void build(FlowLayout root) {
|
||||||
|
root.alignment(HorizontalAlignment.CENTER,VerticalAlignment.CENTER);
|
||||||
|
|
||||||
|
FlowLayout main = (FlowLayout) Containers.verticalFlow(Sizing.fixed(350), Sizing.fixed(420))
|
||||||
|
.padding(Insets.of(10)).alignment(HorizontalAlignment.CENTER,VerticalAlignment.CENTER);
|
||||||
|
|
||||||
|
FlowLayout content = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content())
|
||||||
|
.gap(6)
|
||||||
|
.padding(Insets.of(8))
|
||||||
|
.surface(Surface.DARK_PANEL);
|
||||||
|
|
||||||
|
FlowLayout currentRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content())
|
||||||
|
.gap(6)
|
||||||
|
.horizontalAlignment(HorizontalAlignment.CENTER);
|
||||||
|
currentRow.child(Components.label(Text.literal("Current:").formatted(Formatting.BOLD)));
|
||||||
|
currentCapeLabel = Components.label(Text.literal("None"));
|
||||||
|
currentRow.child(currentCapeLabel);
|
||||||
|
content.child(currentRow);
|
||||||
|
|
||||||
|
FlowLayout statusRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content())
|
||||||
|
.gap(6)
|
||||||
|
.horizontalAlignment(HorizontalAlignment.CENTER);
|
||||||
|
statusRow.child(Components.label(Text.literal("Status:").formatted(Formatting.BOLD)));
|
||||||
|
statusLabel = Components.label(Text.literal("Checking..."));
|
||||||
|
statusRow.child(statusLabel);
|
||||||
|
content.child(statusRow);
|
||||||
|
|
||||||
|
FlowLayout countRow = (FlowLayout) Containers.horizontalFlow(Sizing.fill(100), Sizing.content())
|
||||||
|
.gap(6)
|
||||||
|
.horizontalAlignment(HorizontalAlignment.CENTER);
|
||||||
|
countRow.child(Components.label(Text.literal("Available:").formatted(Formatting.BOLD)));
|
||||||
|
capeCountLabel = Components.label(Text.literal("0"));
|
||||||
|
countRow.child(capeCountLabel);
|
||||||
|
content.child(countRow);
|
||||||
|
|
||||||
|
|
||||||
|
capeGrid = (FlowLayout) Containers.verticalFlow(Sizing.fill(100), Sizing.content())
|
||||||
|
.gap(6)
|
||||||
|
.padding(Insets.of(4));
|
||||||
|
|
||||||
|
var scroll = Containers.verticalScroll(
|
||||||
|
Sizing.fill(100),
|
||||||
|
Sizing.fixed(180),
|
||||||
|
capeGrid
|
||||||
|
);
|
||||||
|
|
||||||
|
content.child(scroll);
|
||||||
|
main.child(content);
|
||||||
|
root.child(main);
|
||||||
|
|
||||||
|
updateStatus();
|
||||||
|
populateCapeGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateCapeGrid() {
|
||||||
|
capeGrid.clearChildren();
|
||||||
|
capeButtons.clear();
|
||||||
|
|
||||||
|
var capes = CapeHandler.getAvailableCapes();
|
||||||
|
var client = MinecraftClient.getInstance();
|
||||||
|
if (client.world == null || client.player == null) return;
|
||||||
|
|
||||||
|
capeCountLabel.text(Text.literal(String.valueOf(capes.size())));
|
||||||
|
|
||||||
|
if (capes.isEmpty()) {
|
||||||
|
capeGrid.child(Components.label(Text.literal("No capes available")).horizontalSizing(Sizing.fill(100)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int perRow = 3;
|
||||||
|
FlowLayout row = null;
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for (String cape : capes) {
|
||||||
|
if (i++ % perRow == 0) {
|
||||||
|
row = Containers.horizontalFlow(Sizing.fill(100), Sizing.content()).gap(8);
|
||||||
|
capeGrid.child(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowLayout capeEntry = Containers.verticalFlow(Sizing.fixed(100), Sizing.content());
|
||||||
|
capeEntry
|
||||||
|
.gap(4)
|
||||||
|
.horizontalAlignment(HorizontalAlignment.CENTER)
|
||||||
|
.padding(Insets.bottom(20));
|
||||||
|
|
||||||
|
Identifier textureId = Identifier.of("citrus", "capes/" + cape);
|
||||||
|
|
||||||
|
var preview = Components.texture(textureId, 0, 0, 64, 32, 64, 32)
|
||||||
|
.sizing(Sizing.fixed(80), Sizing.fixed(40));
|
||||||
|
|
||||||
|
preview.tooltip(Text.literal(cape));
|
||||||
|
|
||||||
|
ButtonComponent button = (ButtonComponent) Components.button(
|
||||||
|
Text.literal(capitalizeFirst(formatCapeName(cape))),
|
||||||
|
b -> equipCape(cape)
|
||||||
|
).horizontalSizing(Sizing.fixed(100)).verticalSizing(Sizing.fixed(20));
|
||||||
|
|
||||||
|
capeButtons.put(cape, button.renderer(
|
||||||
|
ButtonComponent.Renderer.flat(
|
||||||
|
0x5FFFFFFF,
|
||||||
|
0x8FFFFFFF,
|
||||||
|
0x1FFFFFFF
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
capeEntry.child(preview);
|
||||||
|
capeEntry.child(button);
|
||||||
|
row.child(capeEntry);
|
||||||
|
|
||||||
|
if (!CapeHandler.isCapeLoaded(cape)) {
|
||||||
|
CapeHandler.preloadSpecificCapes(Collections.singletonList(cape));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllButtonStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||||
|
super.render(context, mouseX, mouseY, delta);
|
||||||
|
|
||||||
|
updateTimer += delta;
|
||||||
|
if (updateTimer > 10) {
|
||||||
|
updateAllButtonStates();
|
||||||
|
updateTimer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCapeName(String name) {
|
||||||
|
name = name.replace("_", " ");
|
||||||
|
return name.length() > 10 ? name.substring(0, 10) + ".." : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void equipCape(String cape) {
|
||||||
|
if (!CapeHandler.capeExists(cape)) return;
|
||||||
|
|
||||||
|
if ("failed".equals(CapeHandler.getCapeStatus(cape))) {
|
||||||
|
setStatusMessage("§cFailed to load cape!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CapeHandler.setLocalPlayerCape(cape, false);
|
||||||
|
updateCurrentCapeDisplay();
|
||||||
|
CustomToast.sendCustomToast("Equipped Cape", 0xFF55FF55, "You equipped cape " + capitalizeFirst(cape), 0xFFBBBBBB);
|
||||||
|
updateAllButtonStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatus() {
|
||||||
|
CapeSyncManager.checkServerHealth().thenAccept(ok ->
|
||||||
|
MinecraftClient.getInstance().execute(() ->
|
||||||
|
setStatusMessage(ok ? "Server online" : "Server offline")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
updateCurrentCapeDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCurrentCapeDisplay() {
|
||||||
|
String cape = CapeHandler.getCurrentCapeForLocalPlayer();
|
||||||
|
currentCapeLabel.text(Text.literal(
|
||||||
|
cape == null || "none".equals(cape) ? "None" :
|
||||||
|
"auto".equals(cape) ? "Auto" : cape
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAllButtonStates() {
|
||||||
|
String current = CapeHandler.getCurrentCapeForLocalPlayer();
|
||||||
|
|
||||||
|
for (var entry : capeButtons.entrySet()) {
|
||||||
|
String capeName = entry.getKey();
|
||||||
|
var status = CapeHandler.getCapeStatus(capeName);
|
||||||
|
|
||||||
|
boolean isEquipped = capeName.equals(current);
|
||||||
|
|
||||||
|
entry.getValue().active(!isEquipped);
|
||||||
|
|
||||||
|
if ("failed".equals(status)) {
|
||||||
|
entry.getValue().setMessage(Text.literal("Error").formatted(Formatting.RED));
|
||||||
|
} else if ("loading".equals(status)) {
|
||||||
|
entry.getValue().setMessage(Text.literal("Loading...").formatted(Formatting.YELLOW));
|
||||||
|
} else {
|
||||||
|
entry.getValue().setMessage(Text.literal(capitalizeFirst(formatCapeName(capeName))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setStatusMessage(String msg) {
|
||||||
|
statusLabel.text(Text.literal(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String capitalizeFirst(String text) {
|
||||||
|
return text == null || text.isEmpty()
|
||||||
|
? text
|
||||||
|
: text.substring(0, 1).toUpperCase(Locale.ROOT)
|
||||||
|
+ text.substring(1).toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean keyPressed(int key, int scancode, int mods) {
|
||||||
|
if (key == GLFW.GLFW_KEY_ESCAPE) {
|
||||||
|
close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.keyPressed(key, scancode, mods);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
assert client != null;
|
||||||
|
client.setScreen(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
package de.winniepat.citrus.gui;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.CapeHandler;
|
||||||
|
import de.winniepat.citrus.cosmetics.wings.ClientWingManager;
|
||||||
|
import de.winniepat.citrus.cosmetics.wings.WingType;
|
||||||
|
import de.winniepat.citrus.draggui.HudConfig;
|
||||||
|
import icyllis.modernui.core.Context;
|
||||||
|
import icyllis.modernui.fragment.Fragment;
|
||||||
|
import icyllis.modernui.graphics.Color;
|
||||||
|
import icyllis.modernui.graphics.drawable.ImageDrawable;
|
||||||
|
import icyllis.modernui.graphics.drawable.ShapeDrawable;
|
||||||
|
import icyllis.modernui.util.DataSet;
|
||||||
|
import icyllis.modernui.view.Gravity;
|
||||||
|
import icyllis.modernui.view.LayoutInflater;
|
||||||
|
import icyllis.modernui.view.View;
|
||||||
|
import icyllis.modernui.view.ViewGroup;
|
||||||
|
import icyllis.modernui.widget.*;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class CosmeticsFragment extends Fragment {
|
||||||
|
private LinearLayout contentArea;
|
||||||
|
private TextView contentTitle;
|
||||||
|
private Button capesButton;
|
||||||
|
private Button wingsButton;
|
||||||
|
private ShapeDrawable highlighted;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable DataSet savedInstanceState) {
|
||||||
|
Context context = requireContext();
|
||||||
|
HudConfig.load();
|
||||||
|
|
||||||
|
FrameLayout wrapper = new FrameLayout(context);
|
||||||
|
wrapper.setLayoutParams(new ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
|
||||||
|
LinearLayout mainLayout = new LinearLayout(context);
|
||||||
|
mainLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
mainLayout.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams mainParams = new FrameLayout.LayoutParams(
|
||||||
|
dp(1200),
|
||||||
|
dp(800)
|
||||||
|
);
|
||||||
|
mainParams.gravity = Gravity.CENTER;
|
||||||
|
mainLayout.setLayoutParams(mainParams);
|
||||||
|
|
||||||
|
mainLayout.setPadding(dp(10), dp(10), dp(10), dp(10));
|
||||||
|
ShapeDrawable background = new ShapeDrawable();
|
||||||
|
background.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
background.setColor(0x50000000);
|
||||||
|
background.setCornerRadius(20);
|
||||||
|
background.setStroke(dp(10), 0x305d5b5f);
|
||||||
|
background.setCornerRadii(dp(20), 20, dp(20), 20);
|
||||||
|
mainLayout.setBackground(background);
|
||||||
|
|
||||||
|
LinearLayout sidebar = new LinearLayout(context);
|
||||||
|
sidebar.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
sidebar.setPadding(dp(16), dp(16), dp(16), dp(16));
|
||||||
|
LinearLayout.LayoutParams sidebarParams = new LinearLayout.LayoutParams(
|
||||||
|
dp(200),
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
);
|
||||||
|
sidebar.setLayoutParams(sidebarParams);
|
||||||
|
|
||||||
|
ShapeDrawable sidebarBackground = new ShapeDrawable();
|
||||||
|
sidebarBackground.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
sidebarBackground.setColor(0x80000000);
|
||||||
|
sidebarBackground.setCornerRadii(10,10,10,10);
|
||||||
|
sidebar.setBackground(sidebarBackground);
|
||||||
|
|
||||||
|
TextView sidebarTitle = new TextView(context);
|
||||||
|
sidebarTitle.setText("Menu");
|
||||||
|
sidebarTitle.setTextSize(20);
|
||||||
|
sidebarTitle.setTextColor(Color.WHITE);
|
||||||
|
sidebarTitle.setPadding(dp(8), dp(8), dp(8), dp(16));
|
||||||
|
sidebar.addView(sidebarTitle);
|
||||||
|
|
||||||
|
capesButton = new Button(context);
|
||||||
|
capesButton.setText("Capes");
|
||||||
|
capesButton.setTextColor(Color.WHITE);
|
||||||
|
LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
dp(40)
|
||||||
|
);
|
||||||
|
buttonParams.setMargins(0, dp(4), 0, dp(4));
|
||||||
|
capesButton.setLayoutParams(buttonParams);
|
||||||
|
capesButton.setOnClickListener(v -> showCapesTab());
|
||||||
|
sidebar.addView(capesButton);
|
||||||
|
|
||||||
|
wingsButton = new Button(context);
|
||||||
|
wingsButton.setText("Wings");
|
||||||
|
wingsButton.setTextColor(Color.WHITE);
|
||||||
|
wingsButton.setOnClickListener(v -> showWingsTab());
|
||||||
|
LinearLayout.LayoutParams wingsParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
dp(40)
|
||||||
|
);
|
||||||
|
wingsParams.setMargins(0, dp(4), 0, dp(4));
|
||||||
|
wingsButton.setLayoutParams(wingsParams);
|
||||||
|
sidebar.addView(wingsButton);
|
||||||
|
|
||||||
|
mainLayout.addView(sidebar);
|
||||||
|
|
||||||
|
contentArea = new LinearLayout(context);
|
||||||
|
contentArea.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
contentArea.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
0,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
1.0f
|
||||||
|
));
|
||||||
|
|
||||||
|
mainLayout.addView(contentArea);
|
||||||
|
wrapper.addView(mainLayout);
|
||||||
|
|
||||||
|
showCapesTab();
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCapesTab() {
|
||||||
|
System.out.println("Switching to Capes tab");
|
||||||
|
Context context = requireContext();
|
||||||
|
contentArea.removeAllViews();
|
||||||
|
|
||||||
|
updateButtonStates(true);
|
||||||
|
|
||||||
|
LinearLayout root = new LinearLayout(context);
|
||||||
|
root.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
root.setPadding(dp(16), dp(16), dp(16), dp(16));
|
||||||
|
|
||||||
|
contentTitle = new TextView(context);
|
||||||
|
contentTitle.setText("Select a Cape");
|
||||||
|
contentTitle.setTextSize(24);
|
||||||
|
contentTitle.setTextColor(Color.WHITE);
|
||||||
|
root.addView(contentTitle);
|
||||||
|
|
||||||
|
List<String> capes = CapeHandler.getAvailableCapes();
|
||||||
|
int perRow = 3;
|
||||||
|
int i = 0;
|
||||||
|
LinearLayout row = null;
|
||||||
|
|
||||||
|
for (String cape : capes) {
|
||||||
|
if (i % perRow == 0) {
|
||||||
|
row = new LinearLayout(context);
|
||||||
|
row.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
));
|
||||||
|
root.addView(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout buttonContainer = new LinearLayout(context);
|
||||||
|
buttonContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
buttonContainer.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
ShapeDrawable customShape = new ShapeDrawable();
|
||||||
|
customShape.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
customShape.setStroke(dp(4), 0x605d5b5f);
|
||||||
|
customShape.setCornerRadii(dp(10), 10, dp(10), 10);
|
||||||
|
buttonContainer.setBackground(customShape);
|
||||||
|
|
||||||
|
ImageView capeImage = new ImageView(context);
|
||||||
|
ImageDrawable imageDrawable = new ImageDrawable("citrus", "capes/" + cape + ".png");
|
||||||
|
capeImage.setImageDrawable(imageDrawable);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams imageParams = new LinearLayout.LayoutParams(
|
||||||
|
dp(64),
|
||||||
|
dp(80)
|
||||||
|
);
|
||||||
|
imageParams.setMargins(dp(4), dp(4), dp(4), dp(4));
|
||||||
|
capeImage.setLayoutParams(imageParams);
|
||||||
|
buttonContainer.addView(capeImage);
|
||||||
|
|
||||||
|
TextView nameView = new TextView(context);
|
||||||
|
nameView.setText(cape);
|
||||||
|
nameView.setTextColor(Color.WHITE);
|
||||||
|
nameView.setTextSize(12);
|
||||||
|
nameView.setGravity(Gravity.CENTER);
|
||||||
|
nameView.setPadding(dp(4), dp(4), dp(4), dp(4));
|
||||||
|
buttonContainer.addView(nameView);
|
||||||
|
|
||||||
|
buttonContainer.setOnClickListener(v -> {
|
||||||
|
CapeHandler.setLocalPlayerCape(cape, false);
|
||||||
|
contentTitle.setText("Equipped: " + cape);
|
||||||
|
});
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
dp(90),
|
||||||
|
dp(120)
|
||||||
|
);
|
||||||
|
params.setMargins(dp(4), dp(4), dp(4), dp(4));
|
||||||
|
buttonContainer.setLayoutParams(params);
|
||||||
|
|
||||||
|
row.addView(buttonContainer);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView scrollView = new ScrollView(context);
|
||||||
|
FrameLayout.LayoutParams scrollParams = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
);
|
||||||
|
scrollParams.gravity = Gravity.CENTER;
|
||||||
|
scrollView.setLayoutParams(scrollParams);
|
||||||
|
|
||||||
|
scrollView.addView(root);
|
||||||
|
contentArea.addView(scrollView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showWingsTab() {
|
||||||
|
System.out.println("Switching to Wings tab");
|
||||||
|
Context context = requireContext();
|
||||||
|
contentArea.removeAllViews();
|
||||||
|
|
||||||
|
updateButtonStates(false);
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) {
|
||||||
|
TextView error = new TextView(context);
|
||||||
|
error.setText("You must be in-game!");
|
||||||
|
error.setTextColor(0xFFFF5555);
|
||||||
|
error.setGravity(Gravity.CENTER);
|
||||||
|
contentArea.addView(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID playerUuid = client.player.getUuid();
|
||||||
|
|
||||||
|
LinearLayout root = new LinearLayout(context);
|
||||||
|
root.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
root.setPadding(dp(16), dp(16), dp(16), dp(16));
|
||||||
|
|
||||||
|
TextView title = new TextView(context);
|
||||||
|
title.setText("Wings Configuration");
|
||||||
|
title.setTextSize(24);
|
||||||
|
title.setTextColor(Color.WHITE);
|
||||||
|
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
titleParams.setMargins(0, 0, 0, dp(20));
|
||||||
|
root.addView(title, titleParams);
|
||||||
|
|
||||||
|
LinearLayout toggleRow = new LinearLayout(context);
|
||||||
|
toggleRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
toggleRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams toggleParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
toggleParams.setMargins(0, 0, 0, dp(20));
|
||||||
|
|
||||||
|
TextView toggleLabel = new TextView(context);
|
||||||
|
toggleLabel.setText("Show Wings:");
|
||||||
|
toggleLabel.setTextSize(16);
|
||||||
|
toggleLabel.setTextColor(Color.WHITE);
|
||||||
|
|
||||||
|
Switch wingsToggle = new Switch(context);
|
||||||
|
wingsToggle.setChecked(ClientWingManager.areWingsVisible(playerUuid));
|
||||||
|
wingsToggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||||
|
if (isChecked != ClientWingManager.areWingsVisible(playerUuid)) {
|
||||||
|
ClientWingManager.toggleWings(playerUuid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleRow.addView(toggleLabel);
|
||||||
|
LinearLayout.LayoutParams switchParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
switchParams.setMargins(dp(10), 0, 0, 0);
|
||||||
|
toggleRow.addView(wingsToggle, switchParams);
|
||||||
|
root.addView(toggleRow, toggleParams);
|
||||||
|
|
||||||
|
TextView typeLabel = new TextView(context);
|
||||||
|
typeLabel.setText("Wing Type:");
|
||||||
|
typeLabel.setTextSize(18);
|
||||||
|
typeLabel.setTextColor(Color.WHITE);
|
||||||
|
LinearLayout.LayoutParams typeLabelParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
typeLabelParams.setMargins(0, 0, 0, dp(10));
|
||||||
|
root.addView(typeLabel, typeLabelParams);
|
||||||
|
|
||||||
|
String currentWingType = ClientWingManager.getActiveWings(playerUuid);
|
||||||
|
|
||||||
|
int perRow = 3;
|
||||||
|
int i = 0;
|
||||||
|
LinearLayout row = null;
|
||||||
|
|
||||||
|
for (WingType wingType : WingType.getAvailableTypes()) {
|
||||||
|
if (i % perRow == 0) {
|
||||||
|
row = new LinearLayout(context);
|
||||||
|
row.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
));
|
||||||
|
root.addView(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout wingContainer = new LinearLayout(context);
|
||||||
|
wingContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
wingContainer.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
boolean isSelected = wingType.getId().equals(currentWingType);
|
||||||
|
ShapeDrawable wingShape = new ShapeDrawable();
|
||||||
|
wingShape.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
wingShape.setStroke(dp(4), isSelected ? 0xFF55FF55 : 0x605d5b5f);
|
||||||
|
wingShape.setCornerRadii(dp(10), 10, dp(10), 10);
|
||||||
|
wingContainer.setBackground(wingShape);
|
||||||
|
|
||||||
|
ImageView wingImage = new ImageView(context);
|
||||||
|
ImageDrawable imageDrawable = new ImageDrawable("citrus", "textures/cosmetics/" + wingType.getId() + ".png");
|
||||||
|
wingImage.setImageDrawable(imageDrawable);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams imageParams = new LinearLayout.LayoutParams(
|
||||||
|
dp(64),
|
||||||
|
dp(80)
|
||||||
|
);
|
||||||
|
imageParams.setMargins(dp(4), dp(4), dp(4), dp(4));
|
||||||
|
wingImage.setLayoutParams(imageParams);
|
||||||
|
wingContainer.addView(wingImage);
|
||||||
|
|
||||||
|
TextView wingName = new TextView(context);
|
||||||
|
wingName.setText(formatWingTypeName(wingType.getId()));
|
||||||
|
wingName.setTextColor(Color.WHITE);
|
||||||
|
wingName.setTextSize(12);
|
||||||
|
wingName.setGravity(Gravity.CENTER);
|
||||||
|
wingName.setPadding(dp(4), dp(4), dp(4), dp(4));
|
||||||
|
wingContainer.addView(wingName);
|
||||||
|
|
||||||
|
wingContainer.setOnClickListener(v -> {
|
||||||
|
ClientWingManager.setActiveWings(playerUuid, wingType.getId());
|
||||||
|
showWingsTab();
|
||||||
|
});
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
dp(90),
|
||||||
|
dp(120)
|
||||||
|
);
|
||||||
|
params.setMargins(dp(4), dp(4), dp(4), dp(4));
|
||||||
|
wingContainer.setLayoutParams(params);
|
||||||
|
|
||||||
|
row.addView(wingContainer);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView scrollView = new ScrollView(context);
|
||||||
|
scrollView.addView(root);
|
||||||
|
contentArea.addView(scrollView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateButtonStates(boolean capesActive) {
|
||||||
|
if (capesActive) {
|
||||||
|
capesButton.setBackground(highlighted);
|
||||||
|
wingsButton.setBackground(null);
|
||||||
|
} else {
|
||||||
|
capesButton.setBackground(null);
|
||||||
|
wingsButton.setBackground(highlighted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatWingTypeName(String id) {
|
||||||
|
String[] parts = id.split("_");
|
||||||
|
StringBuilder formatted = new StringBuilder();
|
||||||
|
for (String part : parts) {
|
||||||
|
if (formatted.length() > 0) {
|
||||||
|
formatted.append(" ");
|
||||||
|
}
|
||||||
|
formatted.append(part.substring(0, 1).toUpperCase())
|
||||||
|
.append(part.substring(1).toLowerCase());
|
||||||
|
}
|
||||||
|
return formatted.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dp(int value) {
|
||||||
|
return (int) (value * getContext().getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,732 @@
|
|||||||
|
package de.winniepat.citrus.gui;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.managers.FriendManager;
|
||||||
|
import de.winniepat.citrus.managers.FriendManager.Friend;
|
||||||
|
import de.winniepat.citrus.managers.FriendManager.FriendRequest;
|
||||||
|
import de.winniepat.citrus.managers.WallpaperManager;
|
||||||
|
import de.winniepat.citrus.managers.ConfigManager;
|
||||||
|
import de.winniepat.citrus.utils.texture.CdnTextureDrawable;
|
||||||
|
import de.winniepat.citrus.utils.toasts.CustomToast;
|
||||||
|
import icyllis.modernui.animation.*;
|
||||||
|
import icyllis.modernui.core.Context;
|
||||||
|
import icyllis.modernui.fragment.Fragment;
|
||||||
|
import icyllis.modernui.graphics.drawable.*;
|
||||||
|
import icyllis.modernui.mc.*;
|
||||||
|
import icyllis.modernui.text.InputFilter;
|
||||||
|
import icyllis.modernui.text.TextPaint;
|
||||||
|
import icyllis.modernui.util.DataSet;
|
||||||
|
import icyllis.modernui.view.*;
|
||||||
|
import icyllis.modernui.widget.*;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import org.jetbrains.annotations.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FriendsFragment extends Fragment implements ScreenCallback {
|
||||||
|
|
||||||
|
private LinearLayout mFriendsListContainer;
|
||||||
|
private LinearLayout mRequestsListContainer;
|
||||||
|
private EditText mAddFriendInput;
|
||||||
|
private CdnTextureDrawable mBackground1Drawable;
|
||||||
|
private final List<View> mStaggeredViews = new ArrayList<>();
|
||||||
|
|
||||||
|
private float getGuiScale() {
|
||||||
|
return (float) MinecraftClient.getInstance().getWindow().getScaleFactor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFixedTextSize(TextView tv, float px) {
|
||||||
|
float scale = getGuiScale();
|
||||||
|
tv.setTextSize(px / scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable DataSet savedInstanceState) {
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
FrameLayout root = new FrameLayout(context);
|
||||||
|
root.setLayoutParams(new ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
|
||||||
|
FrameLayout mBackgroundView = new FrameLayout(context);
|
||||||
|
mBackgroundView.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
mBackgroundView.setScaleX(1.1f);
|
||||||
|
mBackgroundView.setScaleY(1.1f);
|
||||||
|
|
||||||
|
View mBackgroundView1 = new View(context);
|
||||||
|
mBackgroundView1.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
mBackgroundView.addView(mBackgroundView1);
|
||||||
|
|
||||||
|
root.addView(mBackgroundView);
|
||||||
|
|
||||||
|
mBackground1Drawable = new CdnTextureDrawable();
|
||||||
|
mBackgroundView1.setBackground(mBackground1Drawable);
|
||||||
|
|
||||||
|
String currentWallpaper = ConfigManager.config.mainmenuWallpaper;
|
||||||
|
if (currentWallpaper == null || currentWallpaper.isEmpty()) {
|
||||||
|
currentWallpaper = "default.png";
|
||||||
|
}
|
||||||
|
WallpaperManager.loadWallpaperWithData(currentWallpaper, imageData -> {
|
||||||
|
MuiModApi.postToUiThread(() -> mBackground1Drawable.setImageData(imageData));
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectAnimator bgPulseX = ObjectAnimator.ofFloat(mBackgroundView, View.SCALE_X, 1.1f, 1.15f);
|
||||||
|
ObjectAnimator bgPulseY = ObjectAnimator.ofFloat(mBackgroundView, View.SCALE_Y, 1.1f, 1.15f);
|
||||||
|
bgPulseX.setDuration(15000);
|
||||||
|
bgPulseY.setDuration(15000);
|
||||||
|
bgPulseX.setRepeatCount(ValueAnimator.INFINITE);
|
||||||
|
bgPulseY.setRepeatCount(ValueAnimator.INFINITE);
|
||||||
|
bgPulseX.setRepeatMode(ValueAnimator.REVERSE);
|
||||||
|
bgPulseY.setRepeatMode(ValueAnimator.REVERSE);
|
||||||
|
bgPulseX.start();
|
||||||
|
bgPulseY.start();
|
||||||
|
|
||||||
|
FrameLayout contentWrapper = new FrameLayout(context);
|
||||||
|
contentWrapper.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
root.addView(contentWrapper);
|
||||||
|
|
||||||
|
LinearLayout leftPanel = new LinearLayout(context);
|
||||||
|
leftPanel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
FrameLayout.LayoutParams leftPanelLp = new FrameLayout.LayoutParams(
|
||||||
|
300,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
leftPanelLp.gravity = Gravity.CENTER_VERTICAL;
|
||||||
|
leftPanelLp.setMargins(60, 0, 0, 0);
|
||||||
|
leftPanel.setLayoutParams(leftPanelLp);
|
||||||
|
|
||||||
|
TextView title = new TextView(context);
|
||||||
|
title.setText("FRIENDS");
|
||||||
|
setFixedTextSize(title, 96);
|
||||||
|
title.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
title.setTextColor(0xFFFFFFFF);
|
||||||
|
leftPanel.addView(title);
|
||||||
|
mStaggeredViews.add(title);
|
||||||
|
|
||||||
|
TextView subtitle = new TextView(context);
|
||||||
|
subtitle.setText("Manage your friends list");
|
||||||
|
setFixedTextSize(subtitle, 32);
|
||||||
|
subtitle.setTextColor(0xCCFFFFFF);
|
||||||
|
LinearLayout.LayoutParams subtitleLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
subtitleLp.setMargins(0, 0, 0, dp(30));
|
||||||
|
subtitle.setLayoutParams(subtitleLp);
|
||||||
|
leftPanel.addView(subtitle);
|
||||||
|
mStaggeredViews.add(subtitle);
|
||||||
|
|
||||||
|
LinearLayout addSection = new LinearLayout(context);
|
||||||
|
addSection.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
LinearLayout.LayoutParams addSectionLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
addSectionLp.setMargins(0, 0, 0, dp(20));
|
||||||
|
addSection.setLayoutParams(addSectionLp);
|
||||||
|
|
||||||
|
TextView addLabel = new TextView(context);
|
||||||
|
addLabel.setText("Add a Friend");
|
||||||
|
setFixedTextSize(addLabel, 28);
|
||||||
|
addLabel.setTextColor(0x88FFFFFF);
|
||||||
|
addLabel.setPadding(0, 0, 0, dp(8));
|
||||||
|
addSection.addView(addLabel);
|
||||||
|
|
||||||
|
mAddFriendInput = new EditText(context);
|
||||||
|
mAddFriendInput.setHint("Enter player name...");
|
||||||
|
setFixedTextSize(mAddFriendInput, 32);
|
||||||
|
mAddFriendInput.setTextColor(0xFFFFFFFF);
|
||||||
|
mAddFriendInput.setHintTextColor(0x66FFFFFF);
|
||||||
|
mAddFriendInput.setPadding(dp(16), dp(12), dp(16), dp(12));
|
||||||
|
mAddFriendInput.setSingleLine(true);
|
||||||
|
mAddFriendInput.setFilters(new InputFilter.LengthFilter(16));
|
||||||
|
ShapeDrawable inputBg = new ShapeDrawable();
|
||||||
|
inputBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
inputBg.setColor(0x26FFFFFF);
|
||||||
|
inputBg.setCornerRadius(dp(10));
|
||||||
|
mAddFriendInput.setBackground(inputBg);
|
||||||
|
LinearLayout.LayoutParams inputLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
inputLp.setMargins(0, 0, 0, dp(8));
|
||||||
|
mAddFriendInput.setLayoutParams(inputLp);
|
||||||
|
addSection.addView(mAddFriendInput);
|
||||||
|
|
||||||
|
leftPanel.addView(addSection);
|
||||||
|
mStaggeredViews.add(addSection);
|
||||||
|
|
||||||
|
addNavButton(leftPanel, "Send Request", "multiplayer_icon", v -> sendFriendRequest());
|
||||||
|
addNavButton(leftPanel, "Refresh", "settings_icon", v -> refreshData());
|
||||||
|
addNavButton(leftPanel, "Back to Menu", "door_icon", v -> {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(MuiModApi.get().createScreen(new MainMenuScreen(), null, null));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
contentWrapper.addView(leftPanel);
|
||||||
|
|
||||||
|
LinearLayout rightPanel = new LinearLayout(context);
|
||||||
|
rightPanel.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
rightPanel.setGravity(Gravity.TOP);
|
||||||
|
FrameLayout.LayoutParams rightPanelLp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
rightPanelLp.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT;
|
||||||
|
rightPanelLp.setMargins(0, 0, 60, 0);
|
||||||
|
rightPanel.setLayoutParams(rightPanelLp);
|
||||||
|
|
||||||
|
LinearLayout friendsPanel = createListPanel(context, "FRIENDS", true);
|
||||||
|
LinearLayout.LayoutParams friendsPanelLp = new LinearLayout.LayoutParams(
|
||||||
|
320,
|
||||||
|
450
|
||||||
|
);
|
||||||
|
friendsPanelLp.setMargins(0, 0, dp(20), 0);
|
||||||
|
friendsPanel.setLayoutParams(friendsPanelLp);
|
||||||
|
rightPanel.addView(friendsPanel);
|
||||||
|
mStaggeredViews.add(friendsPanel);
|
||||||
|
|
||||||
|
LinearLayout requestsPanel = createListPanel(context, "REQUESTS", false);
|
||||||
|
LinearLayout.LayoutParams requestsPanelLp = new LinearLayout.LayoutParams(
|
||||||
|
320,
|
||||||
|
450
|
||||||
|
);
|
||||||
|
requestsPanel.setLayoutParams(requestsPanelLp);
|
||||||
|
rightPanel.addView(requestsPanel);
|
||||||
|
mStaggeredViews.add(requestsPanel);
|
||||||
|
|
||||||
|
contentWrapper.addView(rightPanel);
|
||||||
|
|
||||||
|
TextView infoText = new TextView(context);
|
||||||
|
infoText.setText("Add your friends and see what they are up to • Online status updates every minute");
|
||||||
|
setFixedTextSize(infoText, 24);
|
||||||
|
infoText.setTextColor(0x66FFFFFF);
|
||||||
|
FrameLayout.LayoutParams infoLp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
infoLp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
|
||||||
|
infoLp.setMargins(0, 0, 0, dp(20));
|
||||||
|
infoText.setLayoutParams(infoLp);
|
||||||
|
contentWrapper.addView(infoText);
|
||||||
|
mStaggeredViews.add(infoText);
|
||||||
|
|
||||||
|
refreshLists();
|
||||||
|
refreshData();
|
||||||
|
animateEntrance();
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout createListPanel(Context context, String title, boolean isFriendsList) {
|
||||||
|
LinearLayout panel = new LinearLayout(context);
|
||||||
|
panel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
panel.setPadding(dp(16), dp(16), dp(16), dp(16));
|
||||||
|
|
||||||
|
ShapeDrawable panelBg = new ShapeDrawable();
|
||||||
|
panelBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
panelBg.setColor(0x33000000);
|
||||||
|
panelBg.setCornerRadius(dp(16));
|
||||||
|
panel.setBackground(panelBg);
|
||||||
|
|
||||||
|
TextView titleTv = new TextView(context);
|
||||||
|
titleTv.setText(title);
|
||||||
|
setFixedTextSize(titleTv, 48);
|
||||||
|
titleTv.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
titleTv.setTextColor(isFriendsList ? 0xFF55FF55 : 0xFFFFD700);
|
||||||
|
titleTv.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams titleLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
titleLp.setMargins(0, 0, 0, dp(12));
|
||||||
|
titleTv.setLayoutParams(titleLp);
|
||||||
|
panel.addView(titleTv);
|
||||||
|
|
||||||
|
ScrollView scrollView = new ScrollView(context);
|
||||||
|
LinearLayout.LayoutParams scrollLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
0, 1f
|
||||||
|
);
|
||||||
|
scrollView.setLayoutParams(scrollLp);
|
||||||
|
scrollView.setScrollBarSize(dp(6));
|
||||||
|
|
||||||
|
LinearLayout listContainer = new LinearLayout(context);
|
||||||
|
listContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
if (isFriendsList) {
|
||||||
|
mFriendsListContainer = listContainer;
|
||||||
|
} else {
|
||||||
|
mRequestsListContainer = listContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollView.addView(listContainer);
|
||||||
|
panel.addView(scrollView);
|
||||||
|
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNavButton(LinearLayout parent, String text, String iconPath, View.OnClickListener listener) {
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
FrameLayout btnWrapper = new FrameLayout(context);
|
||||||
|
LinearLayout.LayoutParams wrapperLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
56
|
||||||
|
);
|
||||||
|
wrapperLp.setMargins(0, 0, 0, 8);
|
||||||
|
btnWrapper.setLayoutParams(wrapperLp);
|
||||||
|
|
||||||
|
ShapeDrawable btnBg = new ShapeDrawable();
|
||||||
|
btnBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
btnBg.setColor(0x1AFFFFFF);
|
||||||
|
btnBg.setCornerRadius(12);
|
||||||
|
btnWrapper.setBackground(btnBg);
|
||||||
|
|
||||||
|
LinearLayout content = new LinearLayout(context);
|
||||||
|
content.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
content.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
content.setPadding(20, 0, 20, 0);
|
||||||
|
|
||||||
|
ImageDrawable icon = new ImageDrawable("citrus", "startscreen/" + iconPath + ".png");
|
||||||
|
icon.setBounds(0, 0, 24, 24);
|
||||||
|
|
||||||
|
TextView tv = new TextView(context);
|
||||||
|
tv.setText(text);
|
||||||
|
setFixedTextSize(tv, 36);
|
||||||
|
tv.setTextColor(0xFFFFFFFF);
|
||||||
|
tv.setCompoundDrawables(icon, null, null, null);
|
||||||
|
tv.setCompoundDrawablePadding(16);
|
||||||
|
|
||||||
|
content.addView(tv);
|
||||||
|
btnWrapper.addView(content);
|
||||||
|
|
||||||
|
btnWrapper.setClickable(true);
|
||||||
|
btnWrapper.setFocusable(true);
|
||||||
|
btnWrapper.setOnClickListener(listener);
|
||||||
|
|
||||||
|
btnWrapper.setOnHoverListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
|
||||||
|
ObjectAnimator animator = ObjectAnimator.ofFloat(v, View.TRANSLATION_X, 0f, (float) dp(15));
|
||||||
|
animator.setDuration(250);
|
||||||
|
animator.setInterpolator(TimeInterpolator.DECELERATE);
|
||||||
|
animator.start();
|
||||||
|
btnBg.setColor(0x33FFFFFF);
|
||||||
|
return true;
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||||
|
ObjectAnimator animator = ObjectAnimator.ofFloat(v, View.TRANSLATION_X, (float) dp(15), 0f);
|
||||||
|
animator.setDuration(250);
|
||||||
|
animator.setInterpolator(TimeInterpolator.DECELERATE);
|
||||||
|
animator.start();
|
||||||
|
btnBg.setColor(0x1AFFFFFF);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
parent.addView(btnWrapper);
|
||||||
|
mStaggeredViews.add(btnWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateEntrance() {
|
||||||
|
long delay = 100;
|
||||||
|
for (View v : mStaggeredViews) {
|
||||||
|
v.setAlpha(0);
|
||||||
|
float startX = -dp(40);
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||||||
|
if (lp instanceof FrameLayout.LayoutParams flp) {
|
||||||
|
if ((flp.gravity & Gravity.RIGHT) == Gravity.RIGHT) {
|
||||||
|
startX = dp(40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.setTranslationX(startX);
|
||||||
|
|
||||||
|
ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f);
|
||||||
|
ObjectAnimator trans = ObjectAnimator.ofFloat(v, View.TRANSLATION_X, startX, 0f);
|
||||||
|
|
||||||
|
AnimatorSet set = new AnimatorSet();
|
||||||
|
set.playTogether(alpha, trans);
|
||||||
|
set.setDuration(800);
|
||||||
|
set.setStartDelay(delay);
|
||||||
|
set.setInterpolator(TimeInterpolator.DECELERATE);
|
||||||
|
set.start();
|
||||||
|
|
||||||
|
delay += 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendFriendRequest() {
|
||||||
|
String name = mAddFriendInput.getText().toString().trim();
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
CustomToast.sendCustomToast("Error", 0xFFFF5555, "Please enter a player name", 0xFFFFFFFF);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FriendManager.sendRequest(name).thenAccept(error -> {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (error == null) {
|
||||||
|
CustomToast.sendCustomToast("Success", 0xFF55FF55, "Friend request sent to " + name, 0xFFBBBBBB);
|
||||||
|
mAddFriendInput.setText("");
|
||||||
|
} else {
|
||||||
|
CustomToast.sendCustomToast("Error", 0xFFFF5555, error, 0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshData() {
|
||||||
|
FriendManager.refresh().thenAccept(v -> {
|
||||||
|
MuiModApi.postToUiThread(this::refreshLists);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshLists() {
|
||||||
|
if (mFriendsListContainer == null || mRequestsListContainer == null) return;
|
||||||
|
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
mFriendsListContainer.removeAllViews();
|
||||||
|
List<Friend> friends = FriendManager.getFriends();
|
||||||
|
|
||||||
|
if (friends.isEmpty()) {
|
||||||
|
mFriendsListContainer.addView(createEmptyState(context, "No friends yet", "Add someone using the input on the left!"));
|
||||||
|
} else {
|
||||||
|
for (Friend friend : friends) {
|
||||||
|
mFriendsListContainer.addView(createFriendCard(context, friend));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mRequestsListContainer.removeAllViews();
|
||||||
|
List<FriendRequest> requests = FriendManager.getIncomingRequests();
|
||||||
|
|
||||||
|
if (requests.isEmpty()) {
|
||||||
|
mRequestsListContainer.addView(createEmptyState(context, "No requests", "Pending requests will appear here"));
|
||||||
|
} else {
|
||||||
|
for (FriendRequest request : requests) {
|
||||||
|
mRequestsListContainer.addView(createRequestCard(context, request));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createEmptyState(Context context, String title, String subtitle) {
|
||||||
|
LinearLayout container = new LinearLayout(context);
|
||||||
|
container.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
container.setGravity(Gravity.CENTER);
|
||||||
|
container.setPadding(dp(16), dp(40), dp(16), dp(40));
|
||||||
|
|
||||||
|
TextView titleTv = new TextView(context);
|
||||||
|
titleTv.setText(title);
|
||||||
|
setFixedTextSize(titleTv, 32);
|
||||||
|
titleTv.setTextColor(0x88FFFFFF);
|
||||||
|
titleTv.setGravity(Gravity.CENTER);
|
||||||
|
container.addView(titleTv);
|
||||||
|
|
||||||
|
TextView subtitleTv = new TextView(context);
|
||||||
|
subtitleTv.setText(subtitle);
|
||||||
|
setFixedTextSize(subtitleTv, 24);
|
||||||
|
subtitleTv.setTextColor(0x55FFFFFF);
|
||||||
|
subtitleTv.setGravity(Gravity.CENTER);
|
||||||
|
subtitleTv.setPadding(0, dp(4), 0, 0);
|
||||||
|
container.addView(subtitleTv);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createFriendCard(Context context, Friend friend) {
|
||||||
|
LinearLayout card = new LinearLayout(context);
|
||||||
|
card.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
card.setPadding(dp(12), dp(10), dp(12), dp(10));
|
||||||
|
LinearLayout.LayoutParams cardLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
cardLp.setMargins(0, 0, 0, dp(8));
|
||||||
|
card.setLayoutParams(cardLp);
|
||||||
|
|
||||||
|
ShapeDrawable cardBg = new ShapeDrawable();
|
||||||
|
cardBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
cardBg.setColor(0x20FFFFFF);
|
||||||
|
cardBg.setCornerRadius(dp(10));
|
||||||
|
card.setBackground(cardBg);
|
||||||
|
|
||||||
|
LinearLayout topRow = new LinearLayout(context);
|
||||||
|
topRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
topRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
View statusDot = new View(context);
|
||||||
|
LinearLayout.LayoutParams dotLp = new LinearLayout.LayoutParams(dp(8), dp(8));
|
||||||
|
dotLp.setMargins(0, 0, dp(8), 0);
|
||||||
|
statusDot.setLayoutParams(dotLp);
|
||||||
|
ShapeDrawable dotBg = new ShapeDrawable();
|
||||||
|
dotBg.setShape(ShapeDrawable.CIRCLE);
|
||||||
|
dotBg.setColor(friend.isOnline() ? 0xFF55FF55 : 0xFF666666);
|
||||||
|
statusDot.setBackground(dotBg);
|
||||||
|
topRow.addView(statusDot);
|
||||||
|
|
||||||
|
String name = friend.player_name != null ? friend.player_name : "Unknown";
|
||||||
|
TextView nameTv = new TextView(context);
|
||||||
|
nameTv.setText(name);
|
||||||
|
setFixedTextSize(nameTv, 30);
|
||||||
|
nameTv.setTextColor(0xFFFFFFFF);
|
||||||
|
LinearLayout.LayoutParams nameLp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
nameTv.setLayoutParams(nameLp);
|
||||||
|
topRow.addView(nameTv);
|
||||||
|
|
||||||
|
TextView statusTv = new TextView(context);
|
||||||
|
statusTv.setText(friend.isOnline() ? "Online" : "Offline");
|
||||||
|
setFixedTextSize(statusTv, 22);
|
||||||
|
statusTv.setTextColor(friend.isOnline() ? 0xFF55FF55 : 0x88FFFFFF);
|
||||||
|
topRow.addView(statusTv);
|
||||||
|
|
||||||
|
card.addView(topRow);
|
||||||
|
|
||||||
|
String serverDisplay = friend.getServerDisplay();
|
||||||
|
if (serverDisplay != null) {
|
||||||
|
LinearLayout serverRow = new LinearLayout(context);
|
||||||
|
serverRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
serverRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams serverRowLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
serverRowLp.setMargins(0, dp(6), 0, 0);
|
||||||
|
serverRow.setLayoutParams(serverRowLp);
|
||||||
|
|
||||||
|
View serverIcon = new View(context);
|
||||||
|
LinearLayout.LayoutParams iconLp = new LinearLayout.LayoutParams(dp(6), dp(20));
|
||||||
|
iconLp.setMargins(0, 0, dp(10), 0);
|
||||||
|
serverIcon.setLayoutParams(iconLp);
|
||||||
|
ShapeDrawable iconBg = new ShapeDrawable();
|
||||||
|
iconBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
iconBg.setColor(0xFF4a90d9);
|
||||||
|
iconBg.setCornerRadius(dp(3));
|
||||||
|
serverIcon.setBackground(iconBg);
|
||||||
|
serverRow.addView(serverIcon);
|
||||||
|
|
||||||
|
TextView serverTv = new TextView(context);
|
||||||
|
serverTv.setText("Playing on: " + serverDisplay);
|
||||||
|
setFixedTextSize(serverTv, 28);
|
||||||
|
serverTv.setTextColor(0xFF4a90d9);
|
||||||
|
LinearLayout.LayoutParams serverTvLp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
serverTv.setLayoutParams(serverTvLp);
|
||||||
|
serverRow.addView(serverTv);
|
||||||
|
|
||||||
|
TextView joinBtn = new TextView(context);
|
||||||
|
joinBtn.setText("Copy IP");
|
||||||
|
setFixedTextSize(joinBtn, 24);
|
||||||
|
joinBtn.setTextColor(0xFFFFFFFF);
|
||||||
|
joinBtn.setPadding(dp(12), dp(6), dp(12), dp(6));
|
||||||
|
ShapeDrawable joinBg = new ShapeDrawable();
|
||||||
|
joinBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
joinBg.setColor(0xFF4a90d9);
|
||||||
|
joinBg.setCornerRadius(dp(6));
|
||||||
|
joinBtn.setBackground(joinBg);
|
||||||
|
joinBtn.setClickable(true);
|
||||||
|
String finalServerDisplay = serverDisplay;
|
||||||
|
joinBtn.setOnClickListener(v -> {
|
||||||
|
MinecraftClient.getInstance().keyboard.setClipboard(finalServerDisplay);
|
||||||
|
CustomToast.sendCustomToast("Copied!", 0xFF4a90d9, "Server IP copied to clipboard", 0xFFBBBBBB);
|
||||||
|
});
|
||||||
|
joinBtn.setOnHoverListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
|
||||||
|
joinBg.setColor(0xFF6ab0f9);
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||||
|
joinBg.setColor(0xFF4a90d9);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
serverRow.addView(joinBtn);
|
||||||
|
|
||||||
|
card.addView(serverRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
LinearLayout bottomRow = new LinearLayout(context);
|
||||||
|
bottomRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
bottomRow.setGravity(Gravity.RIGHT);
|
||||||
|
LinearLayout.LayoutParams bottomRowLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
bottomRowLp.setMargins(0, dp(8), 0, 0);
|
||||||
|
bottomRow.setLayoutParams(bottomRowLp);
|
||||||
|
|
||||||
|
TextView removeBtn = createSmallActionButton(context, "Remove", 0xFFd9534f);
|
||||||
|
removeBtn.setOnClickListener(v -> {
|
||||||
|
FriendManager.removeFriend(friend.getUuid()).thenAccept(error -> {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (error == null) {
|
||||||
|
refreshLists();
|
||||||
|
CustomToast.sendCustomToast("Removed", 0xFFFF9955, name + " was removed from friends", 0xFFBBBBBB);
|
||||||
|
} else {
|
||||||
|
CustomToast.sendCustomToast("Error", 0xFFFF5555, error, 0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
bottomRow.addView(removeBtn);
|
||||||
|
|
||||||
|
card.addView(bottomRow);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createRequestCard(Context context, FriendRequest request) {
|
||||||
|
LinearLayout card = new LinearLayout(context);
|
||||||
|
card.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
card.setPadding(dp(12), dp(10), dp(12), dp(10));
|
||||||
|
LinearLayout.LayoutParams cardLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
cardLp.setMargins(0, 0, 0, dp(8));
|
||||||
|
card.setLayoutParams(cardLp);
|
||||||
|
|
||||||
|
ShapeDrawable cardBg = new ShapeDrawable();
|
||||||
|
cardBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
cardBg.setColor(0x20FFFFFF);
|
||||||
|
cardBg.setCornerRadius(dp(10));
|
||||||
|
card.setBackground(cardBg);
|
||||||
|
|
||||||
|
View accentBar = new View(context);
|
||||||
|
LinearLayout.LayoutParams accentLp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(3));
|
||||||
|
accentLp.setMargins(0, 0, 0, dp(8));
|
||||||
|
accentBar.setLayoutParams(accentLp);
|
||||||
|
ShapeDrawable accentBg = new ShapeDrawable();
|
||||||
|
accentBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
accentBg.setColor(0xFFFFD700);
|
||||||
|
accentBg.setCornerRadius(dp(2));
|
||||||
|
accentBar.setBackground(accentBg);
|
||||||
|
card.addView(accentBar);
|
||||||
|
|
||||||
|
String name = request.player_name != null ? request.player_name : "Unknown";
|
||||||
|
TextView nameTv = new TextView(context);
|
||||||
|
nameTv.setText(name);
|
||||||
|
setFixedTextSize(nameTv, 30);
|
||||||
|
nameTv.setTextColor(0xFFFFFFFF);
|
||||||
|
card.addView(nameTv);
|
||||||
|
|
||||||
|
TextView labelTv = new TextView(context);
|
||||||
|
labelTv.setText("wants to be your friend");
|
||||||
|
setFixedTextSize(labelTv, 22);
|
||||||
|
labelTv.setTextColor(0x88FFFFFF);
|
||||||
|
card.addView(labelTv);
|
||||||
|
|
||||||
|
LinearLayout actionsRow = new LinearLayout(context);
|
||||||
|
actionsRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
actionsRow.setGravity(Gravity.RIGHT);
|
||||||
|
LinearLayout.LayoutParams actionsLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
actionsLp.setMargins(0, dp(10), 0, 0);
|
||||||
|
actionsRow.setLayoutParams(actionsLp);
|
||||||
|
|
||||||
|
TextView declineBtn = createSmallActionButton(context, "Decline", 0xFFd9534f);
|
||||||
|
declineBtn.setOnClickListener(v -> {
|
||||||
|
FriendManager.removeFriend(request.getUuid()).thenAccept(error -> {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (error == null) {
|
||||||
|
refreshLists();
|
||||||
|
} else {
|
||||||
|
CustomToast.sendCustomToast("Error", 0xFFFF5555, error, 0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
LinearLayout.LayoutParams declineLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
declineLp.setMargins(0, 0, dp(8), 0);
|
||||||
|
declineBtn.setLayoutParams(declineLp);
|
||||||
|
actionsRow.addView(declineBtn);
|
||||||
|
|
||||||
|
TextView acceptBtn = createSmallActionButton(context, "Accept", 0xFF5cb85c);
|
||||||
|
acceptBtn.setOnClickListener(v -> {
|
||||||
|
FriendManager.acceptRequest(request.getUuid()).thenAccept(error -> {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (error == null) {
|
||||||
|
refreshLists();
|
||||||
|
CustomToast.sendCustomToast("Success", 0xFF55FF55, "You are now friends with " + name, 0xFFBBBBBB);
|
||||||
|
} else {
|
||||||
|
CustomToast.sendCustomToast("Error", 0xFFFF5555, error, 0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
actionsRow.addView(acceptBtn);
|
||||||
|
|
||||||
|
card.addView(actionsRow);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextView createSmallActionButton(Context context, String text, int color) {
|
||||||
|
TextView btn = new TextView(context);
|
||||||
|
btn.setText(text);
|
||||||
|
setFixedTextSize(btn, 24);
|
||||||
|
btn.setTextColor(0xFFFFFFFF);
|
||||||
|
btn.setPadding(dp(14), dp(6), dp(14), dp(6));
|
||||||
|
btn.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
ShapeDrawable bg = new ShapeDrawable();
|
||||||
|
bg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
bg.setColor(color);
|
||||||
|
bg.setCornerRadius(dp(6));
|
||||||
|
btn.setBackground(bg);
|
||||||
|
btn.setClickable(true);
|
||||||
|
|
||||||
|
btn.setOnHoverListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
|
||||||
|
bg.setColor(lightenColor(color, 0.2f));
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||||
|
bg.setColor(color);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int lightenColor(int color, float factor) {
|
||||||
|
int a = (color >> 24) & 0xFF;
|
||||||
|
int r = Math.min(255, (int) (((color >> 16) & 0xFF) * (1 + factor)));
|
||||||
|
int g = Math.min(255, (int) (((color >> 8) & 0xFF) * (1 + factor)));
|
||||||
|
int b = Math.min(255, (int) ((color & 0xFF) * (1 + factor)));
|
||||||
|
return (a << 24) | (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dp(int value) {
|
||||||
|
return (int) (value * getContext().getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPauseScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldBlurBackground() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasDefaultBackground() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,801 @@
|
|||||||
|
package de.winniepat.citrus.gui;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.draggui.HudConfig;
|
||||||
|
import icyllis.modernui.animation.*;
|
||||||
|
import icyllis.modernui.core.Context;
|
||||||
|
import icyllis.modernui.fragment.Fragment;
|
||||||
|
import icyllis.modernui.graphics.drawable.*;
|
||||||
|
import icyllis.modernui.mc.*;
|
||||||
|
import icyllis.modernui.util.DataSet;
|
||||||
|
import icyllis.modernui.view.*;
|
||||||
|
import icyllis.modernui.widget.*;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import org.jetbrains.annotations.*;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.CapeHandler;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class HudEditorFragment extends Fragment implements ScreenCallback {
|
||||||
|
|
||||||
|
private FrameLayout mModuleContainer;
|
||||||
|
private final Map<String, ModuleCard> mModuleCards = new LinkedHashMap<>();
|
||||||
|
private final List<View> mStaggeredViews = new ArrayList<>();
|
||||||
|
|
||||||
|
private ModuleCard mSelectedModule = null;
|
||||||
|
private float mDragOffsetX, mDragOffsetY;
|
||||||
|
|
||||||
|
private float mScaleFactor = 1.0f;
|
||||||
|
private float mUiScale = HudConfig.INSTANCE.uiScale;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable DataSet savedInstanceState) {
|
||||||
|
Context context = requireContext();
|
||||||
|
HudConfig.load();
|
||||||
|
|
||||||
|
FrameLayout root = new FrameLayout(context);
|
||||||
|
root.setLayoutParams(new ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
|
||||||
|
root.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||||
|
if (mScaleFactor == 1.0f && right > 0) {
|
||||||
|
MinecraftClient mc = MinecraftClient.getInstance();
|
||||||
|
int mcScaledWidth = mc.getWindow().getScaledWidth();
|
||||||
|
int muiWidth = right - left;
|
||||||
|
mScaleFactor = (float) muiWidth / (float) mcScaledWidth;
|
||||||
|
|
||||||
|
repositionAllCards();
|
||||||
|
updateAllCardSizes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
root.setBackground(new ColorDrawable(0x88000000));
|
||||||
|
|
||||||
|
mModuleContainer = new FrameLayout(context);
|
||||||
|
mModuleContainer.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
root.addView(mModuleContainer);
|
||||||
|
|
||||||
|
createModuleCard("FPS", HudConfig.INSTANCE.fpsX, HudConfig.INSTANCE.fpsY, HudConfig.INSTANCE.fpsEnabled, HudConfig.INSTANCE.fpsColor);
|
||||||
|
createModuleCard("Ping", HudConfig.INSTANCE.pingX, HudConfig.INSTANCE.pingY, HudConfig.INSTANCE.pingEnabled, HudConfig.INSTANCE.pingColor);
|
||||||
|
createModuleCard("Server", HudConfig.INSTANCE.serverX, HudConfig.INSTANCE.serverY, HudConfig.INSTANCE.serverEnabled, HudConfig.INSTANCE.serverColor);
|
||||||
|
createModuleCard("Keys", HudConfig.INSTANCE.keyX, HudConfig.INSTANCE.keyY, HudConfig.INSTANCE.keyEnabled, HudConfig.INSTANCE.keyColor);
|
||||||
|
createModuleCard("Armor", HudConfig.INSTANCE.armorX, HudConfig.INSTANCE.armorY, HudConfig.INSTANCE.armorEnabled, HudConfig.INSTANCE.armorColor);
|
||||||
|
createModuleCard("Potion", HudConfig.INSTANCE.potionX, HudConfig.INSTANCE.potionY, HudConfig.INSTANCE.potionEnabled, HudConfig.INSTANCE.potionColor);
|
||||||
|
|
||||||
|
applyScaleToModules(mUiScale);
|
||||||
|
|
||||||
|
LinearLayout mSidePanel = createSidePanel(context);
|
||||||
|
root.addView(mSidePanel);
|
||||||
|
|
||||||
|
TextView title = new TextView(context);
|
||||||
|
title.setText("HUD Editor");
|
||||||
|
title.setTextSize(24);
|
||||||
|
title.setTextColor(0xFFFFFFFF);
|
||||||
|
FrameLayout.LayoutParams titleLp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
titleLp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
|
||||||
|
titleLp.setMargins(0, dp(20), 0, 0);
|
||||||
|
title.setLayoutParams(titleLp);
|
||||||
|
root.addView(title);
|
||||||
|
mStaggeredViews.add(title);
|
||||||
|
|
||||||
|
TextView instructions = new TextView(context);
|
||||||
|
instructions.setText("Drag modules to reposition • Use panel on right to toggle & customize");
|
||||||
|
instructions.setTextSize(12);
|
||||||
|
instructions.setTextColor(0x88FFFFFF);
|
||||||
|
FrameLayout.LayoutParams instrLp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
instrLp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
|
||||||
|
instrLp.setMargins(0, 0, 0, dp(20));
|
||||||
|
instructions.setLayoutParams(instrLp);
|
||||||
|
root.addView(instructions);
|
||||||
|
mStaggeredViews.add(instructions);
|
||||||
|
|
||||||
|
animateEntrance();
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createModuleCard(String name, int x, int y, boolean enabled, int color) {
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
ModuleCard card = new ModuleCard(context, name, enabled, color);
|
||||||
|
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
lp.leftMargin = mcToMui(x);
|
||||||
|
lp.topMargin = mcToMui(y);
|
||||||
|
card.setLayoutParams(lp);
|
||||||
|
applyModuleCardSize(name, card);
|
||||||
|
|
||||||
|
card.setOnTouchListener((v, event) -> {
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN -> {
|
||||||
|
mSelectedModule = card;
|
||||||
|
mDragOffsetX = event.getX();
|
||||||
|
mDragOffsetY = event.getY();
|
||||||
|
card.setSelected(true);
|
||||||
|
animateSelect(card);
|
||||||
|
updateSidePanelForModule(card);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_MOVE -> {
|
||||||
|
if (mSelectedModule == card) {
|
||||||
|
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) card.getLayoutParams();
|
||||||
|
params.leftMargin = (int) (event.getRawX() - mDragOffsetX);
|
||||||
|
params.topMargin = (int) (event.getRawY() - mDragOffsetY);
|
||||||
|
card.setLayoutParams(params);
|
||||||
|
updateConfigPosition(name, params.leftMargin, params.topMargin);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||||
|
if (mSelectedModule == card) {
|
||||||
|
animateDeselect(card);
|
||||||
|
HudConfig.save();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
mModuleContainer.addView(card);
|
||||||
|
mModuleCards.put(name, card);
|
||||||
|
mStaggeredViews.add(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateConfigPosition(String name, int x, int y) {
|
||||||
|
int mcX = muiToMc(x);
|
||||||
|
int mcY = muiToMc(y);
|
||||||
|
switch (name) {
|
||||||
|
case "FPS" -> { HudConfig.INSTANCE.fpsX = mcX; HudConfig.INSTANCE.fpsY = mcY; }
|
||||||
|
case "Ping" -> { HudConfig.INSTANCE.pingX = mcX; HudConfig.INSTANCE.pingY = mcY; }
|
||||||
|
case "Server" -> { HudConfig.INSTANCE.serverX = mcX; HudConfig.INSTANCE.serverY = mcY; }
|
||||||
|
case "Keys" -> { HudConfig.INSTANCE.keyX = mcX; HudConfig.INSTANCE.keyY = mcY; }
|
||||||
|
case "Armor" -> { HudConfig.INSTANCE.armorX = mcX; HudConfig.INSTANCE.armorY = mcY; }
|
||||||
|
case "Potion" -> { HudConfig.INSTANCE.potionX = mcX; HudConfig.INSTANCE.potionY = mcY; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout createSidePanel(Context context) {
|
||||||
|
LinearLayout panel = new LinearLayout(context);
|
||||||
|
panel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
panel.setPadding(dp(16), dp(16), dp(16), dp(16));
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(dp(280), ViewGroup.LayoutParams.MATCH_PARENT);
|
||||||
|
lp.gravity = Gravity.RIGHT;
|
||||||
|
panel.setLayoutParams(lp);
|
||||||
|
|
||||||
|
ShapeDrawable bg = new ShapeDrawable();
|
||||||
|
bg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
bg.setColor(0xE6181818);
|
||||||
|
bg.setCornerRadii(dp(16), 0, 0, dp(16));
|
||||||
|
panel.setBackground(bg);
|
||||||
|
|
||||||
|
TextView panelTitle = new TextView(context);
|
||||||
|
panelTitle.setText("Module Settings");
|
||||||
|
panelTitle.setTextSize(18);
|
||||||
|
panelTitle.setTextColor(0xFFFFFFFF);
|
||||||
|
panelTitle.setPadding(0, 0, 0, dp(16));
|
||||||
|
panel.addView(panelTitle);
|
||||||
|
|
||||||
|
ScrollView scroll = new ScrollView(context);
|
||||||
|
scroll.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
|
||||||
|
LinearLayout content = new LinearLayout(context);
|
||||||
|
content.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
content.setId(View.generateViewId());
|
||||||
|
|
||||||
|
for (String moduleName : new String[]{"FPS", "Ping", "Server", "Keys", "Armor", "Potion"}) {
|
||||||
|
content.addView(createModuleSettingItem(context, moduleName));
|
||||||
|
}
|
||||||
|
|
||||||
|
View separator = new View(context);
|
||||||
|
separator.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(1)));
|
||||||
|
separator.setBackground(new ColorDrawable(0x33FFFFFF));
|
||||||
|
LinearLayout.LayoutParams sepLp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, dp(1));
|
||||||
|
sepLp.setMargins(0, dp(16), 0, dp(16));
|
||||||
|
separator.setLayoutParams(sepLp);
|
||||||
|
content.addView(separator);
|
||||||
|
|
||||||
|
content.addView(createSliderSetting(context, "UI Scale", 50, 150, (int) (HudConfig.INSTANCE.uiScale * 100), value -> {
|
||||||
|
float scale = value / 100f;
|
||||||
|
applyScaleToModules(scale);
|
||||||
|
HudConfig.save();
|
||||||
|
}));
|
||||||
|
|
||||||
|
Button resetBtn = new Button(context);
|
||||||
|
resetBtn.setText("Reset Positions");
|
||||||
|
LinearLayout.LayoutParams resetLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
dp(44)
|
||||||
|
);
|
||||||
|
|
||||||
|
LinearLayout list = new LinearLayout(context);
|
||||||
|
list.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
for (String cape : CapeHandler.getAvailableCapes()) {
|
||||||
|
|
||||||
|
Button btn = new Button(context);
|
||||||
|
btn.setText((cape));
|
||||||
|
|
||||||
|
btn.setOnClickListener(v -> {
|
||||||
|
CapeHandler.setLocalPlayerCape(cape, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams blp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
dp(44)
|
||||||
|
);
|
||||||
|
blp.setMargins(0, dp(6), 0, 0);
|
||||||
|
btn.setLayoutParams(blp);
|
||||||
|
|
||||||
|
list.addView(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLp.setMargins(0, dp(16), 0, 0);
|
||||||
|
resetBtn.setLayoutParams(resetLp);
|
||||||
|
resetBtn.setOnClickListener(v -> resetPositions());
|
||||||
|
content.addView(resetBtn);
|
||||||
|
|
||||||
|
scroll.addView(content);
|
||||||
|
panel.addView(scroll);
|
||||||
|
|
||||||
|
mStaggeredViews.add(panel);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout createModuleSettingItem(Context context, String moduleName) {
|
||||||
|
LinearLayout item = new LinearLayout(context);
|
||||||
|
item.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
item.setPadding(dp(12), dp(12), dp(12), dp(12));
|
||||||
|
LinearLayout.LayoutParams itemLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
itemLp.setMargins(0, 0, 0, dp(8));
|
||||||
|
item.setLayoutParams(itemLp);
|
||||||
|
|
||||||
|
ShapeDrawable itemBg = new ShapeDrawable();
|
||||||
|
itemBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
itemBg.setColor(0x1AFFFFFF);
|
||||||
|
itemBg.setCornerRadius(dp(8));
|
||||||
|
item.setBackground(itemBg);
|
||||||
|
|
||||||
|
LinearLayout header = new LinearLayout(context);
|
||||||
|
header.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
header.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
TextView nameTv = new TextView(context);
|
||||||
|
nameTv.setText(moduleName);
|
||||||
|
nameTv.setTextSize(14);
|
||||||
|
nameTv.setTextColor(0xFFFFFFFF);
|
||||||
|
nameTv.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
header.addView(nameTv);
|
||||||
|
|
||||||
|
CheckBox toggle = new CheckBox(context);
|
||||||
|
toggle.setChecked(HudConfig.INSTANCE.isModuleEnabled(moduleName));
|
||||||
|
toggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||||
|
setModuleEnabled(moduleName, isChecked);
|
||||||
|
ModuleCard card = mModuleCards.get(moduleName);
|
||||||
|
if (card != null) {
|
||||||
|
card.setModuleEnabled(isChecked);
|
||||||
|
animateVisibility(card, isChecked);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
header.addView(toggle);
|
||||||
|
|
||||||
|
item.addView(header);
|
||||||
|
|
||||||
|
TextView colorLabel = new TextView(context);
|
||||||
|
colorLabel.setText("Color");
|
||||||
|
colorLabel.setTextSize(11);
|
||||||
|
colorLabel.setTextColor(0x88FFFFFF);
|
||||||
|
colorLabel.setPadding(0, dp(8), 0, dp(4));
|
||||||
|
item.addView(colorLabel);
|
||||||
|
|
||||||
|
final int initialColor = getModuleColor(moduleName) | 0xFF000000;
|
||||||
|
|
||||||
|
LinearLayout colorRow = new LinearLayout(context);
|
||||||
|
colorRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
colorRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
SeekBar colorSlider = new SeekBar(context);
|
||||||
|
colorSlider.setMax(360);
|
||||||
|
colorSlider.setProgress(getHueFromColor(initialColor));
|
||||||
|
colorSlider.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
|
||||||
|
Button minus = new Button(context);
|
||||||
|
minus.setText("-");
|
||||||
|
minus.setOnClickListener(v -> {
|
||||||
|
int step = Math.max(0, colorSlider.getProgress() - 5);
|
||||||
|
colorSlider.setProgress(step);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
Button plus = new Button(context);
|
||||||
|
plus.setText("+");
|
||||||
|
plus.setOnClickListener(v -> {
|
||||||
|
int step = Math.min(360, colorSlider.getProgress() + 5);
|
||||||
|
colorSlider.setProgress(step);
|
||||||
|
});
|
||||||
|
|
||||||
|
View colorPreview = new View(context);
|
||||||
|
LinearLayout.LayoutParams previewLp = new LinearLayout.LayoutParams(dp(28), dp(20));
|
||||||
|
previewLp.setMargins(dp(8), 0, 0, 0);
|
||||||
|
colorPreview.setLayoutParams(previewLp);
|
||||||
|
colorPreview.setBackground(new ColorDrawable(initialColor));
|
||||||
|
|
||||||
|
colorRow.addView(minus);
|
||||||
|
colorRow.addView(colorSlider);
|
||||||
|
colorRow.addView(plus);
|
||||||
|
colorRow.addView(colorPreview);
|
||||||
|
item.addView(colorRow);
|
||||||
|
|
||||||
|
LinearLayout hexRow = new LinearLayout(context);
|
||||||
|
hexRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
hexRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
hexRow.setPadding(0, dp(6), 0, 0);
|
||||||
|
|
||||||
|
EditText hexInput = new EditText(context);
|
||||||
|
hexInput.setSingleLine(true);
|
||||||
|
hexInput.setText(formatColor(initialColor));
|
||||||
|
LinearLayout.LayoutParams hexLp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
hexLp.setMargins(0, 0, dp(8), 0);
|
||||||
|
hexInput.setLayoutParams(hexLp);
|
||||||
|
|
||||||
|
Button resetColor = new Button(context);
|
||||||
|
resetColor.setText("White");
|
||||||
|
resetColor.setOnClickListener(v -> {
|
||||||
|
int white = 0xFFFFFFFF;
|
||||||
|
setModuleColor(moduleName, white);
|
||||||
|
colorSlider.setProgress(getHueFromColor(white));
|
||||||
|
ModuleCard card = mModuleCards.get(moduleName);
|
||||||
|
if (card != null) {
|
||||||
|
card.updateColor(white);
|
||||||
|
}
|
||||||
|
colorPreview.setBackground(new ColorDrawable(white));
|
||||||
|
hexInput.setText(formatColor(white));
|
||||||
|
HudConfig.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
hexRow.addView(hexInput);
|
||||||
|
hexRow.addView(resetColor);
|
||||||
|
item.addView(hexRow);
|
||||||
|
|
||||||
|
colorSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
|
int color = java.awt.Color.HSBtoRGB(progress / 360f, 0.8f, 1f) | 0xFF000000;
|
||||||
|
setModuleColor(moduleName, color);
|
||||||
|
ModuleCard card = mModuleCards.get(moduleName);
|
||||||
|
if (card != null) {
|
||||||
|
card.updateColor(color);
|
||||||
|
}
|
||||||
|
colorPreview.setBackground(new ColorDrawable(color));
|
||||||
|
hexInput.setText(formatColor(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||||
|
HudConfig.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout createSliderSetting(Context context, String label, int min, int max, int defaultVal, SliderCallback callback) {
|
||||||
|
LinearLayout container = new LinearLayout(context);
|
||||||
|
container.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
container.setPadding(0, dp(8), 0, dp(8));
|
||||||
|
|
||||||
|
TextView labelTv = new TextView(context);
|
||||||
|
labelTv.setText(label);
|
||||||
|
labelTv.setTextSize(14);
|
||||||
|
labelTv.setTextColor(0xFFFFFFFF);
|
||||||
|
container.addView(labelTv);
|
||||||
|
|
||||||
|
LinearLayout sliderRow = new LinearLayout(context);
|
||||||
|
sliderRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
sliderRow.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
|
SeekBar slider = new SeekBar(context);
|
||||||
|
slider.setMax(max - min);
|
||||||
|
slider.setProgress(defaultVal - min);
|
||||||
|
slider.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
|
||||||
|
|
||||||
|
TextView valueTv = new TextView(context);
|
||||||
|
valueTv.setText(defaultVal + "%");
|
||||||
|
valueTv.setTextSize(12);
|
||||||
|
valueTv.setTextColor(0xAAFFFFFF);
|
||||||
|
valueTv.setMinWidth(dp(40));
|
||||||
|
valueTv.setGravity(Gravity.RIGHT);
|
||||||
|
|
||||||
|
slider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
|
int value = progress + min;
|
||||||
|
valueTv.setText(value + "%");
|
||||||
|
callback.onValueChanged(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||||
|
HudConfig.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sliderRow.addView(slider);
|
||||||
|
sliderRow.addView(valueTv);
|
||||||
|
container.addView(sliderRow);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetPositions() {
|
||||||
|
HudConfig.INSTANCE.fpsX = 10; HudConfig.INSTANCE.fpsY = 10;
|
||||||
|
HudConfig.INSTANCE.pingX = 10; HudConfig.INSTANCE.pingY = 25;
|
||||||
|
HudConfig.INSTANCE.serverX = 10; HudConfig.INSTANCE.serverY = 40;
|
||||||
|
HudConfig.INSTANCE.keyX = 10; HudConfig.INSTANCE.keyY = 60;
|
||||||
|
HudConfig.INSTANCE.armorX = 10; HudConfig.INSTANCE.armorY = 120;
|
||||||
|
HudConfig.INSTANCE.potionX = 10; HudConfig.INSTANCE.potionY = 180;
|
||||||
|
|
||||||
|
for (Map.Entry<String, ModuleCard> entry : mModuleCards.entrySet()) {
|
||||||
|
ModuleCard card = entry.getValue();
|
||||||
|
int targetX = 10, targetY = 10;
|
||||||
|
switch (entry.getKey()) {
|
||||||
|
case "Ping" -> targetY = 25;
|
||||||
|
case "Server" -> targetY = 40;
|
||||||
|
case "Keys" -> targetY = 60;
|
||||||
|
case "Armor" -> targetY = 120;
|
||||||
|
case "Potion" -> targetY = 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int finalTargetX = mcToMui(targetX);
|
||||||
|
final int finalTargetY = mcToMui(targetY);
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) card.getLayoutParams();
|
||||||
|
final int startX = lp.leftMargin;
|
||||||
|
final int startY = lp.topMargin;
|
||||||
|
|
||||||
|
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
|
||||||
|
animator.setDuration(300);
|
||||||
|
animator.setInterpolator(MotionEasingUtils.MOTION_EASING_EMPHASIZED);
|
||||||
|
animator.addUpdateListener(animation -> {
|
||||||
|
float fraction = (float) animation.getAnimatedValue();
|
||||||
|
FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) card.getLayoutParams();
|
||||||
|
params.leftMargin = (int) (startX + (finalTargetX - startX) * fraction);
|
||||||
|
params.topMargin = (int) (startY + (finalTargetY - startY) * fraction);
|
||||||
|
card.setLayoutParams(params);
|
||||||
|
});
|
||||||
|
animator.start();
|
||||||
|
}
|
||||||
|
HudConfig.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateSelect(View v) {
|
||||||
|
ObjectAnimator scaleX = ObjectAnimator.ofFloat(v, View.SCALE_X, 1f, 1.05f);
|
||||||
|
ObjectAnimator scaleY = ObjectAnimator.ofFloat(v, View.SCALE_Y, 1f, 1.05f);
|
||||||
|
AnimatorSet set = new AnimatorSet();
|
||||||
|
set.playTogether(scaleX, scaleY);
|
||||||
|
set.setDuration(150);
|
||||||
|
set.setInterpolator(MotionEasingUtils.MOTION_EASING_EMPHASIZED);
|
||||||
|
set.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateDeselect(View v) {
|
||||||
|
ObjectAnimator scaleX = ObjectAnimator.ofFloat(v, View.SCALE_X, 1.05f, 1f);
|
||||||
|
ObjectAnimator scaleY = ObjectAnimator.ofFloat(v, View.SCALE_Y, 1.05f, 1f);
|
||||||
|
AnimatorSet set = new AnimatorSet();
|
||||||
|
set.playTogether(scaleX, scaleY);
|
||||||
|
set.setDuration(150);
|
||||||
|
set.setInterpolator(MotionEasingUtils.MOTION_EASING_EMPHASIZED);
|
||||||
|
set.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateVisibility(View v, boolean visible) {
|
||||||
|
float targetAlpha = visible ? 1f : 0.3f;
|
||||||
|
ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, v.getAlpha(), targetAlpha);
|
||||||
|
alpha.setDuration(200);
|
||||||
|
alpha.setInterpolator(MotionEasingUtils.LINEAR_OUT_SLOW_IN);
|
||||||
|
alpha.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateEntrance() {
|
||||||
|
long delay = 0;
|
||||||
|
for (View v : mStaggeredViews) {
|
||||||
|
v.setAlpha(0);
|
||||||
|
v.setTranslationY(dp(30));
|
||||||
|
|
||||||
|
ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f);
|
||||||
|
ObjectAnimator trans = ObjectAnimator.ofFloat(v, View.TRANSLATION_Y, dp(30), 0f);
|
||||||
|
|
||||||
|
AnimatorSet set = new AnimatorSet();
|
||||||
|
set.playTogether(alpha, trans);
|
||||||
|
set.setDuration(400);
|
||||||
|
set.setStartDelay(delay);
|
||||||
|
set.setInterpolator(MotionEasingUtils.MOTION_EASING_EMPHASIZED_DECELERATE);
|
||||||
|
set.start();
|
||||||
|
|
||||||
|
delay += 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSidePanelForModule(ModuleCard card) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setModuleEnabled(String name, boolean enabled) {
|
||||||
|
switch (name) {
|
||||||
|
case "FPS" -> HudConfig.INSTANCE.fpsEnabled = enabled;
|
||||||
|
case "Ping" -> HudConfig.INSTANCE.pingEnabled = enabled;
|
||||||
|
case "Server" -> HudConfig.INSTANCE.serverEnabled = enabled;
|
||||||
|
case "Keys" -> HudConfig.INSTANCE.keyEnabled = enabled;
|
||||||
|
case "Armor" -> HudConfig.INSTANCE.armorEnabled = enabled;
|
||||||
|
case "Potion" -> HudConfig.INSTANCE.potionEnabled = enabled;
|
||||||
|
}
|
||||||
|
HudConfig.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setModuleColor(String name, int color) {
|
||||||
|
switch (name) {
|
||||||
|
case "FPS" -> HudConfig.INSTANCE.fpsColor = color;
|
||||||
|
case "Ping" -> HudConfig.INSTANCE.pingColor = color;
|
||||||
|
case "Server" -> HudConfig.INSTANCE.serverColor = color;
|
||||||
|
case "Keys" -> HudConfig.INSTANCE.keyColor = color;
|
||||||
|
case "Armor" -> HudConfig.INSTANCE.armorColor = color;
|
||||||
|
case "Potion" -> HudConfig.INSTANCE.potionColor = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getModuleColor(String name) {
|
||||||
|
return switch (name) {
|
||||||
|
case "FPS" -> HudConfig.INSTANCE.fpsColor;
|
||||||
|
case "Ping" -> HudConfig.INSTANCE.pingColor;
|
||||||
|
case "Server" -> HudConfig.INSTANCE.serverColor;
|
||||||
|
case "Keys" -> HudConfig.INSTANCE.keyColor;
|
||||||
|
case "Armor" -> HudConfig.INSTANCE.armorColor;
|
||||||
|
case "Potion" -> HudConfig.INSTANCE.potionColor;
|
||||||
|
default -> 0xFFFFFFFF;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getHueFromColor(int color) {
|
||||||
|
float[] hsb = java.awt.Color.RGBtoHSB((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF, null);
|
||||||
|
return (int) (hsb[0] * 360);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void repositionAllCards() {
|
||||||
|
for (Map.Entry<String, ModuleCard> entry : mModuleCards.entrySet()) {
|
||||||
|
ModuleCard card = entry.getValue();
|
||||||
|
int mcX = 0, mcY = 0;
|
||||||
|
switch (entry.getKey()) {
|
||||||
|
case "FPS" -> { mcX = HudConfig.INSTANCE.fpsX; mcY = HudConfig.INSTANCE.fpsY; }
|
||||||
|
case "Ping" -> { mcX = HudConfig.INSTANCE.pingX; mcY = HudConfig.INSTANCE.pingY; }
|
||||||
|
case "Server" -> { mcX = HudConfig.INSTANCE.serverX; mcY = HudConfig.INSTANCE.serverY; }
|
||||||
|
case "Keys" -> { mcX = HudConfig.INSTANCE.keyX; mcY = HudConfig.INSTANCE.keyY; }
|
||||||
|
case "Armor" -> { mcX = HudConfig.INSTANCE.armorX; mcY = HudConfig.INSTANCE.armorY; }
|
||||||
|
case "Potion" -> { mcX = HudConfig.INSTANCE.potionX; mcY = HudConfig.INSTANCE.potionY; }
|
||||||
|
}
|
||||||
|
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) card.getLayoutParams();
|
||||||
|
lp.leftMargin = mcToMui(mcX);
|
||||||
|
lp.topMargin = mcToMui(mcY);
|
||||||
|
card.setLayoutParams(lp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAllCardSizes() {
|
||||||
|
for (Map.Entry<String, ModuleCard> entry : mModuleCards.entrySet()) {
|
||||||
|
applyModuleCardSize(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyModuleCardSize(String name, ModuleCard card) {
|
||||||
|
MinecraftClient mc = MinecraftClient.getInstance();
|
||||||
|
int width = mcToMui(80);
|
||||||
|
int height = mcToMui(mc.textRenderer.fontHeight + 4);
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case "Keys" -> {
|
||||||
|
width = mcToMui(64);
|
||||||
|
height = mcToMui(86);
|
||||||
|
}
|
||||||
|
case "Armor" -> {
|
||||||
|
int armorCount = 0;
|
||||||
|
if (mc.player != null) {
|
||||||
|
for (var stack : mc.player.getArmorItems()) {
|
||||||
|
if (!stack.isEmpty()) armorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int rows = Math.max(1, armorCount);
|
||||||
|
width = mcToMui(50);
|
||||||
|
height = mcToMui(18 * rows);
|
||||||
|
}
|
||||||
|
case "Potion" -> {
|
||||||
|
int lines = Math.max(1, mc.player != null ? mc.player.getStatusEffects().size() : 1);
|
||||||
|
width = mcToMui(100);
|
||||||
|
height = mcToMui(12 * lines);
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
String text = card.getCurrentDisplayText();
|
||||||
|
width = mcToMui(mc.textRenderer.getWidth(text) + 6);
|
||||||
|
height = mcToMui(mc.textRenderer.fontHeight + 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) card.getLayoutParams();
|
||||||
|
if (lp != null) {
|
||||||
|
lp.width = Math.max(width, dp(40));
|
||||||
|
lp.height = Math.max(height, dp(20));
|
||||||
|
card.setLayoutParams(lp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int mcToMui(int mcCoord) {
|
||||||
|
return (int) (mcCoord * mScaleFactor * mUiScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int muiToMc(int muiCoord) {
|
||||||
|
return (int) (muiCoord / (mScaleFactor * mUiScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dp(int value) {
|
||||||
|
return (int) (value * getContext().getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
HudConfig.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPauseScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldBlurBackground() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasDefaultBackground() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface SliderCallback {
|
||||||
|
void onValueChanged(int value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyScaleToModules(float scale) {
|
||||||
|
mUiScale = scale;
|
||||||
|
HudConfig.INSTANCE.uiScale = scale;
|
||||||
|
repositionAllCards();
|
||||||
|
updateAllCardSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatColor(int color) {
|
||||||
|
return String.format(Locale.ROOT, "#%06X", color & 0xFFFFFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int parseColorInput(String input, int fallback) {
|
||||||
|
if (input == null) return fallback;
|
||||||
|
String cleaned = input.trim();
|
||||||
|
if (cleaned.startsWith("#")) cleaned = cleaned.substring(1);
|
||||||
|
if (cleaned.length() == 6) {
|
||||||
|
try {
|
||||||
|
return (int) (0xFF000000L | Long.parseLong(cleaned, 16));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ModuleCard extends FrameLayout {
|
||||||
|
private final String mName;
|
||||||
|
private final TextView mLabel;
|
||||||
|
private final ShapeDrawable mBackground;
|
||||||
|
private int mColor;
|
||||||
|
private boolean mModuleEnabled;
|
||||||
|
|
||||||
|
public ModuleCard(Context context, String name, boolean enabled, int color) {
|
||||||
|
super(context);
|
||||||
|
mName = name;
|
||||||
|
mColor = color;
|
||||||
|
mModuleEnabled = enabled;
|
||||||
|
|
||||||
|
setPadding(dp(12), dp(8), dp(12), dp(8));
|
||||||
|
setMinimumWidth(dp(80));
|
||||||
|
|
||||||
|
mBackground = new ShapeDrawable();
|
||||||
|
mBackground.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
mBackground.setCornerRadius(dp(8));
|
||||||
|
updateBackgroundColor(false);
|
||||||
|
setBackground(mBackground);
|
||||||
|
|
||||||
|
mLabel = new TextView(context);
|
||||||
|
mLabel.setText(getDisplayText());
|
||||||
|
mLabel.setTextSize(14);
|
||||||
|
mLabel.setTextColor(color);
|
||||||
|
mLabel.setGravity(Gravity.CENTER);
|
||||||
|
addView(mLabel);
|
||||||
|
|
||||||
|
setAlpha(enabled ? 1f : 0.3f);
|
||||||
|
|
||||||
|
setOnHoverListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
|
||||||
|
updateBackgroundColor(true);
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||||
|
if (!isSelected()) {
|
||||||
|
updateBackgroundColor(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrentDisplayText() {
|
||||||
|
return getDisplayText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDisplayText() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
return switch (mName) {
|
||||||
|
case "FPS" -> client.getCurrentFps() + " FPS";
|
||||||
|
case "Ping" -> {
|
||||||
|
int ping = 0;
|
||||||
|
if (client.getNetworkHandler() != null && client.player != null) {
|
||||||
|
var entry = client.getNetworkHandler().getPlayerListEntry(client.player.getUuid());
|
||||||
|
if (entry != null) ping = entry.getLatency();
|
||||||
|
}
|
||||||
|
yield ping + "ms";
|
||||||
|
}
|
||||||
|
case "Server" -> client.getCurrentServerEntry() != null ?
|
||||||
|
client.getCurrentServerEntry().address : "Singleplayer";
|
||||||
|
case "Keys" -> "[WASD]";
|
||||||
|
case "Armor" -> "[Armor]";
|
||||||
|
case "Potion" -> "[Potions]";
|
||||||
|
default -> mName;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateColor(int color) {
|
||||||
|
mColor = color;
|
||||||
|
mLabel.setTextColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModuleEnabled(boolean enabled) {
|
||||||
|
mModuleEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBackgroundColor(boolean hovered) {
|
||||||
|
if (hovered || isSelected()) {
|
||||||
|
mBackground.setColor(0x44FFFFFF);
|
||||||
|
} else {
|
||||||
|
mBackground.setColor(0x22FFFFFF);
|
||||||
|
}
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSelected(boolean selected) {
|
||||||
|
super.setSelected(selected);
|
||||||
|
updateBackgroundColor(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,759 @@
|
|||||||
|
package de.winniepat.citrus.gui;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.managers.WallpaperManager;
|
||||||
|
import de.winniepat.citrus.utils.texture.CdnTextureDrawable;
|
||||||
|
import de.winniepat.citrus.managers.ConfigManager;
|
||||||
|
import de.winniepat.citrus.managers.ClientRegistrationManager;
|
||||||
|
import de.winniepat.citrus.managers.RankManager;
|
||||||
|
import icyllis.modernui.animation.*;
|
||||||
|
import icyllis.modernui.core.Context;
|
||||||
|
import icyllis.modernui.fragment.Fragment;
|
||||||
|
import icyllis.modernui.graphics.drawable.*;
|
||||||
|
import icyllis.modernui.markflow.Markflow;
|
||||||
|
import icyllis.modernui.mc.*;
|
||||||
|
import icyllis.modernui.text.TextPaint;
|
||||||
|
import icyllis.modernui.util.DataSet;
|
||||||
|
import icyllis.modernui.view.*;
|
||||||
|
import icyllis.modernui.widget.*;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
|
||||||
|
import net.minecraft.client.gui.screen.option.OptionsScreen;
|
||||||
|
import net.minecraft.client.gui.screen.world.SelectWorldScreen;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
import org.jetbrains.annotations.*;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class MainMenuScreen extends Fragment implements ScreenCallback {
|
||||||
|
|
||||||
|
private FrameLayout mBackgroundView;
|
||||||
|
private View mBackgroundView1;
|
||||||
|
private View mBackgroundView2;
|
||||||
|
private CdnTextureDrawable mBackground1Drawable;
|
||||||
|
private CdnTextureDrawable mBackground2Drawable;
|
||||||
|
private boolean mIsUsingView1 = true;
|
||||||
|
private TextView mOnlineCountTv;
|
||||||
|
private View mRankIconView;
|
||||||
|
private LinearLayout mNewsContainer;
|
||||||
|
private LinearLayout mWpOptionsContainer;
|
||||||
|
private TextView mCurrentWpTv;
|
||||||
|
private final List<View> mStaggeredViews = new ArrayList<>();
|
||||||
|
private final String[] mSplashes = {
|
||||||
|
"100% freshly squeezed!",
|
||||||
|
"Optimized to the core.",
|
||||||
|
"Latency? Never heard of it.",
|
||||||
|
"Taste the difference!",
|
||||||
|
"Still fresh.",
|
||||||
|
"Fresh since 1.0!",
|
||||||
|
"Powered by WinniePat."
|
||||||
|
};
|
||||||
|
|
||||||
|
private float getGuiScale() {
|
||||||
|
return (float) MinecraftClient.getInstance().getWindow().getScaleFactor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFixedTextSize(TextView tv, float px) {
|
||||||
|
float scale = getGuiScale();
|
||||||
|
tv.setTextSize(px / scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable DataSet savedInstanceState) {
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
FrameLayout root = new FrameLayout(context);
|
||||||
|
root.setLayoutParams(new ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
|
||||||
|
mBackgroundView = new FrameLayout(context);
|
||||||
|
mBackgroundView.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
mBackgroundView.setScaleX(1.1f);
|
||||||
|
mBackgroundView.setScaleY(1.1f);
|
||||||
|
|
||||||
|
mBackgroundView1 = new View(context);
|
||||||
|
mBackgroundView1.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
mBackgroundView.addView(mBackgroundView1);
|
||||||
|
|
||||||
|
mBackgroundView2 = new View(context);
|
||||||
|
mBackgroundView2.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
mBackgroundView2.setAlpha(0f);
|
||||||
|
mBackgroundView.addView(mBackgroundView2);
|
||||||
|
|
||||||
|
root.addView(mBackgroundView);
|
||||||
|
|
||||||
|
WallpaperManager.init();
|
||||||
|
|
||||||
|
mBackground1Drawable = new CdnTextureDrawable();
|
||||||
|
mBackground2Drawable = new CdnTextureDrawable();
|
||||||
|
mBackgroundView1.setBackground(mBackground1Drawable);
|
||||||
|
mBackgroundView2.setBackground(mBackground2Drawable);
|
||||||
|
|
||||||
|
String currentWallpaper = ConfigManager.config.mainmenuWallpaper;
|
||||||
|
if (currentWallpaper == null || currentWallpaper.isEmpty()) {
|
||||||
|
currentWallpaper = "default.png";
|
||||||
|
ConfigManager.config.mainmenuWallpaper = currentWallpaper;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadInitialWallpaper(currentWallpaper);
|
||||||
|
|
||||||
|
ObjectAnimator bgPulseX = ObjectAnimator.ofFloat(mBackgroundView, View.SCALE_X, 1.1f, 1.15f);
|
||||||
|
ObjectAnimator bgPulseY = ObjectAnimator.ofFloat(mBackgroundView, View.SCALE_Y, 1.1f, 1.15f);
|
||||||
|
bgPulseX.setDuration(15000);
|
||||||
|
bgPulseY.setDuration(15000);
|
||||||
|
bgPulseX.setRepeatCount(ValueAnimator.INFINITE);
|
||||||
|
bgPulseY.setRepeatCount(ValueAnimator.INFINITE);
|
||||||
|
bgPulseX.setRepeatMode(ValueAnimator.REVERSE);
|
||||||
|
bgPulseY.setRepeatMode(ValueAnimator.REVERSE);
|
||||||
|
bgPulseX.start();
|
||||||
|
bgPulseY.start();
|
||||||
|
|
||||||
|
startParallaxLoop();
|
||||||
|
|
||||||
|
FrameLayout contentWrapper = new FrameLayout(context);
|
||||||
|
contentWrapper.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
root.addView(contentWrapper);
|
||||||
|
|
||||||
|
View logoView = new View(context);
|
||||||
|
FrameLayout.LayoutParams logoLp = new FrameLayout.LayoutParams(150, 150);
|
||||||
|
logoLp.gravity = Gravity.TOP | Gravity.CENTER_HORIZONTAL;
|
||||||
|
logoLp.setMargins(0, dp(30), 0, 0);
|
||||||
|
logoView.setLayoutParams(logoLp);
|
||||||
|
ImageDrawable logoDrawable = new ImageDrawable("citrus", "startscreen/logo.png");
|
||||||
|
logoDrawable.setGravity(Gravity.FILL);
|
||||||
|
logoView.setBackground(logoDrawable);
|
||||||
|
contentWrapper.addView(logoView);
|
||||||
|
mStaggeredViews.add(logoView);
|
||||||
|
|
||||||
|
LinearLayout titleLayout = new LinearLayout(context);
|
||||||
|
titleLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
FrameLayout.LayoutParams titleLp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
titleLp.setMargins(60,50, 0, 0);
|
||||||
|
titleLayout.setLayoutParams(titleLp);
|
||||||
|
|
||||||
|
TextView title = new TextView(context);
|
||||||
|
title.setText("CITRUS");
|
||||||
|
setFixedTextSize(title,186);
|
||||||
|
title.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
title.setTextColor(0xFFFFFFFF);
|
||||||
|
titleLayout.addView(title);
|
||||||
|
|
||||||
|
TextView subtitle = new TextView(context);
|
||||||
|
subtitle.setText("made by WinniePatGG, FullRiskk and AAirCrafter");
|
||||||
|
setFixedTextSize(subtitle,36);
|
||||||
|
subtitle.setTextColor(0xCCFFFFFF);
|
||||||
|
titleLayout.addView(subtitle);
|
||||||
|
|
||||||
|
TextView splash = new TextView(context);
|
||||||
|
splash.setText(mSplashes[(int) (Math.random() * mSplashes.length)]);
|
||||||
|
setFixedTextSize(splash,30);
|
||||||
|
splash.setTextColor(0xFFFFFF00);
|
||||||
|
splash.getPaint().setTextStyle(TextPaint.ITALIC);
|
||||||
|
titleLayout.addView(splash);
|
||||||
|
|
||||||
|
contentWrapper.addView(titleLayout);
|
||||||
|
mStaggeredViews.add(titleLayout);
|
||||||
|
|
||||||
|
LinearLayout navLayout = new LinearLayout(context);
|
||||||
|
navLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
FrameLayout.LayoutParams navLp = new FrameLayout.LayoutParams(
|
||||||
|
280,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
navLp.gravity = Gravity.CENTER_VERTICAL;
|
||||||
|
navLp.setMargins(60, 0, 0, 0);
|
||||||
|
navLayout.setLayoutParams(navLp);
|
||||||
|
|
||||||
|
addNavButton(navLayout, "Singleplayer", "singleplayer_icon", v -> {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(new SelectWorldScreen(MinecraftClient.getInstance().currentScreen));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
addNavButton(navLayout, "Multiplayer", "multiplayer_icon", v -> {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(new MultiplayerScreen(MinecraftClient.getInstance().currentScreen));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
addNavButton(navLayout, "Friends", "multiplayer_icon", v -> {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(MuiModApi.get().createScreen(new FriendsFragment(), null, MinecraftClient.getInstance().currentScreen));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
addNavButton(navLayout, "Profiles", "multiplayer_icon", v -> {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(MuiModApi.get().createScreen(new ProfileScreen(), null, MinecraftClient.getInstance().currentScreen));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
addNavButton(navLayout, "Settings", "settings_icon", v -> {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(new OptionsScreen(MinecraftClient.getInstance().currentScreen, MinecraftClient.getInstance().options));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
addNavButton(navLayout, "test", "settings_icon", v -> {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(MuiModApi.get().createScreen(new TestScreen(), null, MinecraftClient.getInstance().currentScreen));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
addNavButton(navLayout, "Discord", "discord_icon", v -> {
|
||||||
|
Util.getOperatingSystem().open("https://dc.citrus-client.de");
|
||||||
|
});
|
||||||
|
addNavButton(navLayout, "Quit", "door_icon", v -> {
|
||||||
|
MinecraftClient.getInstance().scheduleStop();
|
||||||
|
});
|
||||||
|
|
||||||
|
contentWrapper.addView(navLayout);
|
||||||
|
|
||||||
|
mNewsContainer = new LinearLayout(context);
|
||||||
|
mNewsContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
mNewsContainer.setPadding(20, 20, 20, 20);
|
||||||
|
|
||||||
|
LinearLayout newsSection = new LinearLayout(context);
|
||||||
|
newsSection.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
FrameLayout.LayoutParams newsSectionLp = new FrameLayout.LayoutParams(
|
||||||
|
360,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
newsSectionLp.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT;
|
||||||
|
newsSectionLp.setMargins(0, 0, 50, 0);
|
||||||
|
newsSection.setLayoutParams(newsSectionLp);
|
||||||
|
|
||||||
|
TextView newsLabel = new TextView(context);
|
||||||
|
newsLabel.setText("NEWS");
|
||||||
|
newsLabel.setTextSize(36);
|
||||||
|
setFixedTextSize(newsLabel,72);
|
||||||
|
newsLabel.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
newsLabel.setTextColor(0xFFFFFFFF);
|
||||||
|
newsLabel.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams newsLabelLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
newsLabelLp.setMargins(0, 0, 0, 12);
|
||||||
|
newsLabel.setLayoutParams(newsLabelLp);
|
||||||
|
newsSection.addView(newsLabel);
|
||||||
|
|
||||||
|
ScrollView newsScroll = new ScrollView(context);
|
||||||
|
LinearLayout.LayoutParams newsScrollLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
400
|
||||||
|
);
|
||||||
|
newsScroll.setScrollBarSize(10);
|
||||||
|
newsScroll.setLayoutParams(newsScrollLp);
|
||||||
|
|
||||||
|
ShapeDrawable newsBg = new ShapeDrawable();
|
||||||
|
newsBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
newsBg.setColor(0x33000000);
|
||||||
|
newsBg.setCornerRadius(16);
|
||||||
|
newsScroll.setBackground(newsBg);
|
||||||
|
|
||||||
|
newsScroll.addView(mNewsContainer);
|
||||||
|
newsSection.addView(newsScroll);
|
||||||
|
|
||||||
|
contentWrapper.addView(newsSection);
|
||||||
|
mStaggeredViews.add(newsSection);
|
||||||
|
|
||||||
|
mOnlineCountTv = new TextView(context);
|
||||||
|
mOnlineCountTv.setText("Online Users: -");
|
||||||
|
setFixedTextSize(mOnlineCountTv,30);
|
||||||
|
mOnlineCountTv.setTextColor(0x88FFFFFF);
|
||||||
|
FrameLayout.LayoutParams onlineLp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
onlineLp.gravity = Gravity.BOTTOM | Gravity.RIGHT;
|
||||||
|
onlineLp.setMargins(0, 0, 60, 20);
|
||||||
|
mOnlineCountTv.setLayoutParams(onlineLp);
|
||||||
|
contentWrapper.addView(mOnlineCountTv);
|
||||||
|
mStaggeredViews.add(mOnlineCountTv);
|
||||||
|
|
||||||
|
LinearLayout profileLayout = new LinearLayout(context);
|
||||||
|
profileLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
profileLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
FrameLayout.LayoutParams profileLp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
profileLp.gravity = Gravity.TOP | Gravity.RIGHT;
|
||||||
|
profileLp.setMargins(0, 40, 60, 0);
|
||||||
|
profileLayout.setLayoutParams(profileLp);
|
||||||
|
|
||||||
|
mRankIconView = new View(context);
|
||||||
|
LinearLayout.LayoutParams rankIconLp = new LinearLayout.LayoutParams(20, 20);
|
||||||
|
rankIconLp.setMargins(0, 0, 4, 0);
|
||||||
|
mRankIconView.setLayoutParams(rankIconLp);
|
||||||
|
UUID currentUuid = MinecraftClient.getInstance().getSession().getUuidOrNull();
|
||||||
|
if (currentUuid != null) {
|
||||||
|
String rank = RankManager.getRank(currentUuid);
|
||||||
|
if (rank.equals("none") || rank.equals("loading")) {
|
||||||
|
mRankIconView.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
mRankIconView.setVisibility(View.VISIBLE);
|
||||||
|
ImageDrawable rankIcon = new ImageDrawable("citrus", "ranks/" + rank + ".png");
|
||||||
|
rankIcon.setGravity(Gravity.FILL);
|
||||||
|
mRankIconView.setBackground(rankIcon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profileLayout.addView(mRankIconView);
|
||||||
|
|
||||||
|
TextView playerName = new TextView(context);
|
||||||
|
playerName.setText(MinecraftClient.getInstance().getSession().getUsername());
|
||||||
|
playerName.setTextSize(16);
|
||||||
|
setFixedTextSize(playerName,42);
|
||||||
|
playerName.setTextColor(0xFFFFFFFF);
|
||||||
|
playerName.setPadding(0, 0, 12, 0);
|
||||||
|
profileLayout.addView(playerName);
|
||||||
|
|
||||||
|
contentWrapper.addView(profileLayout);
|
||||||
|
mStaggeredViews.add(profileLayout);
|
||||||
|
|
||||||
|
LinearLayout wallpaperLayout = new LinearLayout(context);
|
||||||
|
wallpaperLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
FrameLayout.LayoutParams wallpaperLp = new FrameLayout.LayoutParams(
|
||||||
|
160,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
wallpaperLp.gravity = Gravity.BOTTOM | Gravity.LEFT;
|
||||||
|
wallpaperLp.setMargins(60, 0, 0, 40);
|
||||||
|
wallpaperLayout.setLayoutParams(wallpaperLp);
|
||||||
|
|
||||||
|
TextView wallpaperLabel = new TextView(context);
|
||||||
|
wallpaperLabel.setText("Select Wallpaper");
|
||||||
|
setFixedTextSize(wallpaperLabel,28);
|
||||||
|
wallpaperLabel.setTextColor(0x88FFFFFF);
|
||||||
|
wallpaperLabel.setPadding(0, 0, 0, 4);
|
||||||
|
wallpaperLayout.addView(wallpaperLabel);
|
||||||
|
|
||||||
|
mCurrentWpTv = new TextView(context);
|
||||||
|
String displayWallpaper = ConfigManager.config.mainmenuWallpaper;
|
||||||
|
if (displayWallpaper == null || displayWallpaper.isEmpty()) {
|
||||||
|
displayWallpaper = "default.png";
|
||||||
|
}
|
||||||
|
mCurrentWpTv.setText(displayWallpaper);
|
||||||
|
setFixedTextSize(mCurrentWpTv,36);
|
||||||
|
mCurrentWpTv.setTextColor(0xFFFFFFFF);
|
||||||
|
mCurrentWpTv.setPadding(12, 8, 12, 9);
|
||||||
|
|
||||||
|
ShapeDrawable wpBg = new ShapeDrawable();
|
||||||
|
wpBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
wpBg.setColor(0x26FFFFFF);
|
||||||
|
wpBg.setCornerRadius(dp(8));
|
||||||
|
mCurrentWpTv.setBackground(wpBg);
|
||||||
|
mCurrentWpTv.setClickable(true);
|
||||||
|
|
||||||
|
wallpaperLayout.addView(mCurrentWpTv);
|
||||||
|
|
||||||
|
ScrollView wpScroll = new ScrollView(context);
|
||||||
|
LinearLayout.LayoutParams wpScrollLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
120
|
||||||
|
);
|
||||||
|
wpScrollLp.setMargins(0, 4, 0, 0);
|
||||||
|
wpScroll.setLayoutParams(wpScrollLp);
|
||||||
|
wpScroll.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
mWpOptionsContainer = new LinearLayout(context);
|
||||||
|
mWpOptionsContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
mWpOptionsContainer.setPadding(4, 4, 4, 4);
|
||||||
|
ShapeDrawable optionsBg = new ShapeDrawable();
|
||||||
|
optionsBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
optionsBg.setColor(0xCC000000);
|
||||||
|
optionsBg.setCornerRadius(8);
|
||||||
|
mWpOptionsContainer.setBackground(optionsBg);
|
||||||
|
|
||||||
|
mCurrentWpTv.setOnClickListener(v -> {
|
||||||
|
wpScroll.setVisibility(wpScroll.getVisibility() == View.VISIBLE ? View.GONE : View.VISIBLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
populateWallpaperOptions(wpScroll);
|
||||||
|
|
||||||
|
wpScroll.addView(mWpOptionsContainer);
|
||||||
|
wallpaperLayout.addView(wpScroll);
|
||||||
|
contentWrapper.addView(wallpaperLayout);
|
||||||
|
mStaggeredViews.add(wallpaperLayout);
|
||||||
|
|
||||||
|
animateEntrance();
|
||||||
|
|
||||||
|
fetchNews();
|
||||||
|
updatePlayerInfo();
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePlayerInfo() {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
int count = ClientRegistrationManager.getOnlineCitrusUsers().size();
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (mOnlineCountTv != null) {
|
||||||
|
mOnlineCountTv.setText("Online Users: " + count);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
UUID uuid = MinecraftClient.getInstance().getSession().getUuidOrNull();
|
||||||
|
if (uuid != null) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
String rank = RankManager.getRank(uuid);
|
||||||
|
int attempts = 0;
|
||||||
|
while (rank.equals("loading") && attempts < 10) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(500);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
rank = RankManager.getRank(uuid);
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
String finalRank = rank;
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (mRankIconView != null) {
|
||||||
|
if (finalRank.equals("none") || finalRank.equals("loading")) {
|
||||||
|
mRankIconView.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
mRankIconView.setVisibility(View.VISIBLE);
|
||||||
|
ImageDrawable rankIcon = new ImageDrawable("citrus", "ranks/" + finalRank + ".png");
|
||||||
|
rankIcon.setGravity(Gravity.FILL);
|
||||||
|
mRankIconView.setBackground(rankIcon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNavButton(LinearLayout parent, String text, String iconPath, View.OnClickListener listener) {
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
FrameLayout btnWrapper = new FrameLayout(context);
|
||||||
|
LinearLayout.LayoutParams wrapperLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
50
|
||||||
|
);
|
||||||
|
wrapperLp.setMargins(0, 6, 0, 6);
|
||||||
|
btnWrapper.setLayoutParams(wrapperLp);
|
||||||
|
|
||||||
|
ShapeDrawable btnBg = new ShapeDrawable();
|
||||||
|
btnBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
btnBg.setColor(0x1AFFFFFF);
|
||||||
|
btnBg.setCornerRadius(10);
|
||||||
|
btnWrapper.setBackground(btnBg);
|
||||||
|
|
||||||
|
LinearLayout content = new LinearLayout(context);
|
||||||
|
content.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
content.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
content.setPadding(20, 0, 20, 0);
|
||||||
|
|
||||||
|
ImageDrawable icon = new ImageDrawable("citrus", "startscreen/" + iconPath + ".png");
|
||||||
|
icon.setBounds(0, 0, 24, 24);
|
||||||
|
|
||||||
|
TextView tv = new TextView(context);
|
||||||
|
tv.setText(text);
|
||||||
|
setFixedTextSize(tv,36);
|
||||||
|
tv.setTextColor(0xFFFFFFFF);
|
||||||
|
tv.setCompoundDrawables(icon, null, null, null);
|
||||||
|
tv.setCompoundDrawablePadding(16);
|
||||||
|
|
||||||
|
content.addView(tv);
|
||||||
|
btnWrapper.addView(content);
|
||||||
|
|
||||||
|
btnWrapper.setClickable(true);
|
||||||
|
btnWrapper.setFocusable(true);
|
||||||
|
btnWrapper.setOnClickListener(listener);
|
||||||
|
|
||||||
|
btnWrapper.setOnHoverListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
|
||||||
|
ObjectAnimator animator = ObjectAnimator.ofFloat(v, View.TRANSLATION_X, 0f, (float) dp(15));
|
||||||
|
animator.setDuration(250);
|
||||||
|
animator.setInterpolator(MotionEasingUtils.LINEAR_OUT_SLOW_IN);
|
||||||
|
animator.start();
|
||||||
|
btnBg.setColor(0x33FFFFFF);
|
||||||
|
return true;
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||||
|
ObjectAnimator animator = ObjectAnimator.ofFloat(v, View.TRANSLATION_X, (float) dp(15), 0f);
|
||||||
|
animator.setDuration(250);
|
||||||
|
animator.setInterpolator(MotionEasingUtils.LINEAR_OUT_SLOW_IN);
|
||||||
|
animator.start();
|
||||||
|
btnBg.setColor(0x1AFFFFFF);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
parent.addView(btnWrapper);
|
||||||
|
mStaggeredViews.add(btnWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateEntrance() {
|
||||||
|
long delay = 200;
|
||||||
|
for (View v : mStaggeredViews) {
|
||||||
|
v.setAlpha(0);
|
||||||
|
float startX = -dp(40);
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||||||
|
if (lp instanceof FrameLayout.LayoutParams flp) {
|
||||||
|
if ((flp.gravity & Gravity.RIGHT) == Gravity.RIGHT) {
|
||||||
|
startX = dp(40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.setTranslationX(startX);
|
||||||
|
|
||||||
|
ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f);
|
||||||
|
ObjectAnimator trans = ObjectAnimator.ofFloat(v, View.TRANSLATION_X, startX, 0f);
|
||||||
|
|
||||||
|
AnimatorSet set = new AnimatorSet();
|
||||||
|
set.playTogether(alpha, trans);
|
||||||
|
set.setDuration(1000);
|
||||||
|
set.setStartDelay(delay);
|
||||||
|
set.setInterpolator(MotionEasingUtils.MOTION_EASING_EMPHASIZED_DECELERATE);
|
||||||
|
set.start();
|
||||||
|
|
||||||
|
delay += 80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchNews() {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(Client.API_URL + "/news"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
JsonObject json = new Gson().fromJson(response.body(), JsonObject.class);
|
||||||
|
|
||||||
|
if (json.get("success").getAsBoolean()) {
|
||||||
|
JsonArray array = json.getAsJsonArray("news");
|
||||||
|
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (mNewsContainer == null) return;
|
||||||
|
mNewsContainer.removeAllViews();
|
||||||
|
for (var element : array) {
|
||||||
|
JsonObject obj = element.getAsJsonObject();
|
||||||
|
mNewsContainer.addView(createNewsCard(
|
||||||
|
obj.get("title").getAsString(),
|
||||||
|
obj.get("content").getAsString(),
|
||||||
|
obj.get("date").getAsString()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error while fetching news", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createNewsCard(String title, String content, String date) {
|
||||||
|
LinearLayout card = new LinearLayout(requireContext());
|
||||||
|
card.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
card.setPadding(12, 12, 12, 12);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams cardLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
cardLp.setMargins(0, 0, 0, 15);
|
||||||
|
card.setLayoutParams(cardLp);
|
||||||
|
|
||||||
|
ShapeDrawable cardBg = new ShapeDrawable();
|
||||||
|
cardBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
cardBg.setColor(0x26FFFFFF);
|
||||||
|
cardBg.setCornerRadius(8);
|
||||||
|
card.setBackground(cardBg);
|
||||||
|
|
||||||
|
TextView titleTv = new TextView(requireContext());
|
||||||
|
titleTv.setText(title);
|
||||||
|
setFixedTextSize(titleTv,33);
|
||||||
|
titleTv.setTextColor(0xFFFFFFFF);
|
||||||
|
titleTv.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
card.addView(titleTv);
|
||||||
|
|
||||||
|
TextView dateTv = new TextView(requireContext());
|
||||||
|
dateTv.setText(date);
|
||||||
|
setFixedTextSize(dateTv,22);
|
||||||
|
dateTv.setTextColor(0x88FFFFFF);
|
||||||
|
dateTv.setPadding(0, 2, 0, 6);
|
||||||
|
card.addView(dateTv);
|
||||||
|
|
||||||
|
TextView contentTv = new TextView(requireContext());
|
||||||
|
Markflow.create(requireContext()).setMarkdown(contentTv, content);
|
||||||
|
setFixedTextSize(contentTv,30);
|
||||||
|
contentTv.setTextColor(0xCCFFFFFF);
|
||||||
|
card.addView(contentTv);
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dp(int value) {
|
||||||
|
return (int) (value * requireContext().getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPauseScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldBlurBackground() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasDefaultBackground() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startParallaxLoop() {
|
||||||
|
if (mBackgroundView == null) return;
|
||||||
|
mBackgroundView.postOnAnimation(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (mBackgroundView != null && mBackgroundView.isAttachedToWindow() && isAdded()) {
|
||||||
|
MinecraftClient mc = MinecraftClient.getInstance();
|
||||||
|
double mouseX = mc.mouse.getX();
|
||||||
|
double mouseY = mc.mouse.getY();
|
||||||
|
int width = mc.getWindow().getWidth();
|
||||||
|
int height = mc.getWindow().getHeight();
|
||||||
|
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
float maxMove = dp(20);
|
||||||
|
float moveX = (float) ((mouseX - width / 2.0) / (width / 2.0) * maxMove);
|
||||||
|
float moveY = (float) ((mouseY - height / 2.0) / (height / 2.0) * maxMove);
|
||||||
|
|
||||||
|
mBackgroundView.setTranslationX(-moveX);
|
||||||
|
mBackgroundView.setTranslationY(-moveY);
|
||||||
|
}
|
||||||
|
mBackgroundView.postOnAnimation(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadInitialWallpaper(String wallpaper) {
|
||||||
|
WallpaperManager.loadWallpaperWithData(wallpaper, imageData -> {
|
||||||
|
if (imageData != null) {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
mBackground1Drawable.setImageData(imageData);
|
||||||
|
mBackgroundView1.invalidate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateWallpaperOptions(ScrollView wpScroll) {
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
if (WallpaperManager.isWallpapersListLoaded()) {
|
||||||
|
addWallpaperOptionsToContainer(context, wpScroll);
|
||||||
|
} else {
|
||||||
|
TextView loadingTv = new TextView(context);
|
||||||
|
loadingTv.setText("Loading wallpapers...");
|
||||||
|
setFixedTextSize(loadingTv, 24);
|
||||||
|
loadingTv.setTextColor(0x88FFFFFF);
|
||||||
|
loadingTv.setPadding(dp(10), dp(6), dp(10), dp(6));
|
||||||
|
mWpOptionsContainer.addView(loadingTv);
|
||||||
|
|
||||||
|
WallpaperManager.onWallpapersLoaded(() -> {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
mWpOptionsContainer.removeAllViews();
|
||||||
|
addWallpaperOptionsToContainer(context, wpScroll);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addWallpaperOptionsToContainer(Context context, ScrollView wpScroll) {
|
||||||
|
List<String> wallpapers = WallpaperManager.getAvailableWallpapers();
|
||||||
|
|
||||||
|
for (String wp : wallpapers) {
|
||||||
|
TextView option = new TextView(context);
|
||||||
|
option.setText(wp);
|
||||||
|
setFixedTextSize(option, 24);
|
||||||
|
option.setTextColor(0xFFFFFFFF);
|
||||||
|
option.setPadding(dp(10), dp(6), dp(10), dp(6));
|
||||||
|
option.setClickable(true);
|
||||||
|
option.setOnClickListener(v_opt -> {
|
||||||
|
ConfigManager.config.mainmenuWallpaper = wp;
|
||||||
|
ConfigManager.save();
|
||||||
|
mCurrentWpTv.setText(wp);
|
||||||
|
updateBackground(wp);
|
||||||
|
wpScroll.setVisibility(View.GONE);
|
||||||
|
});
|
||||||
|
option.setOnHoverListener((v_opt, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
|
||||||
|
option.setBackground(new ColorDrawable(0x33FFFFFF));
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||||
|
option.setBackground(null);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
mWpOptionsContainer.addView(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBackground(String wallpaper) {
|
||||||
|
View activeView = mIsUsingView1 ? mBackgroundView1 : mBackgroundView2;
|
||||||
|
View nextView = mIsUsingView1 ? mBackgroundView2 : mBackgroundView1;
|
||||||
|
CdnTextureDrawable nextDrawable = mIsUsingView1 ? mBackground2Drawable : mBackground1Drawable;
|
||||||
|
|
||||||
|
WallpaperManager.loadWallpaperWithData(wallpaper, imageData -> {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (imageData != null) {
|
||||||
|
nextDrawable.setImageData(imageData);
|
||||||
|
nextView.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectAnimator fadeIn = ObjectAnimator.ofFloat(nextView, View.ALPHA, 0f, 1f);
|
||||||
|
ObjectAnimator fadeOut = ObjectAnimator.ofFloat(activeView, View.ALPHA, 1f, 0f);
|
||||||
|
fadeIn.setDuration(800);
|
||||||
|
fadeOut.setDuration(800);
|
||||||
|
fadeIn.start();
|
||||||
|
fadeOut.start();
|
||||||
|
|
||||||
|
mIsUsingView1 = !mIsUsingView1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleWallpaperDropdown(ScrollView wpScroll, boolean show) {
|
||||||
|
if (show) {
|
||||||
|
wpScroll.setVisibility(View.VISIBLE);
|
||||||
|
wpScroll.setAlpha(0f);
|
||||||
|
wpScroll.setTranslationY(-dp(10));
|
||||||
|
ObjectAnimator.ofFloat(wpScroll, View.ALPHA, 0f, 1f).setDuration(250).start();
|
||||||
|
ObjectAnimator.ofFloat(wpScroll, View.TRANSLATION_Y, -dp(10), 0f).setDuration(250).start();
|
||||||
|
} else {
|
||||||
|
ObjectAnimator.ofFloat(wpScroll, View.ALPHA, 1f, 0f).setDuration(250).start();
|
||||||
|
ObjectAnimator.ofFloat(wpScroll, View.TRANSLATION_Y, 0f, -dp(10)).setDuration(250).start();
|
||||||
|
CompletableFuture.delayedExecutor(250, java.util.concurrent.TimeUnit.MILLISECONDS).execute(() -> {
|
||||||
|
MuiModApi.postToUiThread(() -> wpScroll.setVisibility(View.GONE));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package de.winniepat.citrus.gui;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.draggui.HudConfig;
|
||||||
|
import de.winniepat.citrus.draggui.HudUtils;
|
||||||
|
import de.winniepat.citrus.utils.text.TextComponent;
|
||||||
|
import io.wispforest.owo.ui.base.BaseOwoScreen;
|
||||||
|
import io.wispforest.owo.ui.container.Containers;
|
||||||
|
import io.wispforest.owo.ui.container.FlowLayout;
|
||||||
|
import io.wispforest.owo.ui.core.*;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public class ModuleToggleScreen extends BaseOwoScreen<FlowLayout> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected @NotNull OwoUIAdapter<FlowLayout> createAdapter() { return OwoUIAdapter.create(this, Containers::verticalFlow); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void build(FlowLayout root) {
|
||||||
|
root.horizontalAlignment(HorizontalAlignment.CENTER)
|
||||||
|
.verticalAlignment(VerticalAlignment.CENTER);
|
||||||
|
|
||||||
|
FlowLayout content = (FlowLayout) Containers.verticalFlow(Sizing.content(), Sizing.content())
|
||||||
|
.padding(Insets.of(12))
|
||||||
|
.surface(Surface.DARK_PANEL)
|
||||||
|
.horizontalAlignment(HorizontalAlignment.CENTER);
|
||||||
|
content.gap(3);
|
||||||
|
|
||||||
|
var mainTitle = new TextComponent(Text.literal("Module Toggles")).scale(1.8f).horizontalSizing(Sizing.fixed(140)).margins(Insets.of(10));
|
||||||
|
|
||||||
|
content.child(mainTitle).gap(10);
|
||||||
|
|
||||||
|
FlowLayout firstRow = Containers.horizontalFlow(Sizing.content(), Sizing.content());
|
||||||
|
firstRow.gap(6);
|
||||||
|
content.child(firstRow);
|
||||||
|
|
||||||
|
FlowLayout secondRow = Containers.horizontalFlow(Sizing.content(), Sizing.content());
|
||||||
|
secondRow.gap(6);
|
||||||
|
content.child(secondRow);
|
||||||
|
|
||||||
|
HudConfig c = HudConfig.INSTANCE;
|
||||||
|
|
||||||
|
firstRow.child(HudUtils.createButton("FPS", "fps", "Frames per second", c.fpsEnabled, state -> c.fpsEnabled = state));
|
||||||
|
firstRow.child(HudUtils.createButton("Ping", "ping", "Your latency", c.pingEnabled, state -> c.pingEnabled = state));
|
||||||
|
firstRow.child(HudUtils.createButton("Server", "server", "Server address", c.serverEnabled, state -> c.serverEnabled = state));
|
||||||
|
|
||||||
|
secondRow.child(HudUtils.createButton("Keys", "key", "Keystrokes", c.keyEnabled, state -> c.keyEnabled = state));
|
||||||
|
secondRow.child(HudUtils.createButton("Armor", "armor", "Armor status", c.armorEnabled, state -> c.armorEnabled = state));
|
||||||
|
secondRow.child(HudUtils.createButton("Potion", "potion", "Active effects", c.potionEnabled, state -> c.potionEnabled = state));
|
||||||
|
|
||||||
|
root.child(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,935 @@
|
|||||||
|
package de.winniepat.citrus.gui;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.managers.ProfileManager;
|
||||||
|
import de.winniepat.citrus.managers.ProfileManager.PlayerProfile;
|
||||||
|
import de.winniepat.citrus.managers.ProfileManager.SearchResult;
|
||||||
|
import de.winniepat.citrus.managers.WallpaperManager;
|
||||||
|
import de.winniepat.citrus.managers.ConfigManager;
|
||||||
|
import de.winniepat.citrus.utils.texture.CdnTextureDrawable;
|
||||||
|
import icyllis.modernui.animation.*;
|
||||||
|
import icyllis.modernui.core.Context;
|
||||||
|
import icyllis.modernui.fragment.Fragment;
|
||||||
|
import icyllis.modernui.graphics.drawable.*;
|
||||||
|
import icyllis.modernui.mc.*;
|
||||||
|
import icyllis.modernui.text.InputFilter;
|
||||||
|
import icyllis.modernui.text.TextPaint;
|
||||||
|
import icyllis.modernui.util.DataSet;
|
||||||
|
import icyllis.modernui.view.*;
|
||||||
|
import icyllis.modernui.widget.*;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import org.jetbrains.annotations.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class ProfileScreen extends Fragment implements ScreenCallback {
|
||||||
|
|
||||||
|
private LinearLayout mContentContainer;
|
||||||
|
private LinearLayout mSearchResultsContainer;
|
||||||
|
private EditText mSearchInput;
|
||||||
|
private boolean mIsEditMode = false;
|
||||||
|
private PlayerProfile mCurrentProfile;
|
||||||
|
private UUID mViewingUuid;
|
||||||
|
private CdnTextureDrawable mBackground1Drawable;
|
||||||
|
private final List<View> mStaggeredViews = new ArrayList<>();
|
||||||
|
private EditText mBioEdit;
|
||||||
|
private EditText mDiscordEdit;
|
||||||
|
private EditText mTwitterEdit;
|
||||||
|
private EditText mYoutubeEdit;
|
||||||
|
private EditText mTwitchEdit;
|
||||||
|
private EditText mInstagramEdit;
|
||||||
|
private EditText mGithubEdit;
|
||||||
|
private EditText mWebsiteEdit;
|
||||||
|
|
||||||
|
private float getGuiScale() {
|
||||||
|
return (float) MinecraftClient.getInstance().getWindow().getScaleFactor();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFixedTextSize(TextView tv, float px) {
|
||||||
|
float scale = getGuiScale();
|
||||||
|
tv.setTextSize(px / scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable DataSet savedInstanceState) {
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
FrameLayout root = new FrameLayout(context);
|
||||||
|
root.setLayoutParams(new ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
|
||||||
|
FrameLayout backgroundView = new FrameLayout(context);
|
||||||
|
backgroundView.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
backgroundView.setScaleX(1.1f);
|
||||||
|
backgroundView.setScaleY(1.1f);
|
||||||
|
|
||||||
|
View backgroundView1 = new View(context);
|
||||||
|
backgroundView1.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
backgroundView.addView(backgroundView1);
|
||||||
|
|
||||||
|
root.addView(backgroundView);
|
||||||
|
|
||||||
|
mBackground1Drawable = new CdnTextureDrawable();
|
||||||
|
backgroundView1.setBackground(mBackground1Drawable);
|
||||||
|
|
||||||
|
String currentWallpaper = ConfigManager.config.mainmenuWallpaper;
|
||||||
|
if (currentWallpaper == null || currentWallpaper.isEmpty()) {
|
||||||
|
currentWallpaper = "default.png";
|
||||||
|
}
|
||||||
|
WallpaperManager.loadWallpaperWithData(currentWallpaper, imageData -> {
|
||||||
|
MuiModApi.postToUiThread(() -> mBackground1Drawable.setImageData(imageData));
|
||||||
|
});
|
||||||
|
|
||||||
|
ObjectAnimator bgPulseX = ObjectAnimator.ofFloat(backgroundView, View.SCALE_X, 1.1f, 1.15f);
|
||||||
|
ObjectAnimator bgPulseY = ObjectAnimator.ofFloat(backgroundView, View.SCALE_Y, 1.1f, 1.15f);
|
||||||
|
bgPulseX.setDuration(15000);
|
||||||
|
bgPulseY.setDuration(15000);
|
||||||
|
bgPulseX.setRepeatCount(ValueAnimator.INFINITE);
|
||||||
|
bgPulseY.setRepeatCount(ValueAnimator.INFINITE);
|
||||||
|
bgPulseX.setRepeatMode(ValueAnimator.REVERSE);
|
||||||
|
bgPulseY.setRepeatMode(ValueAnimator.REVERSE);
|
||||||
|
bgPulseX.start();
|
||||||
|
bgPulseY.start();
|
||||||
|
|
||||||
|
FrameLayout contentWrapper = new FrameLayout(context);
|
||||||
|
contentWrapper.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
root.addView(contentWrapper);
|
||||||
|
|
||||||
|
LinearLayout leftPanel = new LinearLayout(context);
|
||||||
|
leftPanel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
FrameLayout.LayoutParams leftPanelLp = new FrameLayout.LayoutParams(
|
||||||
|
320,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
leftPanelLp.gravity = Gravity.CENTER_VERTICAL;
|
||||||
|
leftPanelLp.setMargins(60, 0, 0, 0);
|
||||||
|
leftPanel.setLayoutParams(leftPanelLp);
|
||||||
|
|
||||||
|
TextView title = new TextView(context);
|
||||||
|
title.setText("PROFILES");
|
||||||
|
setFixedTextSize(title, 96);
|
||||||
|
title.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
title.setTextColor(0xFFFFFFFF);
|
||||||
|
leftPanel.addView(title);
|
||||||
|
mStaggeredViews.add(title);
|
||||||
|
|
||||||
|
TextView subtitle = new TextView(context);
|
||||||
|
subtitle.setText("View and edit player profiles");
|
||||||
|
setFixedTextSize(subtitle, 32);
|
||||||
|
subtitle.setTextColor(0xCCFFFFFF);
|
||||||
|
LinearLayout.LayoutParams subtitleLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
subtitleLp.setMargins(0, 0, 0, dp(30));
|
||||||
|
subtitle.setLayoutParams(subtitleLp);
|
||||||
|
leftPanel.addView(subtitle);
|
||||||
|
mStaggeredViews.add(subtitle);
|
||||||
|
|
||||||
|
LinearLayout searchSection = new LinearLayout(context);
|
||||||
|
searchSection.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
LinearLayout.LayoutParams searchSectionLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
searchSectionLp.setMargins(0, 0, 0, dp(20));
|
||||||
|
searchSection.setLayoutParams(searchSectionLp);
|
||||||
|
|
||||||
|
TextView searchLabel = new TextView(context);
|
||||||
|
searchLabel.setText("Search Players");
|
||||||
|
setFixedTextSize(searchLabel, 28);
|
||||||
|
searchLabel.setTextColor(0x88FFFFFF);
|
||||||
|
searchLabel.setPadding(0, 0, 0, dp(8));
|
||||||
|
searchSection.addView(searchLabel);
|
||||||
|
|
||||||
|
mSearchInput = new EditText(context);
|
||||||
|
mSearchInput.setHint("Enter player name...");
|
||||||
|
setFixedTextSize(mSearchInput, 32);
|
||||||
|
mSearchInput.setTextColor(0xFFFFFFFF);
|
||||||
|
mSearchInput.setHintTextColor(0x66FFFFFF);
|
||||||
|
mSearchInput.setPadding(dp(16), dp(12), dp(16), dp(12));
|
||||||
|
mSearchInput.setSingleLine(true);
|
||||||
|
ShapeDrawable inputBg = new ShapeDrawable();
|
||||||
|
inputBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
inputBg.setColor(0x26FFFFFF);
|
||||||
|
inputBg.setCornerRadius(dp(10));
|
||||||
|
mSearchInput.setBackground(inputBg);
|
||||||
|
LinearLayout.LayoutParams inputLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
inputLp.setMargins(0, 0, 0, dp(8));
|
||||||
|
mSearchInput.setLayoutParams(inputLp);
|
||||||
|
searchSection.addView(mSearchInput);
|
||||||
|
|
||||||
|
leftPanel.addView(searchSection);
|
||||||
|
mStaggeredViews.add(searchSection);
|
||||||
|
|
||||||
|
addNavButton(leftPanel, "Search", "multiplayer_icon", v -> performSearch());
|
||||||
|
addNavButton(leftPanel, "My Profile", "settings_icon", v -> loadOwnProfile());
|
||||||
|
addNavButton(leftPanel, "Back to Menu", "door_icon", v -> {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient.getInstance().setScreen(MuiModApi.get().createScreen(new MainMenuScreen(), null, null));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
contentWrapper.addView(leftPanel);
|
||||||
|
|
||||||
|
LinearLayout rightPanel = new LinearLayout(context);
|
||||||
|
rightPanel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
FrameLayout.LayoutParams rightPanelLp = new FrameLayout.LayoutParams(
|
||||||
|
500,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
rightPanelLp.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT;
|
||||||
|
rightPanelLp.setMargins(0, 0, 60, 0);
|
||||||
|
rightPanel.setLayoutParams(rightPanelLp);
|
||||||
|
|
||||||
|
mSearchResultsContainer = new LinearLayout(context);
|
||||||
|
mSearchResultsContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
mSearchResultsContainer.setVisibility(View.GONE);
|
||||||
|
LinearLayout.LayoutParams resultsLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
resultsLp.setMargins(0, 0, 0, dp(12));
|
||||||
|
mSearchResultsContainer.setLayoutParams(resultsLp);
|
||||||
|
|
||||||
|
ShapeDrawable resultsBg = new ShapeDrawable();
|
||||||
|
resultsBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
resultsBg.setColor(0x33000000);
|
||||||
|
resultsBg.setCornerRadius(dp(16));
|
||||||
|
mSearchResultsContainer.setBackground(resultsBg);
|
||||||
|
mSearchResultsContainer.setPadding(dp(16), dp(16), dp(16), dp(16));
|
||||||
|
|
||||||
|
rightPanel.addView(mSearchResultsContainer);
|
||||||
|
|
||||||
|
LinearLayout profilePanel = new LinearLayout(context);
|
||||||
|
profilePanel.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
profilePanel.setPadding(dp(16), dp(16), dp(16), dp(16));
|
||||||
|
LinearLayout.LayoutParams profilePanelLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
0, 1f
|
||||||
|
);
|
||||||
|
profilePanel.setLayoutParams(profilePanelLp);
|
||||||
|
|
||||||
|
ShapeDrawable panelBg = new ShapeDrawable();
|
||||||
|
panelBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
panelBg.setColor(0x33000000);
|
||||||
|
panelBg.setCornerRadius(dp(16));
|
||||||
|
profilePanel.setBackground(panelBg);
|
||||||
|
|
||||||
|
TextView panelTitle = new TextView(context);
|
||||||
|
panelTitle.setText("PROFILE");
|
||||||
|
setFixedTextSize(panelTitle, 48);
|
||||||
|
panelTitle.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
panelTitle.setTextColor(0xFF4a90d9);
|
||||||
|
panelTitle.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams panelTitleLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
panelTitleLp.setMargins(0, 0, 0, dp(12));
|
||||||
|
panelTitle.setLayoutParams(panelTitleLp);
|
||||||
|
profilePanel.addView(panelTitle);
|
||||||
|
|
||||||
|
ScrollView scrollView = new ScrollView(context);
|
||||||
|
LinearLayout.LayoutParams scrollLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
0, 1f
|
||||||
|
);
|
||||||
|
scrollView.setLayoutParams(scrollLp);
|
||||||
|
scrollView.setScrollBarSize(dp(6));
|
||||||
|
|
||||||
|
mContentContainer = new LinearLayout(context);
|
||||||
|
mContentContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
scrollView.addView(mContentContainer);
|
||||||
|
profilePanel.addView(scrollView);
|
||||||
|
|
||||||
|
rightPanel.addView(profilePanel);
|
||||||
|
mStaggeredViews.add(rightPanel);
|
||||||
|
|
||||||
|
contentWrapper.addView(rightPanel);
|
||||||
|
|
||||||
|
TextView infoText = new TextView(context);
|
||||||
|
infoText.setText("Share your profile with friends • Add your socials to connect");
|
||||||
|
setFixedTextSize(infoText, 24);
|
||||||
|
infoText.setTextColor(0x66FFFFFF);
|
||||||
|
FrameLayout.LayoutParams infoLp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
infoLp.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
|
||||||
|
infoLp.setMargins(0, 0, 0, dp(20));
|
||||||
|
infoText.setLayoutParams(infoLp);
|
||||||
|
contentWrapper.addView(infoText);
|
||||||
|
mStaggeredViews.add(infoText);
|
||||||
|
|
||||||
|
loadOwnProfile();
|
||||||
|
animateEntrance();
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNavButton(LinearLayout parent, String text, String iconPath, View.OnClickListener listener) {
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
FrameLayout btnWrapper = new FrameLayout(context);
|
||||||
|
LinearLayout.LayoutParams wrapperLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
56
|
||||||
|
);
|
||||||
|
wrapperLp.setMargins(0, 0, 0, 8);
|
||||||
|
btnWrapper.setLayoutParams(wrapperLp);
|
||||||
|
|
||||||
|
ShapeDrawable btnBg = new ShapeDrawable();
|
||||||
|
btnBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
btnBg.setColor(0x1AFFFFFF);
|
||||||
|
btnBg.setCornerRadius(12);
|
||||||
|
btnWrapper.setBackground(btnBg);
|
||||||
|
|
||||||
|
LinearLayout content = new LinearLayout(context);
|
||||||
|
content.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
content.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
content.setPadding(20, 0, 20, 0);
|
||||||
|
|
||||||
|
ImageDrawable icon = new ImageDrawable("citrus", "startscreen/" + iconPath + ".png");
|
||||||
|
icon.setBounds(0, 0, 24, 24);
|
||||||
|
|
||||||
|
TextView tv = new TextView(context);
|
||||||
|
tv.setText(text);
|
||||||
|
setFixedTextSize(tv, 36);
|
||||||
|
tv.setTextColor(0xFFFFFFFF);
|
||||||
|
tv.setCompoundDrawables(icon, null, null, null);
|
||||||
|
tv.setCompoundDrawablePadding(16);
|
||||||
|
|
||||||
|
content.addView(tv);
|
||||||
|
btnWrapper.addView(content);
|
||||||
|
|
||||||
|
btnWrapper.setClickable(true);
|
||||||
|
btnWrapper.setFocusable(true);
|
||||||
|
btnWrapper.setOnClickListener(listener);
|
||||||
|
|
||||||
|
btnWrapper.setOnHoverListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
|
||||||
|
ObjectAnimator animator = ObjectAnimator.ofFloat(v, View.TRANSLATION_X, 0f, (float) dp(15));
|
||||||
|
animator.setDuration(250);
|
||||||
|
animator.setInterpolator(TimeInterpolator.DECELERATE);
|
||||||
|
animator.start();
|
||||||
|
btnBg.setColor(0x33FFFFFF);
|
||||||
|
return true;
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||||
|
ObjectAnimator animator = ObjectAnimator.ofFloat(v, View.TRANSLATION_X, (float) dp(15), 0f);
|
||||||
|
animator.setDuration(250);
|
||||||
|
animator.setInterpolator(TimeInterpolator.DECELERATE);
|
||||||
|
animator.start();
|
||||||
|
btnBg.setColor(0x1AFFFFFF);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
parent.addView(btnWrapper);
|
||||||
|
mStaggeredViews.add(btnWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateEntrance() {
|
||||||
|
long delay = 100;
|
||||||
|
for (View v : mStaggeredViews) {
|
||||||
|
v.setAlpha(0);
|
||||||
|
float startX = -dp(40);
|
||||||
|
|
||||||
|
ViewGroup.LayoutParams lp = v.getLayoutParams();
|
||||||
|
if (lp instanceof FrameLayout.LayoutParams flp) {
|
||||||
|
if ((flp.gravity & Gravity.RIGHT) == Gravity.RIGHT) {
|
||||||
|
startX = dp(40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.setTranslationX(startX);
|
||||||
|
|
||||||
|
ObjectAnimator alpha = ObjectAnimator.ofFloat(v, View.ALPHA, 0f, 1f);
|
||||||
|
ObjectAnimator trans = ObjectAnimator.ofFloat(v, View.TRANSLATION_X, startX, 0f);
|
||||||
|
|
||||||
|
AnimatorSet set = new AnimatorSet();
|
||||||
|
set.playTogether(alpha, trans);
|
||||||
|
set.setDuration(800);
|
||||||
|
set.setStartDelay(delay);
|
||||||
|
set.setInterpolator(TimeInterpolator.DECELERATE);
|
||||||
|
set.start();
|
||||||
|
|
||||||
|
delay += 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadOwnProfile() {
|
||||||
|
mSearchResultsContainer.setVisibility(View.GONE);
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
UUID uuid = MinecraftClient.getInstance().getSession().getUuidOrNull();
|
||||||
|
if (uuid == null) {
|
||||||
|
showError("Not logged in");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mViewingUuid = uuid;
|
||||||
|
ProfileManager.getOwnProfile().thenAccept(profile -> {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (profile != null) {
|
||||||
|
mCurrentProfile = profile;
|
||||||
|
showProfileView(profile, true);
|
||||||
|
} else {
|
||||||
|
mCurrentProfile = new PlayerProfile(
|
||||||
|
uuid.toString(),
|
||||||
|
MinecraftClient.getInstance().getSession().getUsername()
|
||||||
|
);
|
||||||
|
showProfileEdit(mCurrentProfile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadProfile(UUID uuid) {
|
||||||
|
mSearchResultsContainer.setVisibility(View.GONE);
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
mViewingUuid = uuid;
|
||||||
|
UUID ownUuid = MinecraftClient.getInstance().getSession().getUuidOrNull();
|
||||||
|
boolean isOwn = ownUuid != null && ownUuid.equals(uuid);
|
||||||
|
|
||||||
|
ProfileManager.getProfile(uuid).thenAccept(profile -> {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (profile != null) {
|
||||||
|
mCurrentProfile = profile;
|
||||||
|
showProfileView(profile, isOwn);
|
||||||
|
} else {
|
||||||
|
showError("Profile not found");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performSearch() {
|
||||||
|
String query = mSearchInput.getText().toString().trim();
|
||||||
|
if (query.isEmpty()) return;
|
||||||
|
|
||||||
|
mSearchResultsContainer.removeAllViews();
|
||||||
|
mSearchResultsContainer.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
TextView searching = new TextView(requireContext());
|
||||||
|
searching.setText("Searching...");
|
||||||
|
setFixedTextSize(searching, 28);
|
||||||
|
searching.setTextColor(0x88FFFFFF);
|
||||||
|
mSearchResultsContainer.addView(searching);
|
||||||
|
|
||||||
|
ProfileManager.searchProfiles(query).thenAccept(results -> {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
mSearchResultsContainer.removeAllViews();
|
||||||
|
|
||||||
|
if (results.isEmpty()) {
|
||||||
|
TextView noResults = new TextView(requireContext());
|
||||||
|
noResults.setText("No profiles found");
|
||||||
|
setFixedTextSize(noResults, 28);
|
||||||
|
noResults.setTextColor(0x88FFFFFF);
|
||||||
|
mSearchResultsContainer.addView(noResults);
|
||||||
|
} else {
|
||||||
|
TextView resultsLabel = new TextView(requireContext());
|
||||||
|
resultsLabel.setText("Results (" + results.size() + ")");
|
||||||
|
setFixedTextSize(resultsLabel, 32);
|
||||||
|
resultsLabel.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
resultsLabel.setTextColor(0xFFFFD700);
|
||||||
|
resultsLabel.setPadding(0, 0, 0, dp(10));
|
||||||
|
mSearchResultsContainer.addView(resultsLabel);
|
||||||
|
|
||||||
|
for (SearchResult result : results) {
|
||||||
|
mSearchResultsContainer.addView(createSearchResultCard(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createSearchResultCard(SearchResult result) {
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
LinearLayout card = new LinearLayout(context);
|
||||||
|
card.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
card.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
card.setPadding(dp(12), dp(10), dp(12), dp(10));
|
||||||
|
LinearLayout.LayoutParams cardLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
cardLp.setMargins(0, 0, 0, dp(8));
|
||||||
|
card.setLayoutParams(cardLp);
|
||||||
|
|
||||||
|
ShapeDrawable cardBg = new ShapeDrawable();
|
||||||
|
cardBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
cardBg.setColor(0x20FFFFFF);
|
||||||
|
cardBg.setCornerRadius(dp(10));
|
||||||
|
card.setBackground(cardBg);
|
||||||
|
card.setClickable(true);
|
||||||
|
|
||||||
|
card.setOnClickListener(v -> {
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(result.uuid);
|
||||||
|
loadProfile(uuid);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
card.setOnHoverListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
|
||||||
|
cardBg.setColor(0x33FFFFFF);
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||||
|
cardBg.setColor(0x20FFFFFF);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
TextView nameTv = new TextView(context);
|
||||||
|
nameTv.setText(result.playerName);
|
||||||
|
setFixedTextSize(nameTv, 30);
|
||||||
|
nameTv.setTextColor(0xFFFFFFFF);
|
||||||
|
LinearLayout.LayoutParams nameLp = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f
|
||||||
|
);
|
||||||
|
nameTv.setLayoutParams(nameLp);
|
||||||
|
card.addView(nameTv);
|
||||||
|
|
||||||
|
if (result.hasSocials) {
|
||||||
|
TextView socialsTag = new TextView(context);
|
||||||
|
socialsTag.setText("Socials");
|
||||||
|
setFixedTextSize(socialsTag, 22);
|
||||||
|
socialsTag.setTextColor(0xFF5cb85c);
|
||||||
|
socialsTag.setPadding(dp(8), dp(4), dp(8), dp(4));
|
||||||
|
ShapeDrawable tagBg = new ShapeDrawable();
|
||||||
|
tagBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
tagBg.setColor(0x335cb85c);
|
||||||
|
tagBg.setCornerRadius(dp(4));
|
||||||
|
socialsTag.setBackground(tagBg);
|
||||||
|
card.addView(socialsTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showLoading() {
|
||||||
|
mContentContainer.removeAllViews();
|
||||||
|
|
||||||
|
LinearLayout loadingContainer = new LinearLayout(requireContext());
|
||||||
|
loadingContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
loadingContainer.setGravity(Gravity.CENTER);
|
||||||
|
loadingContainer.setPadding(dp(20), dp(60), dp(20), dp(60));
|
||||||
|
|
||||||
|
TextView loading = new TextView(requireContext());
|
||||||
|
loading.setText("Loading profile...");
|
||||||
|
setFixedTextSize(loading, 32);
|
||||||
|
loading.setTextColor(0x88FFFFFF);
|
||||||
|
loading.setGravity(Gravity.CENTER);
|
||||||
|
loadingContainer.addView(loading);
|
||||||
|
|
||||||
|
mContentContainer.addView(loadingContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showError(String message) {
|
||||||
|
mContentContainer.removeAllViews();
|
||||||
|
|
||||||
|
LinearLayout errorContainer = new LinearLayout(requireContext());
|
||||||
|
errorContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
errorContainer.setGravity(Gravity.CENTER);
|
||||||
|
errorContainer.setPadding(dp(20), dp(60), dp(20), dp(60));
|
||||||
|
|
||||||
|
TextView error = new TextView(requireContext());
|
||||||
|
error.setText(message);
|
||||||
|
setFixedTextSize(error, 32);
|
||||||
|
error.setTextColor(0xFFFF6B6B);
|
||||||
|
error.setGravity(Gravity.CENTER);
|
||||||
|
errorContainer.addView(error);
|
||||||
|
|
||||||
|
mContentContainer.addView(errorContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showProfileView(PlayerProfile profile, boolean isOwn) {
|
||||||
|
mIsEditMode = false;
|
||||||
|
mContentContainer.removeAllViews();
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
LinearLayout header = new LinearLayout(context);
|
||||||
|
header.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
header.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
LinearLayout.LayoutParams headerLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
headerLp.setMargins(0, 0, 0, dp(16));
|
||||||
|
header.setLayoutParams(headerLp);
|
||||||
|
|
||||||
|
TextView nameTv = new TextView(context);
|
||||||
|
nameTv.setText(profile.playerName);
|
||||||
|
setFixedTextSize(nameTv, 42);
|
||||||
|
nameTv.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
nameTv.setTextColor(0xFFFFFFFF);
|
||||||
|
LinearLayout.LayoutParams nameLp = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f
|
||||||
|
);
|
||||||
|
nameTv.setLayoutParams(nameLp);
|
||||||
|
header.addView(nameTv);
|
||||||
|
|
||||||
|
if (isOwn) {
|
||||||
|
TextView editBtn = createSmallActionButton(context, "Edit", 0xFF4a90d9);
|
||||||
|
editBtn.setOnClickListener(v -> showProfileEdit(profile));
|
||||||
|
header.addView(editBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
mContentContainer.addView(header);
|
||||||
|
|
||||||
|
if (profile.bio != null && !profile.bio.trim().isEmpty()) {
|
||||||
|
mContentContainer.addView(createSectionLabel(context, "About"));
|
||||||
|
TextView bioTv = new TextView(context);
|
||||||
|
bioTv.setText(profile.bio);
|
||||||
|
setFixedTextSize(bioTv, 28);
|
||||||
|
bioTv.setTextColor(0xCCFFFFFF);
|
||||||
|
bioTv.setPadding(0, 0, 0, dp(16));
|
||||||
|
mContentContainer.addView(bioTv);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean hasSocials = profile.hasSocialLinks();
|
||||||
|
if (hasSocials) {
|
||||||
|
mContentContainer.addView(createSectionLabel(context, "Social Links"));
|
||||||
|
|
||||||
|
LinearLayout socialsGrid = new LinearLayout(context);
|
||||||
|
socialsGrid.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
if (!isEmpty(profile.discord)) {
|
||||||
|
socialsGrid.addView(createSocialLink(context, "Discord", profile.discord, 0xFF5865F2));
|
||||||
|
}
|
||||||
|
if (!isEmpty(profile.twitter)) {
|
||||||
|
socialsGrid.addView(createSocialLink(context, "Twitter/X", profile.twitter, 0xFF1DA1F2));
|
||||||
|
}
|
||||||
|
if (!isEmpty(profile.youtube)) {
|
||||||
|
socialsGrid.addView(createSocialLink(context, "YouTube", profile.youtube, 0xFFFF0000));
|
||||||
|
}
|
||||||
|
if (!isEmpty(profile.twitch)) {
|
||||||
|
socialsGrid.addView(createSocialLink(context, "Twitch", profile.twitch, 0xFF9146FF));
|
||||||
|
}
|
||||||
|
if (!isEmpty(profile.instagram)) {
|
||||||
|
socialsGrid.addView(createSocialLink(context, "Instagram", profile.instagram, 0xFFE1306C));
|
||||||
|
}
|
||||||
|
if (!isEmpty(profile.github)) {
|
||||||
|
socialsGrid.addView(createSocialLink(context, "GitHub", profile.github, 0xFFFFFFFF));
|
||||||
|
}
|
||||||
|
if (!isEmpty(profile.website)) {
|
||||||
|
socialsGrid.addView(createSocialLink(context, "Website", profile.website, 0xFF4a90d9));
|
||||||
|
}
|
||||||
|
|
||||||
|
mContentContainer.addView(socialsGrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSocials && (profile.bio == null || profile.bio.trim().isEmpty())) {
|
||||||
|
LinearLayout emptyContainer = new LinearLayout(context);
|
||||||
|
emptyContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
emptyContainer.setGravity(Gravity.CENTER);
|
||||||
|
emptyContainer.setPadding(dp(16), dp(40), dp(16), dp(40));
|
||||||
|
|
||||||
|
TextView emptyTitle = new TextView(context);
|
||||||
|
emptyTitle.setText(isOwn ? "Profile Empty" : "No Info");
|
||||||
|
setFixedTextSize(emptyTitle, 32);
|
||||||
|
emptyTitle.setTextColor(0x88FFFFFF);
|
||||||
|
emptyTitle.setGravity(Gravity.CENTER);
|
||||||
|
emptyContainer.addView(emptyTitle);
|
||||||
|
|
||||||
|
TextView emptySubtitle = new TextView(context);
|
||||||
|
emptySubtitle.setText(isOwn ? "Click 'Edit' to add your info!" : "This player hasn't added info yet.");
|
||||||
|
setFixedTextSize(emptySubtitle, 24);
|
||||||
|
emptySubtitle.setTextColor(0x55FFFFFF);
|
||||||
|
emptySubtitle.setGravity(Gravity.CENTER);
|
||||||
|
emptySubtitle.setPadding(0, dp(4), 0, 0);
|
||||||
|
emptyContainer.addView(emptySubtitle);
|
||||||
|
|
||||||
|
mContentContainer.addView(emptyContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showProfileEdit(PlayerProfile profile) {
|
||||||
|
mIsEditMode = true;
|
||||||
|
mContentContainer.removeAllViews();
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
TextView headerTv = new TextView(context);
|
||||||
|
headerTv.setText("Edit Your Profile");
|
||||||
|
setFixedTextSize(headerTv, 36);
|
||||||
|
headerTv.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
headerTv.setTextColor(0xFFFFFFFF);
|
||||||
|
headerTv.setPadding(0, 0, 0, dp(16));
|
||||||
|
mContentContainer.addView(headerTv);
|
||||||
|
|
||||||
|
mContentContainer.addView(createInputLabel(context, "Bio"));
|
||||||
|
mBioEdit = createEditText(context, profile.bio, "Tell others about yourself...", false);
|
||||||
|
mBioEdit.setMinLines(2);
|
||||||
|
mBioEdit.setMaxLines(4);
|
||||||
|
mBioEdit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(500)});
|
||||||
|
mContentContainer.addView(mBioEdit);
|
||||||
|
|
||||||
|
mContentContainer.addView(createSectionLabel(context, "Social Links"));
|
||||||
|
|
||||||
|
mContentContainer.addView(createInputLabel(context, "Discord"));
|
||||||
|
mDiscordEdit = createEditText(context, profile.discord, "username", true);
|
||||||
|
mContentContainer.addView(mDiscordEdit);
|
||||||
|
|
||||||
|
mContentContainer.addView(createInputLabel(context, "Twitter/X"));
|
||||||
|
mTwitterEdit = createEditText(context, profile.twitter, "@username", true);
|
||||||
|
mContentContainer.addView(mTwitterEdit);
|
||||||
|
|
||||||
|
mContentContainer.addView(createInputLabel(context, "YouTube"));
|
||||||
|
mYoutubeEdit = createEditText(context, profile.youtube, "Channel URL", true);
|
||||||
|
mContentContainer.addView(mYoutubeEdit);
|
||||||
|
|
||||||
|
mContentContainer.addView(createInputLabel(context, "Twitch"));
|
||||||
|
mTwitchEdit = createEditText(context, profile.twitch, "username", true);
|
||||||
|
mContentContainer.addView(mTwitchEdit);
|
||||||
|
|
||||||
|
mContentContainer.addView(createInputLabel(context, "Instagram"));
|
||||||
|
mInstagramEdit = createEditText(context, profile.instagram, "@username", true);
|
||||||
|
mContentContainer.addView(mInstagramEdit);
|
||||||
|
|
||||||
|
mContentContainer.addView(createInputLabel(context, "GitHub"));
|
||||||
|
mGithubEdit = createEditText(context, profile.github, "username", true);
|
||||||
|
mContentContainer.addView(mGithubEdit);
|
||||||
|
|
||||||
|
mContentContainer.addView(createInputLabel(context, "Website"));
|
||||||
|
mWebsiteEdit = createEditText(context, profile.website, "https://...", true);
|
||||||
|
mContentContainer.addView(mWebsiteEdit);
|
||||||
|
|
||||||
|
LinearLayout buttonRow = new LinearLayout(context);
|
||||||
|
buttonRow.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
buttonRow.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams buttonRowLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
buttonRowLp.setMargins(0, dp(16), 0, 0);
|
||||||
|
buttonRow.setLayoutParams(buttonRowLp);
|
||||||
|
|
||||||
|
TextView cancelBtn = createSmallActionButton(context, "Cancel", 0xFF666666);
|
||||||
|
cancelBtn.setOnClickListener(v -> showProfileView(profile, true));
|
||||||
|
LinearLayout.LayoutParams cancelLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
cancelLp.setMargins(0, 0, dp(10), 0);
|
||||||
|
cancelBtn.setLayoutParams(cancelLp);
|
||||||
|
buttonRow.addView(cancelBtn);
|
||||||
|
|
||||||
|
TextView saveBtn = createSmallActionButton(context, "Save", 0xFF5cb85c);
|
||||||
|
saveBtn.setOnClickListener(v -> saveProfile());
|
||||||
|
buttonRow.addView(saveBtn);
|
||||||
|
|
||||||
|
mContentContainer.addView(buttonRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveProfile() {
|
||||||
|
PlayerProfile updated = new PlayerProfile();
|
||||||
|
updated.uuid = mCurrentProfile.uuid;
|
||||||
|
updated.playerName = MinecraftClient.getInstance().getSession().getUsername();
|
||||||
|
updated.bio = mBioEdit.getText().toString().trim();
|
||||||
|
updated.discord = mDiscordEdit.getText().toString().trim();
|
||||||
|
updated.twitter = mTwitterEdit.getText().toString().trim();
|
||||||
|
updated.youtube = mYoutubeEdit.getText().toString().trim();
|
||||||
|
updated.twitch = mTwitchEdit.getText().toString().trim();
|
||||||
|
updated.instagram = mInstagramEdit.getText().toString().trim();
|
||||||
|
updated.github = mGithubEdit.getText().toString().trim();
|
||||||
|
updated.website = mWebsiteEdit.getText().toString().trim();
|
||||||
|
|
||||||
|
showLoading();
|
||||||
|
|
||||||
|
ProfileManager.updateProfile(updated).thenAccept(error -> {
|
||||||
|
MuiModApi.postToUiThread(() -> {
|
||||||
|
if (error == null) {
|
||||||
|
mCurrentProfile = updated;
|
||||||
|
ProfileManager.invalidateCache(UUID.fromString(updated.uuid));
|
||||||
|
showProfileView(updated, true);
|
||||||
|
} else {
|
||||||
|
showError("Failed to save: " + error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createSocialLink(Context context, String platform, String value, int color) {
|
||||||
|
LinearLayout row = new LinearLayout(context);
|
||||||
|
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
row.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
row.setPadding(dp(10), dp(8), dp(10), dp(8));
|
||||||
|
LinearLayout.LayoutParams rowLp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
rowLp.setMargins(0, 0, 0, dp(6));
|
||||||
|
row.setLayoutParams(rowLp);
|
||||||
|
|
||||||
|
ShapeDrawable rowBg = new ShapeDrawable();
|
||||||
|
rowBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
rowBg.setColor(0x20FFFFFF);
|
||||||
|
rowBg.setCornerRadius(dp(8));
|
||||||
|
row.setBackground(rowBg);
|
||||||
|
|
||||||
|
View colorIndicator = new View(context);
|
||||||
|
colorIndicator.setLayoutParams(new LinearLayout.LayoutParams(dp(4), dp(20)));
|
||||||
|
ShapeDrawable indicatorBg = new ShapeDrawable();
|
||||||
|
indicatorBg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
indicatorBg.setColor(color);
|
||||||
|
indicatorBg.setCornerRadius(dp(2));
|
||||||
|
colorIndicator.setBackground(indicatorBg);
|
||||||
|
row.addView(colorIndicator);
|
||||||
|
|
||||||
|
TextView platformTv = new TextView(context);
|
||||||
|
platformTv.setText(platform);
|
||||||
|
setFixedTextSize(platformTv, 26);
|
||||||
|
platformTv.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
platformTv.setTextColor(0xFFFFFFFF);
|
||||||
|
platformTv.setPadding(dp(10), 0, dp(8), 0);
|
||||||
|
row.addView(platformTv);
|
||||||
|
|
||||||
|
TextView valueTv = new TextView(context);
|
||||||
|
valueTv.setText(value);
|
||||||
|
setFixedTextSize(valueTv, 24);
|
||||||
|
valueTv.setTextColor(0xCCFFFFFF);
|
||||||
|
LinearLayout.LayoutParams valueLp = new LinearLayout.LayoutParams(
|
||||||
|
0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f
|
||||||
|
);
|
||||||
|
valueTv.setLayoutParams(valueLp);
|
||||||
|
row.addView(valueTv);
|
||||||
|
|
||||||
|
TextView copyBtn = new TextView(context);
|
||||||
|
copyBtn.setText("Copy");
|
||||||
|
setFixedTextSize(copyBtn, 22);
|
||||||
|
copyBtn.setTextColor(0xFF4a90d9);
|
||||||
|
copyBtn.setPadding(dp(8), dp(4), dp(8), dp(4));
|
||||||
|
copyBtn.setClickable(true);
|
||||||
|
copyBtn.setOnClickListener(v -> {
|
||||||
|
MinecraftClient.getInstance().keyboard.setClipboard(value);
|
||||||
|
copyBtn.setText("✓");
|
||||||
|
v.postDelayed(() -> copyBtn.setText("Copy"), 2000);
|
||||||
|
});
|
||||||
|
row.addView(copyBtn);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextView createSmallActionButton(Context context, String text, int color) {
|
||||||
|
TextView btn = new TextView(context);
|
||||||
|
btn.setText(text);
|
||||||
|
setFixedTextSize(btn, 26);
|
||||||
|
btn.setTextColor(0xFFFFFFFF);
|
||||||
|
btn.setPadding(dp(14), dp(8), dp(14), dp(8));
|
||||||
|
btn.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
ShapeDrawable bg = new ShapeDrawable();
|
||||||
|
bg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
bg.setColor(color);
|
||||||
|
bg.setCornerRadius(dp(8));
|
||||||
|
btn.setBackground(bg);
|
||||||
|
btn.setClickable(true);
|
||||||
|
|
||||||
|
btn.setOnHoverListener((v, event) -> {
|
||||||
|
if (event.getAction() == MotionEvent.ACTION_HOVER_ENTER) {
|
||||||
|
bg.setColor(lightenColor(color, 0.2f));
|
||||||
|
} else if (event.getAction() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||||
|
bg.setColor(color);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextView createSectionLabel(Context context, String text) {
|
||||||
|
TextView label = new TextView(context);
|
||||||
|
label.setText(text);
|
||||||
|
setFixedTextSize(label, 30);
|
||||||
|
label.getPaint().setTextStyle(TextPaint.BOLD);
|
||||||
|
label.setTextColor(0xFFFFFFFF);
|
||||||
|
label.setPadding(0, dp(8), 0, dp(8));
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextView createInputLabel(Context context, String text) {
|
||||||
|
TextView label = new TextView(context);
|
||||||
|
label.setText(text);
|
||||||
|
setFixedTextSize(label, 24);
|
||||||
|
label.setTextColor(0xAAFFFFFF);
|
||||||
|
label.setPadding(0, dp(6), 0, dp(4));
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EditText createEditText(Context context, String value, String hint, boolean singleLine) {
|
||||||
|
EditText edit = new EditText(context);
|
||||||
|
if (value != null) edit.setText(value);
|
||||||
|
edit.setHint(hint);
|
||||||
|
setFixedTextSize(edit, 26);
|
||||||
|
edit.setTextColor(0xFFFFFFFF);
|
||||||
|
edit.setHintTextColor(0x66FFFFFF);
|
||||||
|
edit.setPadding(dp(12), dp(10), dp(12), dp(10));
|
||||||
|
edit.setSingleLine(singleLine);
|
||||||
|
|
||||||
|
ShapeDrawable bg = new ShapeDrawable();
|
||||||
|
bg.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
bg.setColor(0x26FFFFFF);
|
||||||
|
bg.setCornerRadius(dp(8));
|
||||||
|
edit.setBackground(bg);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
lp.setMargins(0, 0, 0, dp(4));
|
||||||
|
edit.setLayoutParams(lp);
|
||||||
|
|
||||||
|
return edit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEmpty(String s) {
|
||||||
|
return s == null || s.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int lightenColor(int color, float factor) {
|
||||||
|
int a = (color >> 24) & 0xFF;
|
||||||
|
int r = Math.min(255, (int) (((color >> 16) & 0xFF) * (1 + factor)));
|
||||||
|
int g = Math.min(255, (int) (((color >> 8) & 0xFF) * (1 + factor)));
|
||||||
|
int b = Math.min(255, (int) ((color & 0xFF) * (1 + factor)));
|
||||||
|
return (a << 24) | (r << 16) | (g << 8) | b;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dp(int value) {
|
||||||
|
return (int) (value * getContext().getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPauseScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldBlurBackground() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasDefaultBackground() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
package de.winniepat.citrus.gui;
|
||||||
|
|
||||||
|
import icyllis.modernui.core.Context;
|
||||||
|
import icyllis.modernui.fragment.Fragment;
|
||||||
|
import icyllis.modernui.graphics.Canvas;
|
||||||
|
import icyllis.modernui.graphics.Paint;
|
||||||
|
import icyllis.modernui.graphics.drawable.ShapeDrawable;
|
||||||
|
import icyllis.modernui.mc.ScreenCallback;
|
||||||
|
import icyllis.modernui.util.DataSet;
|
||||||
|
import icyllis.modernui.view.*;
|
||||||
|
import icyllis.modernui.widget.*;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
|
||||||
|
import net.minecraft.entity.LivingEntity;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test screen that renders a spinnable player preview with all cosmetics
|
||||||
|
* (capes, wings, halos, and hats).
|
||||||
|
*
|
||||||
|
* The player entity is rendered with all cosmetics automatically because
|
||||||
|
* the cosmetic feature renderers (WingsFeatureRenderer, HatFeatureRenderer,
|
||||||
|
* HaloFeatureRenderer) are already registered and will render on any player
|
||||||
|
* entity rendering. Capes are handled via the SkinTextures modification.
|
||||||
|
*/
|
||||||
|
public class TestScreen extends Fragment implements ScreenCallback {
|
||||||
|
|
||||||
|
private static TestScreen activeInstance = null;
|
||||||
|
|
||||||
|
private float mPlayerRotationY = 0f;
|
||||||
|
private float mPlayerRotationX = 0f;
|
||||||
|
private float mLastTouchX = 0f;
|
||||||
|
private float mLastTouchY = 0f;
|
||||||
|
private boolean mIsDragging = false;
|
||||||
|
private boolean mAutoSpinning = false;
|
||||||
|
private PlayerPreviewView mPlayerPreview;
|
||||||
|
|
||||||
|
// Store the preview bounds for Minecraft rendering
|
||||||
|
private int mPreviewCenterX = 0;
|
||||||
|
private int mPreviewCenterY = 0;
|
||||||
|
private int mPreviewSize = 80;
|
||||||
|
private boolean mPreviewBoundsSet = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable DataSet savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
activeInstance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (activeInstance == this) {
|
||||||
|
activeInstance = null;
|
||||||
|
mPreviewBoundsSet = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NotNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable DataSet savedInstanceState) {
|
||||||
|
Context context = requireContext();
|
||||||
|
|
||||||
|
FrameLayout root = new FrameLayout(context);
|
||||||
|
root.setLayoutParams(new ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
|
||||||
|
// Semi-transparent background
|
||||||
|
ShapeDrawable backgroundDrawable = new ShapeDrawable();
|
||||||
|
backgroundDrawable.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
backgroundDrawable.setColor(0x80000000);
|
||||||
|
root.setBackground(backgroundDrawable);
|
||||||
|
|
||||||
|
// Main content container
|
||||||
|
LinearLayout contentContainer = new LinearLayout(context);
|
||||||
|
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
contentContainer.setGravity(Gravity.CENTER);
|
||||||
|
FrameLayout.LayoutParams contentParams = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
);
|
||||||
|
contentContainer.setLayoutParams(contentParams);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
TextView title = new TextView(context);
|
||||||
|
title.setText("Player Preview");
|
||||||
|
title.setTextSize(28);
|
||||||
|
title.setTextColor(0xFFFFFFFF);
|
||||||
|
title.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
titleParams.setMargins(0, dp(20), 0, dp(10));
|
||||||
|
title.setLayoutParams(titleParams);
|
||||||
|
contentContainer.addView(title);
|
||||||
|
|
||||||
|
// Subtitle
|
||||||
|
TextView subtitle = new TextView(context);
|
||||||
|
subtitle.setText("Drag to rotate the player - Shows capes, wings, halos & hats");
|
||||||
|
subtitle.setTextSize(14);
|
||||||
|
subtitle.setTextColor(0xAAFFFFFF);
|
||||||
|
subtitle.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams subtitleParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
subtitleParams.setMargins(0, 0, 0, dp(20));
|
||||||
|
subtitle.setLayoutParams(subtitleParams);
|
||||||
|
contentContainer.addView(subtitle);
|
||||||
|
|
||||||
|
// Player preview container with border
|
||||||
|
FrameLayout previewContainer = new FrameLayout(context);
|
||||||
|
ShapeDrawable previewBackground = new ShapeDrawable();
|
||||||
|
previewBackground.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
previewBackground.setColor(0x40000000);
|
||||||
|
previewBackground.setCornerRadius(dp(10));
|
||||||
|
previewBackground.setStroke(dp(2), 0x60FFFFFF);
|
||||||
|
previewContainer.setBackground(previewBackground);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams previewContainerParams = new LinearLayout.LayoutParams(
|
||||||
|
dp(300),
|
||||||
|
dp(400)
|
||||||
|
);
|
||||||
|
previewContainerParams.gravity = Gravity.CENTER;
|
||||||
|
previewContainer.setLayoutParams(previewContainerParams);
|
||||||
|
|
||||||
|
// Custom player preview view
|
||||||
|
mPlayerPreview = new PlayerPreviewView(context);
|
||||||
|
mPlayerPreview.setLayoutParams(new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
));
|
||||||
|
previewContainer.addView(mPlayerPreview);
|
||||||
|
|
||||||
|
contentContainer.addView(previewContainer);
|
||||||
|
|
||||||
|
// Buttons container
|
||||||
|
LinearLayout buttonsContainer = new LinearLayout(context);
|
||||||
|
buttonsContainer.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
buttonsContainer.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams buttonsParams = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
);
|
||||||
|
buttonsParams.setMargins(0, dp(20), 0, 0);
|
||||||
|
buttonsContainer.setLayoutParams(buttonsParams);
|
||||||
|
|
||||||
|
// Reset rotation button
|
||||||
|
Button resetButton = new Button(context);
|
||||||
|
resetButton.setText("Reset Rotation");
|
||||||
|
resetButton.setTextSize(14);
|
||||||
|
ShapeDrawable buttonBackground = new ShapeDrawable();
|
||||||
|
buttonBackground.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
buttonBackground.setColor(0x80FFFFFF);
|
||||||
|
buttonBackground.setCornerRadius(dp(5));
|
||||||
|
resetButton.setBackground(buttonBackground);
|
||||||
|
resetButton.setOnClickListener(v -> {
|
||||||
|
mPlayerRotationY = 0f;
|
||||||
|
mPlayerRotationX = 0f;
|
||||||
|
});
|
||||||
|
LinearLayout.LayoutParams resetParams = new LinearLayout.LayoutParams(
|
||||||
|
dp(150),
|
||||||
|
dp(40)
|
||||||
|
);
|
||||||
|
resetParams.setMargins(0, 0, dp(10), 0);
|
||||||
|
resetButton.setLayoutParams(resetParams);
|
||||||
|
buttonsContainer.addView(resetButton);
|
||||||
|
|
||||||
|
// Spin button (auto-rotate)
|
||||||
|
Button spinButton = new Button(context);
|
||||||
|
spinButton.setText("Auto Spin");
|
||||||
|
spinButton.setTextSize(14);
|
||||||
|
ShapeDrawable spinBackground = new ShapeDrawable();
|
||||||
|
spinBackground.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
spinBackground.setColor(0x8055AAFF);
|
||||||
|
spinBackground.setCornerRadius(dp(5));
|
||||||
|
spinButton.setBackground(spinBackground);
|
||||||
|
spinButton.setOnClickListener(v -> {
|
||||||
|
mAutoSpinning = !mAutoSpinning;
|
||||||
|
spinButton.setText(mAutoSpinning ? "Stop Spin" : "Auto Spin");
|
||||||
|
if (mAutoSpinning) {
|
||||||
|
startAutoSpin();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
LinearLayout.LayoutParams spinParams = new LinearLayout.LayoutParams(
|
||||||
|
dp(150),
|
||||||
|
dp(40)
|
||||||
|
);
|
||||||
|
spinParams.setMargins(dp(10), 0, 0, 0);
|
||||||
|
spinButton.setLayoutParams(spinParams);
|
||||||
|
buttonsContainer.addView(spinButton);
|
||||||
|
|
||||||
|
contentContainer.addView(buttonsContainer);
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
Button closeButton = new Button(context);
|
||||||
|
closeButton.setText("Close");
|
||||||
|
closeButton.setTextSize(14);
|
||||||
|
ShapeDrawable closeBackground = new ShapeDrawable();
|
||||||
|
closeBackground.setShape(ShapeDrawable.RECTANGLE);
|
||||||
|
closeBackground.setColor(0x80FF5555);
|
||||||
|
closeBackground.setCornerRadius(dp(5));
|
||||||
|
closeButton.setBackground(closeBackground);
|
||||||
|
closeButton.setOnClickListener(v ->
|
||||||
|
MinecraftClient.getInstance().execute(() ->
|
||||||
|
MinecraftClient.getInstance().setScreen(null)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
LinearLayout.LayoutParams closeParams = new LinearLayout.LayoutParams(
|
||||||
|
dp(150),
|
||||||
|
dp(40)
|
||||||
|
);
|
||||||
|
closeParams.gravity = Gravity.CENTER;
|
||||||
|
closeParams.setMargins(0, dp(10), 0, dp(20));
|
||||||
|
closeButton.setLayoutParams(closeParams);
|
||||||
|
contentContainer.addView(closeButton);
|
||||||
|
|
||||||
|
root.addView(contentContainer);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startAutoSpin() {
|
||||||
|
if (mPlayerPreview != null) {
|
||||||
|
mPlayerPreview.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (activeInstance == TestScreen.this && mAutoSpinning) {
|
||||||
|
mPlayerRotationY += 2f;
|
||||||
|
if (mPlayerRotationY > 360f) mPlayerRotationY -= 360f;
|
||||||
|
mPlayerPreview.postDelayed(this, 16); // ~60 FPS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom view that renders the player entity with cosmetics.
|
||||||
|
* Handles touch events for rotation.
|
||||||
|
*/
|
||||||
|
private class PlayerPreviewView extends View {
|
||||||
|
|
||||||
|
public PlayerPreviewView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(@NotNull MotionEvent event) {
|
||||||
|
float x = event.getX();
|
||||||
|
float y = event.getY();
|
||||||
|
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
mIsDragging = true;
|
||||||
|
mLastTouchX = x;
|
||||||
|
mLastTouchY = y;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
if (mIsDragging) {
|
||||||
|
float deltaX = x - mLastTouchX;
|
||||||
|
float deltaY = y - mLastTouchY;
|
||||||
|
|
||||||
|
// Horizontal drag rotates around Y axis
|
||||||
|
mPlayerRotationY += deltaX * 0.5f;
|
||||||
|
|
||||||
|
// Vertical drag rotates around X axis (limited range)
|
||||||
|
mPlayerRotationX += deltaY * 0.3f;
|
||||||
|
mPlayerRotationX = Math.max(-30f, Math.min(30f, mPlayerRotationX));
|
||||||
|
|
||||||
|
mLastTouchX = x;
|
||||||
|
mLastTouchY = y;
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
mIsDragging = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||||
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
|
|
||||||
|
// Calculate the center position in screen coordinates for Minecraft rendering
|
||||||
|
int[] location = new int[2];
|
||||||
|
getLocationOnScreen(location);
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
float guiScale = (float) client.getWindow().getScaleFactor();
|
||||||
|
|
||||||
|
// Convert ModernUI coordinates to Minecraft screen coordinates
|
||||||
|
int viewWidth = right - left;
|
||||||
|
int viewHeight = bottom - top;
|
||||||
|
|
||||||
|
// Calculate position - the location is in window pixels, need to convert to scaled GUI coordinates
|
||||||
|
mPreviewCenterX = (int) ((location[0] + viewWidth / 2.0f) / guiScale);
|
||||||
|
mPreviewCenterY = (int) ((location[1] + viewHeight * 0.7f) / guiScale);
|
||||||
|
mPreviewSize = (int) (Math.min(viewWidth, viewHeight) / guiScale / 2.5f);
|
||||||
|
mPreviewBoundsSet = true;
|
||||||
|
|
||||||
|
System.out.println("[TestScreen] Layout: location=" + location[0] + "," + location[1] +
|
||||||
|
" size=" + viewWidth + "x" + viewHeight +
|
||||||
|
" guiScale=" + guiScale +
|
||||||
|
" preview: center=" + mPreviewCenterX + "," + mPreviewCenterY + " size=" + mPreviewSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(@NotNull Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
Paint paint = Paint.obtain();
|
||||||
|
paint.setColor(0x15FFFFFF);
|
||||||
|
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
|
||||||
|
paint.recycle();
|
||||||
|
|
||||||
|
// Keep redrawing for smooth updates
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) {
|
||||||
|
postInvalidateDelayed(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the player entity at the specified position with rotation.
|
||||||
|
* This method renders the player with all cosmetics (capes, wings, halos, hats)
|
||||||
|
* because the cosmetic feature renderers are already registered in the game.
|
||||||
|
*/
|
||||||
|
public static void renderPlayerEntity(DrawContext context, int x, int y, int size,
|
||||||
|
float rotationX, float rotationY, LivingEntity entity) {
|
||||||
|
if (entity == null) return;
|
||||||
|
|
||||||
|
// Save original rotation values
|
||||||
|
float originalBodyYaw = entity.bodyYaw;
|
||||||
|
float originalYaw = entity.getYaw();
|
||||||
|
float originalPitch = entity.getPitch();
|
||||||
|
float originalPrevHeadYaw = entity.prevHeadYaw;
|
||||||
|
float originalHeadYaw = entity.headYaw;
|
||||||
|
|
||||||
|
// Set rotation based on user input
|
||||||
|
entity.bodyYaw = 180.0F + rotationY;
|
||||||
|
entity.setYaw(180.0F + rotationY);
|
||||||
|
entity.setPitch(-rotationX);
|
||||||
|
entity.headYaw = entity.getYaw();
|
||||||
|
entity.prevHeadYaw = entity.getYaw();
|
||||||
|
|
||||||
|
// Calculate scale based on entity height
|
||||||
|
float entityScale = entity.getHeight() > 2.0F ? 2.0F / entity.getHeight() : 1.0F;
|
||||||
|
|
||||||
|
// Use InventoryScreen's drawEntity method for proper rendering
|
||||||
|
// This will automatically render all cosmetics (capes, wings, halos, hats)
|
||||||
|
// because the feature renderers are registered globally
|
||||||
|
InventoryScreen.drawEntity(
|
||||||
|
context,
|
||||||
|
x - size / 2,
|
||||||
|
y - size,
|
||||||
|
x + size / 2,
|
||||||
|
y + size / 4,
|
||||||
|
(int) (size * 0.8f * entityScale),
|
||||||
|
0.0625F,
|
||||||
|
0, 0, // mouseX, mouseY - not following mouse
|
||||||
|
entity
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore original rotation values
|
||||||
|
entity.bodyYaw = originalBodyYaw;
|
||||||
|
entity.setYaw(originalYaw);
|
||||||
|
entity.setPitch(originalPitch);
|
||||||
|
entity.prevHeadYaw = originalPrevHeadYaw;
|
||||||
|
entity.headYaw = originalHeadYaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current active TestScreen instance.
|
||||||
|
* Used by the render callback to access rotation values.
|
||||||
|
*/
|
||||||
|
public static TestScreen getActiveInstance() {
|
||||||
|
return activeInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPlayerRotationY() {
|
||||||
|
return mPlayerRotationY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPlayerRotationX() {
|
||||||
|
return mPlayerRotationX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPreviewCenterX() {
|
||||||
|
return mPreviewCenterX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPreviewCenterY() {
|
||||||
|
return mPreviewCenterY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPreviewSize() {
|
||||||
|
return mPreviewSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPreviewBoundsSet() {
|
||||||
|
return mPreviewBoundsSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dp(int value) {
|
||||||
|
Context ctx = getContext();
|
||||||
|
if (ctx == null) return value;
|
||||||
|
return (int) (value * ctx.getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPauseScreen() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldBlurBackground() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasDefaultBackground() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the render callback for player preview rendering.
|
||||||
|
* Note: Rendering is now handled by ScreenRenderMixin.
|
||||||
|
* This method is kept for backward compatibility.
|
||||||
|
*/
|
||||||
|
public static void registerRenderer() {
|
||||||
|
// Rendering is now handled by ScreenRenderMixin
|
||||||
|
// This method is kept for backward compatibility
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,411 @@
|
|||||||
|
package de.winniepat.citrus.managers;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.utils.SecurityUtils;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.session.Session;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
public class ClientRegistrationManager {
|
||||||
|
private static final String API_BASE_URL = Client.API_URL;
|
||||||
|
private static final Gson GSON = new GsonBuilder().create();
|
||||||
|
|
||||||
|
private static boolean isRegistered = false;
|
||||||
|
private static boolean isShuttingDown = false;
|
||||||
|
private static Thread heartbeatThread;
|
||||||
|
|
||||||
|
private static final Set<UUID> ONLINE_CITRUS_USERS = new HashSet<>();
|
||||||
|
private static long lastOnlineListUpdate = 0;
|
||||||
|
private static final long ONLINE_LIST_CACHE_TIME = 30000;
|
||||||
|
|
||||||
|
public static void register() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
|
Session session = client.getSession();
|
||||||
|
if (session == null) {
|
||||||
|
Client.logger.error("Cannot register: No session available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String playerName = session.getUsername();
|
||||||
|
UUID playerUUID = session.getUuidOrNull();
|
||||||
|
|
||||||
|
if (playerUUID == null) {
|
||||||
|
Client.logger.error("Cannot register: No UUID in session");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Registering as online user: " + playerName + " (" + playerUUID + ")");
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/client/register").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", playerUUID.toString());
|
||||||
|
json.addProperty("name", playerName);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
isRegistered = true;
|
||||||
|
|
||||||
|
Client.debugLog("Successfully registered as online user");
|
||||||
|
|
||||||
|
startHeartbeat();
|
||||||
|
|
||||||
|
updateOnlineUsersList();
|
||||||
|
} else {
|
||||||
|
Client.logger.error("Failed to register: HTTP {}", conn.getResponseCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Registration failed: ", e);
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void updatePlayerInfo() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null) return;
|
||||||
|
|
||||||
|
if (isRegistered) {
|
||||||
|
UUID worldUUID = client.player.getUuid();
|
||||||
|
String worldName = client.player.getName().getString();
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/client/register").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(3000);
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", worldUUID.toString());
|
||||||
|
json.addProperty("name", worldName);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
Client.debugLog("Updated player info for world: " + worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error updating player info: ", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startHeartbeat() {
|
||||||
|
if (heartbeatThread != null && heartbeatThread.isAlive()) {
|
||||||
|
heartbeatThread.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
heartbeatThread = new Thread(() -> {
|
||||||
|
while (!isShuttingDown && isRegistered) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(30000);
|
||||||
|
sendHeartbeat();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "Citrus-Heartbeat");
|
||||||
|
|
||||||
|
heartbeatThread.setDaemon(true);
|
||||||
|
heartbeatThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sendHeartbeat() {
|
||||||
|
if (!isRegistered) return;
|
||||||
|
|
||||||
|
UUID uuid = getCurrentUUID();
|
||||||
|
if (uuid == null) return;
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/client/heartbeat").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(3000);
|
||||||
|
conn.setReadTimeout(3000);
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", uuid.toString());
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn.getResponseCode() != 200) {
|
||||||
|
Client.logger.error("Heartbeat failed: HTTP {} {}", conn.getResponseCode(), conn.getResponseMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.disconnect();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Heartbeat failed: ", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unregister() {
|
||||||
|
if (!isRegistered) return;
|
||||||
|
|
||||||
|
isShuttingDown = true;
|
||||||
|
|
||||||
|
UUID uuid = getCurrentUUID();
|
||||||
|
if (uuid == null) return;
|
||||||
|
|
||||||
|
Client.debugLog("Unregistering from online users...");
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/client/unregister").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(3000);
|
||||||
|
conn.setReadTimeout(3000);
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", uuid.toString());
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Unregistered successfully");
|
||||||
|
|
||||||
|
conn.disconnect();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Unregistration failed: ", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UUID getCurrentUUID() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
|
if (client.player != null) {
|
||||||
|
return client.player.getUuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
Session session = client.getSession();
|
||||||
|
if (session != null) {
|
||||||
|
return session.getUuidOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<UUID> getOnlineCitrusUsers() {
|
||||||
|
if (System.currentTimeMillis() - lastOnlineListUpdate >= ONLINE_LIST_CACHE_TIME) {
|
||||||
|
updateOnlineUsersList();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (ONLINE_CITRUS_USERS) {
|
||||||
|
return new ArrayList<>(ONLINE_CITRUS_USERS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void updateOnlineUsersList() {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/client/online").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
String response = new String(conn.getInputStream().readAllBytes());
|
||||||
|
JsonObject[] users = GSON.fromJson(response, JsonObject[].class);
|
||||||
|
|
||||||
|
List<UUID> newList = new ArrayList<>();
|
||||||
|
if (users != null) {
|
||||||
|
for (JsonObject user : users) {
|
||||||
|
try {
|
||||||
|
UUID uuid = UUID.fromString(user.get("player_uuid").getAsString());
|
||||||
|
newList.add(uuid);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Invalid UUID in online list: {}", user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (ONLINE_CITRUS_USERS) {
|
||||||
|
ONLINE_CITRUS_USERS.clear();
|
||||||
|
ONLINE_CITRUS_USERS.addAll(newList);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastOnlineListUpdate = System.currentTimeMillis();
|
||||||
|
|
||||||
|
Client.debugLog("Updated online users list: " + newList.size() + " users");
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to update online users list: {}", String.valueOf(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<UUID> filterOnlineUsers(List<UUID> playerUUIDs) {
|
||||||
|
if (playerUUIDs.isEmpty()) return new ArrayList<>();
|
||||||
|
|
||||||
|
CompletableFuture<List<UUID>> future = CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/client/online-check").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
List<String> uuidStrings = new ArrayList<>();
|
||||||
|
for (UUID uuid : playerUUIDs) {
|
||||||
|
uuidStrings.add(uuid.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.add("uuids", GSON.toJsonTree(uuidStrings));
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes());
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
String response = new String(conn.getInputStream().readAllBytes());
|
||||||
|
String[] onlineUUIDs = GSON.fromJson(response, String[].class);
|
||||||
|
|
||||||
|
List<UUID> result = new ArrayList<>();
|
||||||
|
for (String uuidStr : onlineUUIDs) {
|
||||||
|
try {
|
||||||
|
result.add(UUID.fromString(uuidStr));
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Invalid UUID in filter response: {}", uuidStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to filter online users: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArrayList<>();
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
|
||||||
|
try {
|
||||||
|
return future.get(3, TimeUnit.SECONDS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Timeout filtering online users: ", e);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPlayerOnlineWithCitrus(UUID uuid) {
|
||||||
|
if (uuid == null) return false;
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null && client.player.getUuid().equals(uuid)) {
|
||||||
|
return isRegistered();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (System.currentTimeMillis() - lastOnlineListUpdate >= ONLINE_LIST_CACHE_TIME) {
|
||||||
|
updateOnlineUsersList();
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (ONLINE_CITRUS_USERS) {
|
||||||
|
return ONLINE_CITRUS_USERS.contains(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<UUID> getCitrusUsersInWorld() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.world == null) return new ArrayList<>();
|
||||||
|
|
||||||
|
List<UUID> result = new ArrayList<>();
|
||||||
|
|
||||||
|
for (net.minecraft.entity.player.PlayerEntity player : client.world.getPlayers()) {
|
||||||
|
if (isPlayerOnlineWithCitrus(player.getUuid())) {
|
||||||
|
result.add(player.getUuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean shouldSyncWithPlayer(UUID uuid) {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null && client.player.getUuid().equals(uuid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isPlayerOnlineWithCitrus(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void forceUpdateOnlineUsers() {
|
||||||
|
updateOnlineUsersList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isRegistered() {
|
||||||
|
return isRegistered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCurrentPlayerName() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
|
if (client.player != null) {
|
||||||
|
return client.player.getName().getString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Session session = client.getSession();
|
||||||
|
if (session != null) {
|
||||||
|
return session.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package de.winniepat.citrus.managers;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.ModConfig;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ConfigManager {
|
||||||
|
private static final Gson GSON = new GsonBuilder()
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.create();
|
||||||
|
private static final File CONFIG_FILE = new File(Client.CONFIG_DIR, "settings.json");
|
||||||
|
|
||||||
|
public static ModConfig config = new ModConfig();
|
||||||
|
|
||||||
|
public static void load() {
|
||||||
|
if (!CONFIG_FILE.exists()) {
|
||||||
|
Client.debugLog("Config file not found, creating default");
|
||||||
|
save();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileReader reader = new FileReader(CONFIG_FILE)) {
|
||||||
|
config = GSON.fromJson(reader, ModConfig.class);
|
||||||
|
|
||||||
|
if (config.betaConfig == null) {
|
||||||
|
config.betaConfig = new ModConfig.BetaConfig();
|
||||||
|
Client.debugLog("Initialized new beta config for existing user");
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Config loaded successfully");
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
Client.logger.error("Error while loading config", e);
|
||||||
|
config = new ModConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save() {
|
||||||
|
if (!Client.CONFIG_DIR.exists()) {
|
||||||
|
Client.CONFIG_DIR.mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
try (FileWriter writer = new FileWriter(CONFIG_FILE)) {
|
||||||
|
GSON.toJson(config, writer);
|
||||||
|
Client.logger.debug("Config saved successfully");
|
||||||
|
} catch (IOException e) {
|
||||||
|
Client.logger.error("Error while saving config", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ModConfig.BetaConfig getBetaConfig() {
|
||||||
|
return config.betaConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void updateBetaConfig(ModConfig.BetaConfig newBetaConfig) {
|
||||||
|
config.betaConfig = newBetaConfig;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveBetaToken(String token, long expiresAt) {
|
||||||
|
config.betaConfig.verificationToken = token;
|
||||||
|
config.betaConfig.tokenExpiresAt = expiresAt;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasBetaKey() {
|
||||||
|
return config.betaConfig != null &&
|
||||||
|
config.betaConfig.betaKey != null &&
|
||||||
|
!config.betaConfig.betaKey.trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasValidToken() {
|
||||||
|
if (config.betaConfig == null || config.betaConfig.verificationToken == null || config.betaConfig.verificationToken.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return System.currentTimeMillis() < (config.betaConfig.tokenExpiresAt - (5 * 60 * 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
package de.winniepat.citrus.managers;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.utils.SecurityUtils;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.network.ServerInfo;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class FriendManager {
|
||||||
|
private static final String API_BASE_URL = Client.API_URL;
|
||||||
|
private static final Gson GSON = new GsonBuilder().create();
|
||||||
|
|
||||||
|
private static final List<Friend> FRIENDS = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
private static final List<FriendRequest> INCOMING_REQUESTS = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
|
private static long lastRefresh = 0;
|
||||||
|
private static final long REFRESH_INTERVAL = 60000;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCurrentServerIp() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.getCurrentServerEntry() != null) {
|
||||||
|
ServerInfo serverInfo = client.getCurrentServerEntry();
|
||||||
|
return serverInfo.address;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Void> updateServerStatus() {
|
||||||
|
UUID uuid = getCurrentUuid();
|
||||||
|
if (uuid == null) return CompletableFuture.completedFuture(null);
|
||||||
|
|
||||||
|
String serverIp = getCurrentServerIp();
|
||||||
|
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", uuid.toString());
|
||||||
|
json.addProperty("serverIp", serverIp != null ? serverIp : "");
|
||||||
|
|
||||||
|
URL url = new URI(API_BASE_URL + "/friends/server").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
if (responseCode == 200) {
|
||||||
|
Client.debugLog("Updated server status: " + (serverIp != null ? serverIp : "none"));
|
||||||
|
}
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to update server status", e);
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<Void> refresh() {
|
||||||
|
UUID uuid = getCurrentUuid();
|
||||||
|
if (uuid == null) return CompletableFuture.completedFuture(null);
|
||||||
|
|
||||||
|
lastRefresh = System.currentTimeMillis();
|
||||||
|
|
||||||
|
return CompletableFuture.allOf(fetchFriends(uuid), fetchIncomingRequests(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UUID getCurrentUuid() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) return client.player.getUuid();
|
||||||
|
if (client.getSession() != null) return client.getSession().getUuidOrNull();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<Void> fetchFriends(UUID uuid) {
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/friends/list/" + uuid.toString()).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
String response = new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
Friend[] friendsArray = GSON.fromJson(response, Friend[].class);
|
||||||
|
|
||||||
|
if (friendsArray != null) {
|
||||||
|
synchronized (FRIENDS) {
|
||||||
|
FRIENDS.clear();
|
||||||
|
Collections.addAll(FRIENDS, friendsArray);
|
||||||
|
}
|
||||||
|
Client.debugLog("Fetched " + FRIENDS.size() + " friends");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to fetch friends for {}", uuid, e);
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<Void> fetchIncomingRequests(UUID uuid) {
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/friends/requests/" + uuid.toString()).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
String response = new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
FriendRequest[] requests = GSON.fromJson(response, FriendRequest[].class);
|
||||||
|
|
||||||
|
if (requests != null) {
|
||||||
|
synchronized (INCOMING_REQUESTS) {
|
||||||
|
INCOMING_REQUESTS.clear();
|
||||||
|
Collections.addAll(INCOMING_REQUESTS, requests);
|
||||||
|
}
|
||||||
|
Client.debugLog("Fetched " + INCOMING_REQUESTS.size() + " incoming requests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to fetch friend requests for {}", uuid, e);
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<String> postJson(String endpoint, JsonObject json) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + endpoint).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(json).getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
if (responseCode == 200) {
|
||||||
|
conn.disconnect();
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
String errorMsg = "Server error (" + responseCode + ")";
|
||||||
|
try (InputStream es = (responseCode >= 400 ? conn.getErrorStream() : conn.getInputStream())) {
|
||||||
|
if (es != null) {
|
||||||
|
String response = new String(es.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
JsonObject obj = GSON.fromJson(response, JsonObject.class);
|
||||||
|
if (obj != null && obj.has("error")) {
|
||||||
|
errorMsg = obj.get("error").getAsString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Client.debugLog("Friend Action Failed: " + errorMsg);
|
||||||
|
conn.disconnect();
|
||||||
|
return errorMsg;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.debugLog("Friend Action Exception: " + e.getMessage());
|
||||||
|
return "Connection failed: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<String> sendRequest(String friendName) {
|
||||||
|
UUID uuid = getCurrentUuid();
|
||||||
|
if (uuid == null) return CompletableFuture.completedFuture("Not logged in");
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", uuid.toString());
|
||||||
|
json.addProperty("friendName", friendName);
|
||||||
|
|
||||||
|
return postJson("/friends/request", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<String> acceptRequest(UUID friendUuid) {
|
||||||
|
UUID uuid = getCurrentUuid();
|
||||||
|
if (uuid == null) return CompletableFuture.completedFuture("Not logged in");
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", uuid.toString());
|
||||||
|
json.addProperty("friendUuid", friendUuid.toString());
|
||||||
|
|
||||||
|
return postJson("/friends/accept", json).thenCompose(error -> {
|
||||||
|
if (error == null) return refresh().thenApply(v -> null);
|
||||||
|
return CompletableFuture.completedFuture(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<String> removeFriend(UUID friendUuid) {
|
||||||
|
UUID uuid = getCurrentUuid();
|
||||||
|
if (uuid == null) return CompletableFuture.completedFuture("Not logged in");
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("uuid", uuid.toString());
|
||||||
|
json.addProperty("friendUuid", friendUuid.toString());
|
||||||
|
|
||||||
|
return postJson("/friends/remove", json).thenCompose(error -> {
|
||||||
|
if (error == null) return refresh().thenApply(v -> null);
|
||||||
|
return CompletableFuture.completedFuture(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Friend> getFriends() {
|
||||||
|
if (System.currentTimeMillis() - lastRefresh > REFRESH_INTERVAL) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
synchronized (FRIENDS) {
|
||||||
|
return new ArrayList<>(FRIENDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<FriendRequest> getIncomingRequests() {
|
||||||
|
synchronized (INCOMING_REQUESTS) {
|
||||||
|
return new ArrayList<>(INCOMING_REQUESTS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Friend {
|
||||||
|
public String player_uuid;
|
||||||
|
public String player_name;
|
||||||
|
public int is_online;
|
||||||
|
public String server_ip;
|
||||||
|
|
||||||
|
public UUID getUuid() {
|
||||||
|
return UUID.fromString(player_uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOnline() {
|
||||||
|
return this.is_online == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServerDisplay() {
|
||||||
|
if (!isOnline() || server_ip == null || server_ip.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String display = server_ip;
|
||||||
|
if (display.endsWith(":25565")) {
|
||||||
|
display = display.substring(0, display.length() - 6);
|
||||||
|
}
|
||||||
|
return display;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FriendRequest {
|
||||||
|
public String player_uuid;
|
||||||
|
public String player_name;
|
||||||
|
|
||||||
|
public UUID getUuid() {
|
||||||
|
return UUID.fromString(player_uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package de.winniepat.citrus.managers;
|
||||||
|
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
public class FullbrightManager {
|
||||||
|
private static final double BRIGHT_VALUE = 15.0;
|
||||||
|
private static final double NORMAL_VALUE = 1.0;
|
||||||
|
|
||||||
|
public static void toggle(MinecraftClient client) {
|
||||||
|
ConfigManager.config.fullbrightEnabled = !ConfigManager.config.fullbrightEnabled;
|
||||||
|
ConfigManager.save();
|
||||||
|
|
||||||
|
apply(client);
|
||||||
|
|
||||||
|
if (client.player != null) {
|
||||||
|
String status = ConfigManager.config.fullbrightEnabled ? "§aON" : "§cOFF";
|
||||||
|
client.player.sendMessage(Text.literal("Fullbright: " + status), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void apply(MinecraftClient client) {
|
||||||
|
if (client == null || client.options == null) return;
|
||||||
|
|
||||||
|
double target = ConfigManager.config.fullbrightEnabled ? BRIGHT_VALUE : NORMAL_VALUE;
|
||||||
|
client.options.getGamma().setValue(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package de.winniepat.citrus.managers;
|
||||||
|
|
||||||
|
import com.google.gson.*;
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.utils.SecurityUtils;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class ProfileManager {
|
||||||
|
private static final String API_BASE_URL = Client.API_URL;
|
||||||
|
private static final Gson GSON = new GsonBuilder().create();
|
||||||
|
|
||||||
|
private static final Map<UUID, PlayerProfile> PROFILE_CACHE = new ConcurrentHashMap<>();
|
||||||
|
private static final long CACHE_EXPIRY = 300000;
|
||||||
|
private static final Map<UUID, Long> CACHE_TIMESTAMPS = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public static class PlayerProfile {
|
||||||
|
public String uuid;
|
||||||
|
public String playerName;
|
||||||
|
public String bio;
|
||||||
|
public String discord;
|
||||||
|
public String twitter;
|
||||||
|
public String youtube;
|
||||||
|
public String twitch;
|
||||||
|
public String instagram;
|
||||||
|
public String github;
|
||||||
|
public String website;
|
||||||
|
public String createdAt;
|
||||||
|
public String updatedAt;
|
||||||
|
|
||||||
|
public PlayerProfile() {}
|
||||||
|
|
||||||
|
public PlayerProfile(String uuid, String playerName) {
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.playerName = playerName;
|
||||||
|
this.bio = "";
|
||||||
|
this.discord = "";
|
||||||
|
this.twitter = "";
|
||||||
|
this.youtube = "";
|
||||||
|
this.twitch = "";
|
||||||
|
this.instagram = "";
|
||||||
|
this.github = "";
|
||||||
|
this.website = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasSocialLinks() {
|
||||||
|
return !isEmpty(discord) || !isEmpty(twitter) || !isEmpty(youtube)
|
||||||
|
|| !isEmpty(twitch) || !isEmpty(instagram) || !isEmpty(github) || !isEmpty(website);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isEmpty(String s) {
|
||||||
|
return s == null || s.trim().isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SearchResult {
|
||||||
|
public String uuid;
|
||||||
|
public String playerName;
|
||||||
|
public String bio;
|
||||||
|
public boolean hasSocials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<PlayerProfile> getProfile(UUID uuid) {
|
||||||
|
if (PROFILE_CACHE.containsKey(uuid)) {
|
||||||
|
Long timestamp = CACHE_TIMESTAMPS.get(uuid);
|
||||||
|
if (timestamp != null && System.currentTimeMillis() - timestamp < CACHE_EXPIRY) {
|
||||||
|
return CompletableFuture.completedFuture(PROFILE_CACHE.get(uuid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(API_BASE_URL + "/profiles/" + uuid.toString()).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
String response = new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
JsonObject json = GSON.fromJson(response, JsonObject.class);
|
||||||
|
|
||||||
|
if (json.has("success") && json.get("success").getAsBoolean()) {
|
||||||
|
PlayerProfile profile = GSON.fromJson(json.get("profile"), PlayerProfile.class);
|
||||||
|
PROFILE_CACHE.put(uuid, profile);
|
||||||
|
CACHE_TIMESTAMPS.put(uuid, System.currentTimeMillis());
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
} else if (conn.getResponseCode() == 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to fetch profile for {}", uuid, e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<PlayerProfile> getOwnProfile() {
|
||||||
|
UUID uuid = getCurrentUuid();
|
||||||
|
if (uuid == null) return CompletableFuture.completedFuture(null);
|
||||||
|
return getProfile(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<String> updateProfile(PlayerProfile profile) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
UUID uuid = getCurrentUuid();
|
||||||
|
if (uuid == null) return "Not logged in";
|
||||||
|
|
||||||
|
URL url = new URI(API_BASE_URL + "/profiles/update").toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
JsonObject payload = new JsonObject();
|
||||||
|
payload.addProperty("uuid", uuid.toString());
|
||||||
|
payload.addProperty("playerName", MinecraftClient.getInstance().getSession().getUsername());
|
||||||
|
payload.addProperty("bio", profile.bio);
|
||||||
|
payload.addProperty("discord", profile.discord);
|
||||||
|
payload.addProperty("twitter", profile.twitter);
|
||||||
|
payload.addProperty("youtube", profile.youtube);
|
||||||
|
payload.addProperty("twitch", profile.twitch);
|
||||||
|
payload.addProperty("instagram", profile.instagram);
|
||||||
|
payload.addProperty("github", profile.github);
|
||||||
|
payload.addProperty("website", profile.website);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(GSON.toJson(payload).getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
String response = new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
JsonObject json = GSON.fromJson(response, JsonObject.class);
|
||||||
|
|
||||||
|
if (json.has("success") && json.get("success").getAsBoolean()) {
|
||||||
|
PROFILE_CACHE.put(uuid, profile);
|
||||||
|
CACHE_TIMESTAMPS.put(uuid, System.currentTimeMillis());
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return json.has("error") ? json.get("error").getAsString() : "Unknown error";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "Server error: " + conn.getResponseCode();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to update profile", e);
|
||||||
|
return "Failed to update profile: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CompletableFuture<List<SearchResult>> searchProfiles(String query) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
List<SearchResult> results = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
String encodedQuery = java.net.URLEncoder.encode(query, StandardCharsets.UTF_8);
|
||||||
|
URL url = new URI(API_BASE_URL + "/profiles/search?q=" + encodedQuery).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
conn.setConnectTimeout(5000);
|
||||||
|
conn.setReadTimeout(5000);
|
||||||
|
|
||||||
|
if (conn.getResponseCode() == 200) {
|
||||||
|
String response = new String(conn.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
JsonObject json = GSON.fromJson(response, JsonObject.class);
|
||||||
|
|
||||||
|
if (json.has("success") && json.get("success").getAsBoolean()) {
|
||||||
|
JsonArray array = json.getAsJsonArray("results");
|
||||||
|
for (JsonElement element : array) {
|
||||||
|
SearchResult result = GSON.fromJson(element, SearchResult.class);
|
||||||
|
results.add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.disconnect();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to search profiles", e);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void invalidateCache(UUID uuid) {
|
||||||
|
PROFILE_CACHE.remove(uuid);
|
||||||
|
CACHE_TIMESTAMPS.remove(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UUID getCurrentUuid() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) return client.player.getUuid();
|
||||||
|
if (client.getSession() != null) return client.getSession().getUuidOrNull();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package de.winniepat.citrus.managers;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.utils.SecurityUtils;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class RankManager {
|
||||||
|
public static final ConcurrentHashMap<UUID, String> rankCache = new ConcurrentHashMap<>();
|
||||||
|
private static final HttpClient client = HttpClient.newHttpClient();
|
||||||
|
|
||||||
|
public static String getRankIcon(UUID uuid) {
|
||||||
|
String rank = rankCache.get(uuid);
|
||||||
|
|
||||||
|
if (rank == null) {
|
||||||
|
rankCache.put(uuid, "loading");
|
||||||
|
fetchRank(uuid);
|
||||||
|
return " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rank.equals("loading")) return " ";
|
||||||
|
|
||||||
|
return switch (rank) {
|
||||||
|
case "owner" -> "\uE001";
|
||||||
|
case "admin" -> "\uE002";
|
||||||
|
case "vip" -> "\uE003";
|
||||||
|
case "dev" -> "\uE004";
|
||||||
|
case "bughunter" -> "\uE005";
|
||||||
|
case "default" -> "\uE099";
|
||||||
|
default -> "";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getRank(UUID uuid) {
|
||||||
|
String rank = rankCache.get(uuid);
|
||||||
|
if (rank == null) {
|
||||||
|
rankCache.put(uuid, "loading");
|
||||||
|
fetchRank(uuid);
|
||||||
|
return "loading";
|
||||||
|
}
|
||||||
|
return rank;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fetchRank(UUID uuid) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(Client.API_URL + "/rank/" + uuid.toString()))
|
||||||
|
.header("x-daily-key", SecurityUtils.getDailyKey())
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
|
||||||
|
if (response.statusCode() == 200) {
|
||||||
|
String rawRank = response.body().trim().toLowerCase();
|
||||||
|
|
||||||
|
if (rawRank.equals(rankCache.get(uuid))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Client.debugLog("Received Rank Content: '" + rawRank + "' (Length: " + rawRank.length() + ")");
|
||||||
|
|
||||||
|
rankCache.put(uuid, rawRank);
|
||||||
|
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
var world = MinecraftClient.getInstance().world;
|
||||||
|
if (world != null) {
|
||||||
|
var player = world.getPlayerByUuid(uuid);
|
||||||
|
if (player != null) {
|
||||||
|
player.setCustomNameVisible(player.isCustomNameVisible());
|
||||||
|
Client.debugLog("Refreshed nametag for: " + player.getName().getString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
rankCache.put(uuid, "none");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.debugLog("Failed to fetch rank: " + e.getMessage());
|
||||||
|
rankCache.put(uuid, "none");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void refreshRankIfChanged() {
|
||||||
|
if (MinecraftClient.getInstance().getNetworkHandler() == null) return;
|
||||||
|
|
||||||
|
MinecraftClient.getInstance().getNetworkHandler().getPlayerList().forEach(playerListEntry -> {
|
||||||
|
fetchRank(playerListEntry.getProfile().getId());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
package de.winniepat.citrus.managers;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.texture.NativeImage;
|
||||||
|
import net.minecraft.client.texture.NativeImageBackedTexture;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import net.minecraft.util.Util;
|
||||||
|
import org.w3c.dom.*;
|
||||||
|
|
||||||
|
import javax.xml.parsers.*;
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class WallpaperManager {
|
||||||
|
private static final String CDN_WALLPAPERS_URL = Client.CDN_URL + "/wallpapers";
|
||||||
|
private static final String MOD_ID = "citrus";
|
||||||
|
private static final String CACHE_DIR_NAME = "citrus/wallpapers";
|
||||||
|
|
||||||
|
private static final Map<String, Identifier> WALLPAPER_CACHE = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<String, byte[]> WALLPAPER_DATA_CACHE = new ConcurrentHashMap<>();
|
||||||
|
private static final Set<String> LOADING_WALLPAPERS = ConcurrentHashMap.newKeySet();
|
||||||
|
private static final Set<String> FAILED_WALLPAPERS = ConcurrentHashMap.newKeySet();
|
||||||
|
private static final List<String> AVAILABLE_WALLPAPERS = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
private static boolean wallpapersListFetched = false;
|
||||||
|
private static boolean isInitialized = false;
|
||||||
|
private static final List<Runnable> onWallpapersLoadedCallbacks = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
private static Path cacheDir;
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
if (isInitialized) return;
|
||||||
|
isInitialized = true;
|
||||||
|
|
||||||
|
initCacheDirectory();
|
||||||
|
|
||||||
|
fetchWallpaperListFromCDN();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void initCacheDirectory() {
|
||||||
|
try {
|
||||||
|
Path runDir = MinecraftClient.getInstance().runDirectory.toPath();
|
||||||
|
cacheDir = runDir.resolve(CACHE_DIR_NAME);
|
||||||
|
Files.createDirectories(cacheDir);
|
||||||
|
Client.debugLog("Wallpaper cache directory: " + cacheDir);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to create wallpaper cache directory", e);
|
||||||
|
cacheDir = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isWallpaperCachedOnDisk(String wallpaperName) {
|
||||||
|
if (cacheDir == null) return false;
|
||||||
|
Path cachedFile = cacheDir.resolve(wallpaperName);
|
||||||
|
return Files.exists(cachedFile) && Files.isRegularFile(cachedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] loadWallpaperFromDisk(String wallpaperName) {
|
||||||
|
if (cacheDir == null) return null;
|
||||||
|
try {
|
||||||
|
Path cachedFile = cacheDir.resolve(wallpaperName);
|
||||||
|
if (Files.exists(cachedFile)) {
|
||||||
|
byte[] data = Files.readAllBytes(cachedFile);
|
||||||
|
Client.debugLog("Loaded wallpaper from disk cache: " + wallpaperName + " (" + data.length + " bytes)");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to load wallpaper from disk cache: {}", wallpaperName, e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveWallpaperToDisk(String wallpaperName, byte[] data) {
|
||||||
|
if (cacheDir == null || data == null || data.length == 0) return;
|
||||||
|
try {
|
||||||
|
Path cachedFile = cacheDir.resolve(wallpaperName);
|
||||||
|
Files.write(cachedFile, data);
|
||||||
|
Client.debugLog("Saved wallpaper to disk cache: " + wallpaperName + " (" + data.length + " bytes)");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to save wallpaper to disk cache: {}", wallpaperName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void fetchWallpaperListFromCDN() {
|
||||||
|
Client.debugLog("Fetching wallpaper list from CDN...");
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL url = new URI(CDN_WALLPAPERS_URL).toURL();
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setConnectTimeout(10000);
|
||||||
|
connection.setReadTimeout(10000);
|
||||||
|
|
||||||
|
if (connection.getResponseCode() == 200) {
|
||||||
|
try (InputStream stream = connection.getInputStream()) {
|
||||||
|
byte[] xmlData = stream.readAllBytes();
|
||||||
|
parseWallpaperListFromXML(xmlData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Client.logger.error("Failed to fetch wallpaper list. HTTP {}", connection.getResponseCode());
|
||||||
|
loadDefaultWallpapers();
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.disconnect();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error fetching wallpaper list", e);
|
||||||
|
loadDefaultWallpapers();
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void parseWallpaperListFromXML(byte[] xmlData) {
|
||||||
|
try {
|
||||||
|
List<String> discoveredWallpapers = new ArrayList<>();
|
||||||
|
|
||||||
|
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||||
|
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||||
|
Document doc = builder.parse(new ByteArrayInputStream(xmlData));
|
||||||
|
|
||||||
|
NodeList keyNodes = doc.getElementsByTagName("Key");
|
||||||
|
|
||||||
|
for (int i = 0; i < keyNodes.getLength(); i++) {
|
||||||
|
Node node = keyNodes.item(i);
|
||||||
|
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||||
|
String keyValue = node.getTextContent().trim();
|
||||||
|
|
||||||
|
if (keyValue.toLowerCase().endsWith(".png")) {
|
||||||
|
String wallpaperName = keyValue;
|
||||||
|
discoveredWallpapers.add(wallpaperName);
|
||||||
|
|
||||||
|
Client.debugLog("Found wallpaper: " + wallpaperName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (discoveredWallpapers.isEmpty()) {
|
||||||
|
Client.debugLog("No wallpapers found in CDN, using defaults");
|
||||||
|
loadDefaultWallpapers();
|
||||||
|
} else {
|
||||||
|
AVAILABLE_WALLPAPERS.clear();
|
||||||
|
AVAILABLE_WALLPAPERS.addAll(discoveredWallpapers);
|
||||||
|
wallpapersListFetched = true;
|
||||||
|
Client.debugLog("Loaded " + discoveredWallpapers.size() + " wallpapers from CDN");
|
||||||
|
|
||||||
|
for (Runnable callback : onWallpapersLoadedCallbacks) {
|
||||||
|
try {
|
||||||
|
callback.run();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error in wallpaper loaded callback", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error parsing wallpaper list XML", e);
|
||||||
|
loadDefaultWallpapers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void loadDefaultWallpapers() {
|
||||||
|
AVAILABLE_WALLPAPERS.clear();
|
||||||
|
AVAILABLE_WALLPAPERS.add("default.png");
|
||||||
|
wallpapersListFetched = true;
|
||||||
|
|
||||||
|
for (Runnable callback : onWallpapersLoadedCallbacks) {
|
||||||
|
try {
|
||||||
|
callback.run();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error in wallpaper loaded callback", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onWallpapersLoaded(Runnable callback) {
|
||||||
|
if (wallpapersListFetched) {
|
||||||
|
callback.run();
|
||||||
|
} else {
|
||||||
|
onWallpapersLoadedCallbacks.add(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<String> getAvailableWallpapers() {
|
||||||
|
return new ArrayList<>(AVAILABLE_WALLPAPERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isWallpapersListLoaded() {
|
||||||
|
return wallpapersListFetched;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Identifier getWallpaperTexture(String wallpaperName) {
|
||||||
|
return WALLPAPER_CACHE.get(wallpaperName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isWallpaperLoaded(String wallpaperName) {
|
||||||
|
return WALLPAPER_CACHE.containsKey(wallpaperName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isWallpaperLoading(String wallpaperName) {
|
||||||
|
return LOADING_WALLPAPERS.contains(wallpaperName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasWallpaperFailed(String wallpaperName) {
|
||||||
|
return FAILED_WALLPAPERS.contains(wallpaperName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getWallpaperImageData(String wallpaperName) {
|
||||||
|
return WALLPAPER_DATA_CACHE.get(wallpaperName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadWallpaperWithData(String wallpaperName, Consumer<byte[]> onLoaded) {
|
||||||
|
if (WALLPAPER_DATA_CACHE.containsKey(wallpaperName)) {
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(WALLPAPER_DATA_CACHE.get(wallpaperName));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWallpaperCachedOnDisk(wallpaperName)) {
|
||||||
|
byte[] cachedData = loadWallpaperFromDisk(wallpaperName);
|
||||||
|
if (cachedData != null && cachedData.length > 0) {
|
||||||
|
WALLPAPER_DATA_CACHE.put(wallpaperName, cachedData);
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(cachedData);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FAILED_WALLPAPERS.contains(wallpaperName)) {
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LOADING_WALLPAPERS.contains(wallpaperName)) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
while (LOADING_WALLPAPERS.contains(wallpaperName)) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(WALLPAPER_DATA_CACHE.get(wallpaperName));
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOADING_WALLPAPERS.add(wallpaperName);
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
byte[] imageData = downloadWallpaperImage(wallpaperName);
|
||||||
|
if (imageData.length == 0) {
|
||||||
|
Client.logger.error("Empty image data for wallpaper {}", wallpaperName);
|
||||||
|
markWallpaperAsFailed(wallpaperName);
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(null);
|
||||||
|
}
|
||||||
|
LOADING_WALLPAPERS.remove(wallpaperName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WALLPAPER_DATA_CACHE.put(wallpaperName, imageData);
|
||||||
|
FAILED_WALLPAPERS.remove(wallpaperName);
|
||||||
|
|
||||||
|
saveWallpaperToDisk(wallpaperName, imageData);
|
||||||
|
|
||||||
|
Client.debugLog("Successfully loaded wallpaper data: " + wallpaperName + " (" + imageData.length + " bytes)");
|
||||||
|
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(imageData);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to download wallpaper {}", wallpaperName, e);
|
||||||
|
markWallpaperAsFailed(wallpaperName);
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(null);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
LOADING_WALLPAPERS.remove(wallpaperName);
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadWallpaper(String wallpaperName, Consumer<Identifier> onLoaded) {
|
||||||
|
if (WALLPAPER_CACHE.containsKey(wallpaperName)) {
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(WALLPAPER_CACHE.get(wallpaperName));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FAILED_WALLPAPERS.contains(wallpaperName)) {
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(null);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LOADING_WALLPAPERS.contains(wallpaperName)) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
while (LOADING_WALLPAPERS.contains(wallpaperName)) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (onLoaded != null) {
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
onLoaded.accept(WALLPAPER_CACHE.get(wallpaperName));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOADING_WALLPAPERS.add(wallpaperName);
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
byte[] imageData = downloadWallpaperImage(wallpaperName);
|
||||||
|
if (imageData.length == 0) {
|
||||||
|
Client.logger.error("Empty image data for wallpaper {}", wallpaperName);
|
||||||
|
markWallpaperAsFailed(wallpaperName);
|
||||||
|
if (onLoaded != null) {
|
||||||
|
MinecraftClient.getInstance().execute(() -> onLoaded.accept(null));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
saveWallpaperToDisk(wallpaperName, imageData);
|
||||||
|
|
||||||
|
NativeImage nativeImage = NativeImage.read(new ByteArrayInputStream(imageData));
|
||||||
|
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
try {
|
||||||
|
NativeImageBackedTexture texture = new NativeImageBackedTexture(nativeImage);
|
||||||
|
String safeName = wallpaperName.replace(".png", "").replaceAll("[^a-zA-Z0-9_]", "_");
|
||||||
|
Identifier textureId = Identifier.of(MOD_ID, "wallpapers/" + safeName);
|
||||||
|
MinecraftClient.getInstance().getTextureManager().registerTexture(textureId, texture);
|
||||||
|
WALLPAPER_CACHE.put(wallpaperName, textureId);
|
||||||
|
FAILED_WALLPAPERS.remove(wallpaperName);
|
||||||
|
Client.debugLog("Successfully loaded wallpaper: " + wallpaperName + " (" + nativeImage.getWidth() + "x" + nativeImage.getHeight() + ")");
|
||||||
|
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(textureId);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to create texture for wallpaper {}", wallpaperName, e);
|
||||||
|
nativeImage.close();
|
||||||
|
markWallpaperAsFailed(wallpaperName);
|
||||||
|
if (onLoaded != null) {
|
||||||
|
onLoaded.accept(null);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
LOADING_WALLPAPERS.remove(wallpaperName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to download wallpaper {}", wallpaperName, e);
|
||||||
|
markWallpaperAsFailed(wallpaperName);
|
||||||
|
LOADING_WALLPAPERS.remove(wallpaperName);
|
||||||
|
if (onLoaded != null) {
|
||||||
|
MinecraftClient.getInstance().execute(() -> onLoaded.accept(null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] downloadWallpaperImage(String wallpaperName) throws Exception {
|
||||||
|
String downloadUrl = CDN_WALLPAPERS_URL + "/" + wallpaperName;
|
||||||
|
Client.debugLog("Downloading wallpaper from: " + downloadUrl);
|
||||||
|
|
||||||
|
URL url = new URI(downloadUrl).toURL();
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setConnectTimeout(30000);
|
||||||
|
connection.setReadTimeout(30000);
|
||||||
|
|
||||||
|
if (connection.getResponseCode() != 200) {
|
||||||
|
throw new IOException("Failed to download wallpaper: HTTP " + connection.getResponseCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
try (InputStream stream = connection.getInputStream()) {
|
||||||
|
return stream.readAllBytes();
|
||||||
|
} finally {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void markWallpaperAsFailed(String wallpaperName) {
|
||||||
|
FAILED_WALLPAPERS.add(wallpaperName);
|
||||||
|
LOADING_WALLPAPERS.remove(wallpaperName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void preloadWallpaper(String wallpaperName) {
|
||||||
|
loadWallpaper(wallpaperName, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package de.winniepat.citrus.managers;
|
||||||
|
|
||||||
|
public class ZoomManager {
|
||||||
|
private static boolean zooming = false;
|
||||||
|
private static double zoomMultiplier = 4.0;
|
||||||
|
|
||||||
|
public static void setZooming(boolean value) { zooming = value; }
|
||||||
|
public static boolean isZooming() { return zooming; }
|
||||||
|
public static double getZoomMultiplier() { return zoomMultiplier; }
|
||||||
|
|
||||||
|
public static void changeZoom(double amount) {
|
||||||
|
zoomMultiplier = Math.max(1.0, Math.min(40.0, zoomMultiplier + amount));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.CapeHandler;
|
||||||
|
import de.winniepat.citrus.managers.RankManager;
|
||||||
|
import net.minecraft.client.network.*;
|
||||||
|
import net.minecraft.client.util.*;
|
||||||
|
import org.spongepowered.asm.mixin.*;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Mixin(AbstractClientPlayerEntity.class)
|
||||||
|
public class AbstractClientPlayerEntityMixin {
|
||||||
|
|
||||||
|
@Shadow
|
||||||
|
private PlayerListEntry playerListEntry;
|
||||||
|
|
||||||
|
@ModifyReturnValue(method = "getSkinTextures", at = @At("RETURN"))
|
||||||
|
public SkinTextures handleCapes(SkinTextures original) throws IOException {
|
||||||
|
AbstractClientPlayerEntity player = (AbstractClientPlayerEntity) (Object) this;
|
||||||
|
RankManager.getRankIcon(player.getUuid());
|
||||||
|
|
||||||
|
if (playerListEntry == null) {
|
||||||
|
return DefaultSkinHelper.getSkinTextures(((AbstractClientPlayerEntity) (Object) this).getUuid());
|
||||||
|
} else {
|
||||||
|
return CapeHandler.getCapes(original, (AbstractClientPlayerEntity) (Object) this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import net.minecraft.client.render.RenderLayer;
|
||||||
|
import net.minecraft.client.render.entity.feature.CapeFeatureRenderer;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
|
|
||||||
|
@Mixin(CapeFeatureRenderer.class)
|
||||||
|
public class CapeTransparencyMixin {
|
||||||
|
|
||||||
|
@Redirect(
|
||||||
|
method = "render*",
|
||||||
|
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/RenderLayer;getEntitySolid(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/RenderLayer;")
|
||||||
|
)
|
||||||
|
private RenderLayer swapToTranslucent(Identifier texture) {
|
||||||
|
return RenderLayer.getEntityTranslucent(texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.world.CreateWorldScreen;
|
||||||
|
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.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(CreateWorldScreen.class)
|
||||||
|
public class CreateWorldScreenMixin {
|
||||||
|
@Inject(method = "render", at = @At("HEAD"))
|
||||||
|
private void onRender(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
|
||||||
|
int screenWidth = context.getScaledWindowWidth();
|
||||||
|
int screenHeight = context.getScaledWindowHeight();
|
||||||
|
|
||||||
|
context.drawTexture(
|
||||||
|
Identifier.of("citrus", "textures/startscreen/wallpaper.png"),
|
||||||
|
0, 0,
|
||||||
|
0.0f, 0.0f,
|
||||||
|
screenWidth, screenHeight,
|
||||||
|
screenWidth, screenHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.managers.ZoomManager;
|
||||||
|
import net.minecraft.client.render.*;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(GameRenderer.class)
|
||||||
|
public class GameRendererMixin {
|
||||||
|
@Inject(method = "getFov", at = @At("RETURN"), cancellable = true)
|
||||||
|
private void onGetFov(Camera camera, float tickDelta, boolean changingFov, CallbackInfoReturnable<Double> info) {
|
||||||
|
if (ZoomManager.isZooming()) {
|
||||||
|
double originalFov = info.getReturnValue();
|
||||||
|
info.setReturnValue(originalFov / ZoomManager.getZoomMultiplier());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import net.minecraft.client.option.SimpleOption;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Mixin(SimpleOption.DoubleSliderCallbacks.class)
|
||||||
|
public class GammaBypassMixin {
|
||||||
|
|
||||||
|
@Inject(method = "validate(Ljava/lang/Object;)Ljava/util/Optional;", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void onValidate(Object value, CallbackInfoReturnable<Optional<?>> cir) {
|
||||||
|
cir.setReturnValue(Optional.of(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.managers.ZoomManager;
|
||||||
|
import net.minecraft.client.Mouse;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(Mouse.class)
|
||||||
|
public class MouseMixin {
|
||||||
|
@Inject(method = "onMouseScroll", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void onScroll(long window, double horizontal, double vertical, CallbackInfo info) {
|
||||||
|
if (ZoomManager.isZooming()) {
|
||||||
|
ZoomManager.changeZoom(vertical);
|
||||||
|
|
||||||
|
info.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ModifyVariable(method = "updateMouse", at = @At("STORE"), ordinal = 2)
|
||||||
|
private double adjustSensitivity(double value) {
|
||||||
|
if (ZoomManager.isZooming()) {
|
||||||
|
return value / ZoomManager.getZoomMultiplier() * 3;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
|
||||||
|
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.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(MultiplayerScreen.class)
|
||||||
|
public class MultiplayerScreenMixin {
|
||||||
|
@Inject(method = "render", at = @At("HEAD"))
|
||||||
|
private void onRender(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
|
||||||
|
int screenWidth = context.getScaledWindowWidth();
|
||||||
|
int screenHeight = context.getScaledWindowHeight();
|
||||||
|
|
||||||
|
context.drawTexture(
|
||||||
|
Identifier.of("citrus", "textures/startscreen/wallpaper.png"),
|
||||||
|
0, 0,
|
||||||
|
0.0f, 0.0f,
|
||||||
|
screenWidth, screenHeight,
|
||||||
|
screenWidth, screenHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.Screen;
|
||||||
|
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.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(Screen.class)
|
||||||
|
public class OptionsScreenMixin {
|
||||||
|
@Inject(method = "renderBackground", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void renderCustomBackground(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
|
||||||
|
int width = context.getScaledWindowWidth();
|
||||||
|
int height = context.getScaledWindowHeight();
|
||||||
|
|
||||||
|
context.drawTexture(
|
||||||
|
Identifier.of("citrus", "textures/startscreen/wallpaper.png"),
|
||||||
|
0, 0,
|
||||||
|
0.0f, 0.0f,
|
||||||
|
width, height,
|
||||||
|
width, height
|
||||||
|
);
|
||||||
|
|
||||||
|
ci.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import net.minecraft.client.network.PlayerListEntry;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||||
|
|
||||||
|
@Mixin(PlayerListEntry.class)
|
||||||
|
public interface PlayerListEntryMixin {
|
||||||
|
@Accessor("latency")
|
||||||
|
int getLatency();
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.managers.*;
|
||||||
|
import net.minecraft.client.render.entity.EntityRenderer;
|
||||||
|
import net.minecraft.entity.Entity;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import net.minecraft.text.*;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Mixin(EntityRenderer.class)
|
||||||
|
public abstract class PlayerNametagMixin {
|
||||||
|
|
||||||
|
@ModifyVariable(method = "renderLabelIfPresent", at = @At("HEAD"), argsOnly = true)
|
||||||
|
private Text addRankIcon(Text text, Entity entity) {
|
||||||
|
if (entity instanceof PlayerEntity player) {
|
||||||
|
UUID uuid = player.getUuid();
|
||||||
|
|
||||||
|
if (!ClientRegistrationManager.isPlayerOnlineWithCitrus(uuid)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
String iconChar = RankManager.getRankIcon(uuid);
|
||||||
|
|
||||||
|
if (!iconChar.trim().isEmpty()) {
|
||||||
|
MutableText icon = Text.literal(iconChar).setStyle(Style.EMPTY.withFont(Identifier.of("citrus", "icon_font")));
|
||||||
|
|
||||||
|
MutableText nameWithSpace = Text.literal(" ")
|
||||||
|
.append(text)
|
||||||
|
.setStyle(Style.EMPTY.withFont(Identifier.of("minecraft", "default")));
|
||||||
|
|
||||||
|
return icon.append(nameWithSpace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.gui.TestScreen;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.Screen;
|
||||||
|
import net.minecraft.client.render.DiffuseLighting;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mixin to render the player entity in the TestScreen.
|
||||||
|
* This hooks into Screen.render to draw on the active UI frame.
|
||||||
|
*/
|
||||||
|
@Mixin(Screen.class)
|
||||||
|
public class ScreenRenderMixin {
|
||||||
|
|
||||||
|
@Inject(method = "render", at = @At("TAIL"))
|
||||||
|
private void citrus$renderPlayerPreview(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
|
||||||
|
TestScreen testScreen = TestScreen.getActiveInstance();
|
||||||
|
if (testScreen == null || !testScreen.isPreviewBoundsSet()) return;
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player == null || client.currentScreen == null) return;
|
||||||
|
|
||||||
|
// Ensure UI-friendly lighting for entity rendering.
|
||||||
|
DiffuseLighting.enableGuiDepthLighting();
|
||||||
|
|
||||||
|
TestScreen.renderPlayerEntity(
|
||||||
|
context,
|
||||||
|
testScreen.getPreviewCenterX(),
|
||||||
|
testScreen.getPreviewCenterY(),
|
||||||
|
testScreen.getPreviewSize(),
|
||||||
|
testScreen.getPlayerRotationX(),
|
||||||
|
testScreen.getPlayerRotationY(),
|
||||||
|
client.player
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||||
|
import net.minecraft.client.render.entity.LivingEntityRenderer;
|
||||||
|
import net.minecraft.entity.Entity;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
|
||||||
|
@Mixin(LivingEntityRenderer.class)
|
||||||
|
public abstract class ShowOwnNametagMixin {
|
||||||
|
@ModifyExpressionValue(
|
||||||
|
method = "hasLabel(Lnet/minecraft/entity/LivingEntity;)Z",
|
||||||
|
at = @At(value = "INVOKE", target = "Lnet/minecraft/client/MinecraftClient;getCameraEntity()Lnet/minecraft/entity/Entity;")
|
||||||
|
)
|
||||||
|
|
||||||
|
public Entity obfuscateCameraEntity(Entity original){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.world.SelectWorldScreen;
|
||||||
|
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.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(SelectWorldScreen.class)
|
||||||
|
public class SingleplayerScreenMixin {
|
||||||
|
@Inject(method = "render", at = @At("HEAD"))
|
||||||
|
private void onRender(DrawContext context, int mouseX, int mouseY, float delta, CallbackInfo ci) {
|
||||||
|
int screenWidth = context.getScaledWindowWidth();
|
||||||
|
int screenHeight = context.getScaledWindowHeight();
|
||||||
|
|
||||||
|
context.drawTexture(
|
||||||
|
Identifier.of("citrus", "textures/startscreen/wallpaper.png"),
|
||||||
|
0, 0,
|
||||||
|
0.0f, 0.0f,
|
||||||
|
screenWidth, screenHeight,
|
||||||
|
screenWidth, screenHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.managers.*;
|
||||||
|
import net.minecraft.client.gui.hud.PlayerListHud;
|
||||||
|
import net.minecraft.client.network.PlayerListEntry;
|
||||||
|
import net.minecraft.text.*;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Mixin(PlayerListHud.class)
|
||||||
|
public abstract class TabListMixin {
|
||||||
|
|
||||||
|
@Inject(method = "getPlayerName", at = @At("RETURN"), cancellable = true)
|
||||||
|
private void addRankIconToTab(PlayerListEntry entry, CallbackInfoReturnable<Text> cir) {
|
||||||
|
UUID uuid = entry.getProfile().getId();
|
||||||
|
|
||||||
|
if (!ClientRegistrationManager.isPlayerOnlineWithCitrus(uuid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String iconChar = RankManager.getRankIcon(uuid);
|
||||||
|
|
||||||
|
if (!iconChar.trim().isEmpty()) {
|
||||||
|
Text currentName = cir.getReturnValue() != null ? cir.getReturnValue() : Text.literal(entry.getProfile().getName());
|
||||||
|
|
||||||
|
MutableText icon = Text.literal(iconChar)
|
||||||
|
.setStyle(Style.EMPTY.withFont(Identifier.of("citrus", "icon_font")));
|
||||||
|
|
||||||
|
MutableText nameWithSpace = Text.literal(" ")
|
||||||
|
.append(currentName)
|
||||||
|
.setStyle(Style.EMPTY.withFont(Identifier.of("minecraft", "default")));
|
||||||
|
|
||||||
|
cir.setReturnValue(icon.append(nameWithSpace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.beta.BetaKeyScreen;
|
||||||
|
import de.winniepat.citrus.gui.MainMenuScreen;
|
||||||
|
import icyllis.modernui.mc.MuiModApi;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.screen.TitleScreen;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
|
@Mixin(TitleScreen.class)
|
||||||
|
public abstract class TitleScreenMixin {
|
||||||
|
@Inject(method = "init", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void onInit(CallbackInfo ci) {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
|
||||||
|
if (!Client.betaFeaturesEnabled) {
|
||||||
|
client.setScreen(MuiModApi.get().createScreen(new MainMenuScreen(), null, null));
|
||||||
|
ci.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Client.getBetaManager().isVerified()) {
|
||||||
|
if ("WinniePat".equals(client.getSession().getUsername())) {
|
||||||
|
Client.logger.info("Welcome back! You have bypass from the Beta Restriction :)");
|
||||||
|
} else {
|
||||||
|
client.setScreen(new BetaKeyScreen());
|
||||||
|
ci.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.setScreen(MuiModApi.get().createScreen(new MainMenuScreen(), null, null));
|
||||||
|
ci.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package de.winniepat.citrus.mixins;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.*;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||||
|
|
||||||
|
@Mixin(MinecraftClient.class)
|
||||||
|
public abstract class WindowTitleMixin {
|
||||||
|
@Inject(method = "getWindowTitle", at = @At("HEAD"), cancellable = true)
|
||||||
|
private void injectCustomWindowTitle(CallbackInfoReturnable<String> cir) {
|
||||||
|
cir.setReturnValue(Client.clientInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package de.winniepat.citrus.utils;
|
||||||
|
|
||||||
|
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.sound.SoundEvents;
|
||||||
|
|
||||||
|
public class ChatSound {
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
ClientReceiveMessageEvents.CHAT.register((message, signedMessage, sender, params, receptionTimestamp) -> checkForMention(message.getString()));
|
||||||
|
|
||||||
|
ClientReceiveMessageEvents.GAME.register((message, overlay) -> {
|
||||||
|
if (!overlay) {
|
||||||
|
checkForMention(message.getString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkForMention(String message) {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client == null || client.getSession() == null) return;
|
||||||
|
|
||||||
|
String myName = client.getSession().getUsername();
|
||||||
|
String cleanMessage = message.replaceAll("§.", "");
|
||||||
|
|
||||||
|
if (cleanMessage.toLowerCase().contains(myName.toLowerCase())) {
|
||||||
|
playMentionSound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void playMentionSound() {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client != null && client.player != null) {
|
||||||
|
client.player.playSound(
|
||||||
|
SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP,
|
||||||
|
0.67F,
|
||||||
|
1.0F
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package de.winniepat.citrus.utils;
|
||||||
|
|
||||||
|
import com.terraformersmc.modmenu.api.*;
|
||||||
|
import de.winniepat.citrus.gui.CapeSelectionScreen;
|
||||||
|
|
||||||
|
public class ModMenuIntegration implements ModMenuApi {
|
||||||
|
@Override
|
||||||
|
public ConfigScreenFactory<?> getModConfigScreenFactory() {
|
||||||
|
return parent -> new CapeSelectionScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package de.winniepat.citrus.utils;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.*;
|
||||||
|
import de.winniepat.citrus.cosmetics.capes.sync.CapeSyncManager;
|
||||||
|
import de.winniepat.citrus.cosmetics.wings.sync.WingSyncManager;
|
||||||
|
import de.winniepat.citrus.managers.*;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
|
||||||
|
import java.util.Timer;
|
||||||
|
import java.util.TimerTask;
|
||||||
|
|
||||||
|
public class RefreshUtil {
|
||||||
|
|
||||||
|
public static void startPeriodicRankRefresh() {
|
||||||
|
Timer timer = new Timer("Citrus-PeriodicRankRefresh", true);
|
||||||
|
timer.scheduleAtFixedRate(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
MinecraftClient mc = MinecraftClient.getInstance();
|
||||||
|
if (mc.getNetworkHandler() != null && mc.player != null) {
|
||||||
|
Client.debugLog("Background Rank Refresh Triggered");
|
||||||
|
RankManager.refreshRankIfChanged();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.debugLog("Error during Rank refresh: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10000, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startPeriodicRegisterCheck() {
|
||||||
|
|
||||||
|
if (ClientRegistrationManager.isRegistered()) return;
|
||||||
|
|
||||||
|
Timer timer = new Timer("Citrus-PeriodicRegisterCheck", true);
|
||||||
|
timer.scheduleAtFixedRate(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
ClientRegistrationManager.register();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.debugLog("Error during Registration refresh: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10000, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startPeriodicCapeSync() {
|
||||||
|
Timer timer = new Timer("Citrus-PeriodicCapeSync", true);
|
||||||
|
timer.scheduleAtFixedRate(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
CapeSyncManager.forceSyncNow();
|
||||||
|
CapeHandler.retryFailedCapes();
|
||||||
|
CapeSyncManager.SYNCED_CAPE_CACHE.clear();
|
||||||
|
Client.debugLog("Background Cape sync triggered");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error during Cape sync: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 10000, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void start() {
|
||||||
|
startPeriodicRankRefresh();
|
||||||
|
startPeriodicRegisterCheck();
|
||||||
|
startPeriodicCapeSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package de.winniepat.citrus.utils;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
|
||||||
|
public class SecurityUtils {
|
||||||
|
private static final String MASTER_SECRET = "xhIc7PAz0whLGm8t8EsIK574mDqvuqGs6oWLm0pWynkSnpuTU14iML1FDHdlXOJhHQBww65mcehps0fYBUnVJe5m4GR7HHlnBjQxQQ4crqhw4R3AbegwByr7aEg6XvwP";
|
||||||
|
|
||||||
|
public static String getDailyKey() {
|
||||||
|
try {
|
||||||
|
String date = java.time.LocalDate.now(ZoneOffset.UTC).toString();
|
||||||
|
|
||||||
|
SecretKeySpec signingKey = new SecretKeySpec(
|
||||||
|
MASTER_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"
|
||||||
|
);
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA256");
|
||||||
|
mac.init(signingKey);
|
||||||
|
|
||||||
|
byte[] rawHmac = mac.doFinal(date.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return bytesToHex(rawHmac);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Error while getting daily key", e);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : bytes) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package de.winniepat.citrus.utils.text;
|
||||||
|
|
||||||
|
import io.wispforest.owo.ui.component.LabelComponent;
|
||||||
|
import io.wispforest.owo.ui.core.*;
|
||||||
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
|
import net.minecraft.text.*;
|
||||||
|
import net.minecraft.util.math.MathHelper;
|
||||||
|
|
||||||
|
public class TextComponent extends LabelComponent {
|
||||||
|
float scale = 1f;
|
||||||
|
|
||||||
|
public TextComponent(Text text) {
|
||||||
|
super(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextComponent scale(float scale) {
|
||||||
|
this.scale = scale;
|
||||||
|
this.notifyParentIfMounted();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int determineHorizontalContentSize(Sizing sizing) {
|
||||||
|
return MathHelper.ceil(super.determineHorizontalContentSize(sizing) * this.scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int determineVerticalContentSize(Sizing sizing) {
|
||||||
|
return MathHelper.ceil(super.determineVerticalContentSize(sizing) * this.scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partialTicks, float delta) {
|
||||||
|
MatrixStack matrices = context.getMatrices();
|
||||||
|
matrices.push();
|
||||||
|
|
||||||
|
matrices.scale(this.scale, this.scale, this.scale);
|
||||||
|
|
||||||
|
int baseScreenY = this.y;
|
||||||
|
|
||||||
|
switch (this.verticalTextAlignment) {
|
||||||
|
case CENTER -> baseScreenY += (this.height - this.textHeight()) / 2;
|
||||||
|
case BOTTOM -> baseScreenY += this.height - this.textHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = (int) (this.x / this.scale);
|
||||||
|
int y = (int) (baseScreenY / this.scale);
|
||||||
|
|
||||||
|
context.draw(() -> {
|
||||||
|
int currenY = y;
|
||||||
|
for (OrderedText renderText : this.wrappedText) {
|
||||||
|
int renderX = x;
|
||||||
|
|
||||||
|
switch (this.horizontalTextAlignment) {
|
||||||
|
case CENTER -> renderX = x + (int) (((this.width / this.scale) - this.textRenderer.getWidth(renderText)) / 2f);
|
||||||
|
case RIGHT -> renderX = x + (int) (this.width / this.scale - this.textRenderer.getWidth(renderText));
|
||||||
|
}
|
||||||
|
|
||||||
|
int renderY = currenY + (int) ((this.lineHeight() - 9) / this.scale);
|
||||||
|
|
||||||
|
context.drawText(this.textRenderer, renderText, renderX, renderY, this.color.get().argb(),this.shadow);
|
||||||
|
|
||||||
|
currenY += (int) ((this.lineHeight() + this.lineSpacing()) / this.scale);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
matrices.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package de.winniepat.citrus.utils.texture;
|
||||||
|
|
||||||
|
import icyllis.modernui.graphics.*;
|
||||||
|
import icyllis.modernui.graphics.drawable.Drawable;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
|
||||||
|
public class CdnTextureDrawable extends Drawable {
|
||||||
|
|
||||||
|
private Identifier textureId;
|
||||||
|
private Image mImage;
|
||||||
|
private byte[] mImageData;
|
||||||
|
|
||||||
|
public CdnTextureDrawable() {
|
||||||
|
this.textureId = null;
|
||||||
|
this.mImage = null;
|
||||||
|
this.mImageData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CdnTextureDrawable(Identifier textureId) {
|
||||||
|
this.textureId = textureId;
|
||||||
|
this.mImage = null;
|
||||||
|
this.mImageData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTextureId(Identifier textureId) {
|
||||||
|
this.textureId = textureId;
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageData(byte[] imageData) {
|
||||||
|
this.mImageData = imageData;
|
||||||
|
this.mImage = null;
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createImageFromData() {
|
||||||
|
if (mImageData == null || mImageData.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Bitmap bitmap = BitmapFactory.decodeStream(new ByteArrayInputStream(mImageData));
|
||||||
|
mImage = Image.createTextureFromBitmap(bitmap);
|
||||||
|
} catch (Exception e) {
|
||||||
|
mImage = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Identifier getTextureId() {
|
||||||
|
return textureId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasTexture() {
|
||||||
|
return textureId != null || mImage != null || mImageData != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NotNull Canvas canvas) {
|
||||||
|
if (mImage == null && mImageData != null) {
|
||||||
|
createImageFromData();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mImage == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect bounds = getBounds();
|
||||||
|
if (bounds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Paint paint = Paint.obtain();
|
||||||
|
canvas.drawImage(mImage, null, bounds, paint);
|
||||||
|
paint.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicWidth() {
|
||||||
|
return mImage != null ? mImage.getWidth() : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicHeight() {
|
||||||
|
return mImage != null ? mImage.getHeight() : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
|
||||||
|
package de.winniepat.citrus.utils.texture;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.utils.SecurityUtils;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.texture.*;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
public class IconLoaderComponent {
|
||||||
|
|
||||||
|
private static final String MOD_ID = "citrus";
|
||||||
|
|
||||||
|
private static final Map<String, Identifier> CACHE = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public static CompletableFuture<Identifier> getRankTexture(String uuid) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
URL rankUrl = new URI(Client.API_URL + "/rank/" + uuid).toURL();
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) rankUrl.openConnection();
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
conn.setRequestProperty("x-daily-key", SecurityUtils.getDailyKey());
|
||||||
|
|
||||||
|
String rank;
|
||||||
|
try (InputStream is = conn.getInputStream();
|
||||||
|
Scanner scanner = new Scanner(is)) {
|
||||||
|
rank = scanner.useDelimiter("\\A").next().trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.disconnect();
|
||||||
|
|
||||||
|
if (CACHE.containsKey(rank)) {
|
||||||
|
return CACHE.get(rank);
|
||||||
|
}
|
||||||
|
|
||||||
|
URL iconUrl = new URI(Client.CDN_URL + "/ranks/" + rank + ".png").toURL();
|
||||||
|
try (InputStream is = iconUrl.openStream()) {
|
||||||
|
NativeImage image = NativeImage.read(is);
|
||||||
|
NativeImageBackedTexture texture = new NativeImageBackedTexture(image);
|
||||||
|
|
||||||
|
Identifier textureId = Identifier.of(MOD_ID, "ranks/" + rank);
|
||||||
|
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
MinecraftClient.getInstance().getTextureManager().registerTexture(textureId, texture);
|
||||||
|
});
|
||||||
|
|
||||||
|
CACHE.put(rank, textureId);
|
||||||
|
return textureId;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to get rank texture for UUID: {}", uuid, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package de.winniepat.citrus.utils.texture;
|
||||||
|
|
||||||
|
import de.winniepat.citrus.Client;
|
||||||
|
import de.winniepat.citrus.utils.SecurityUtils;
|
||||||
|
import io.wispforest.owo.ui.base.BaseComponent;
|
||||||
|
import io.wispforest.owo.ui.core.*;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.texture.*;
|
||||||
|
import net.minecraft.util.*;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class ModIconComponent extends BaseComponent {
|
||||||
|
|
||||||
|
private static final String ICON_URL = Client.API_URL + "/client/logo.png";
|
||||||
|
public static Identifier LOADED_TEXTURE = null;
|
||||||
|
private static boolean loadingStarted = false;
|
||||||
|
|
||||||
|
public ModIconComponent() {
|
||||||
|
this.sizing(Sizing.fixed(20), Sizing.fixed(20));
|
||||||
|
startLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startLoading() {
|
||||||
|
if (loadingStarted || LOADED_TEXTURE != null) return;
|
||||||
|
loadingStarted = true;
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(ICON_URL))
|
||||||
|
.header("x-daily-key", SecurityUtils.getDailyKey())
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<InputStream> response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
||||||
|
|
||||||
|
if (response.statusCode() == 200) {
|
||||||
|
try (InputStream is = response.body()) {
|
||||||
|
NativeImage image = NativeImage.read(is);
|
||||||
|
NativeImageBackedTexture texture = new NativeImageBackedTexture(image);
|
||||||
|
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
LOADED_TEXTURE = MinecraftClient.getInstance()
|
||||||
|
.getTextureManager()
|
||||||
|
.registerDynamicTexture("citrus_logo", texture);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Client.logger.error("Failed to load icon from CDN", e);
|
||||||
|
loadingStarted = false;
|
||||||
|
}
|
||||||
|
}, Util.getMainWorkerExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partialTick, float delta) {
|
||||||
|
if (LOADED_TEXTURE == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.drawTexture(
|
||||||
|
LOADED_TEXTURE,
|
||||||
|
x(), y(),
|
||||||
|
20, 20,
|
||||||
|
0, 0,
|
||||||
|
20, 20,
|
||||||
|
20, 20
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package de.winniepat.citrus.utils.toasts;
|
||||||
|
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.font.TextRenderer;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.toast.Toast;
|
||||||
|
import net.minecraft.client.toast.ToastManager;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
public class CustomToast implements Toast {
|
||||||
|
private final Text title;
|
||||||
|
private final Text description;
|
||||||
|
private final int titleColor;
|
||||||
|
private final int descColor;
|
||||||
|
private final int width;
|
||||||
|
|
||||||
|
private static final Identifier TEXTURE = Identifier.of("minecraft", "toast/advancement");
|
||||||
|
|
||||||
|
public CustomToast(TextRenderer renderer, Text title, int titleColor, Text description, int descColor) {
|
||||||
|
this.title = title;
|
||||||
|
this.titleColor = titleColor;
|
||||||
|
this.description = description;
|
||||||
|
this.descColor = descColor;
|
||||||
|
|
||||||
|
int titleWidth = renderer.getWidth(title);
|
||||||
|
int descWidth = renderer.getWidth(description);
|
||||||
|
|
||||||
|
this.width = Math.max(40, Math.max(titleWidth, descWidth) + 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getWidth() {
|
||||||
|
return this.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Visibility draw(DrawContext context, ToastManager manager, long startTime) {
|
||||||
|
context.drawGuiTexture(TEXTURE, 0, 0, this.getWidth(), 32);
|
||||||
|
|
||||||
|
TextRenderer renderer = manager.getClient().textRenderer;
|
||||||
|
|
||||||
|
context.drawText(renderer, title, 8, 7, titleColor, false);
|
||||||
|
context.drawText(renderer, description, 8, 18, descColor, false);
|
||||||
|
|
||||||
|
return startTime >= 5000L ? Visibility.HIDE : Visibility.SHOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendCustomToast(String title, int titleCol, String desc, int descCol) {
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client != null) {
|
||||||
|
client.getToastManager().add(new CustomToast(
|
||||||
|
client.textRenderer,
|
||||||
|
Text.literal(title), titleCol,
|
||||||
|
Text.literal(desc), descCol
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.8.0",
|
||||||
|
"animations": {
|
||||||
|
"idle": {
|
||||||
|
"loop": true,
|
||||||
|
"animation_length": 125.1667,
|
||||||
|
"bones": {
|
||||||
|
"L5": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, "-40 - ((1 - 0.15) * math.sin(query.anim_time*100 - 50) * 5 + 0.2 * math.sin(query.anim_time*100 - 50 - 0.25) * 25)", 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"L4": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, "-((1 - 0.15) * math.sin(query.anim_time*100 - 100) * 20 + 0.2 * math.sin(query.anim_time*100 - 100 - 0.25) * 1)", 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"R5": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, " 40 + ((1 - 0.15) * math.sin(query.anim_time*100 - 50) * 5 + 0.2 * math.sin(query.anim_time*100 - 50 - 0.25) * 1)", 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"R4": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, "((1 - 0.15) * math.sin(query.anim_time*100 - 100) * 20 + 0.2 * math.sin(query.anim_time*100 - 100 - 0.25) * 35)", 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geckolib_format_version": 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.8.0",
|
||||||
|
"animations": {
|
||||||
|
"nrc.idle": {
|
||||||
|
"loop": true,
|
||||||
|
"bones": {
|
||||||
|
"halo": {
|
||||||
|
"rotation": [0, "math.cos(query.anim_time * 10) * 150", 0],
|
||||||
|
"position": [0, "math.cos(query.anim_time * 100) * 0.3", 0]
|
||||||
|
},
|
||||||
|
"sparkle": {
|
||||||
|
"position": [0, -4.5, 0],
|
||||||
|
"scale": 1.15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geckolib_format_version": 2,
|
||||||
|
"geometry.animations": {
|
||||||
|
"texturewidth": 64,
|
||||||
|
"textureheight": 64
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.8.0",
|
||||||
|
"animations": {
|
||||||
|
"animation.hat.idle": {
|
||||||
|
"loop": true,
|
||||||
|
"bones": {
|
||||||
|
"bandana": {
|
||||||
|
"scale": {
|
||||||
|
"vector": [1.05, 1.05, 1.05]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geckolib_format_version": 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.8.0",
|
||||||
|
"animations": {
|
||||||
|
"animation.wings.idle": {
|
||||||
|
"loop": true,
|
||||||
|
"animation_length": 125.1667,
|
||||||
|
"bones": {
|
||||||
|
"L2": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, "-40 - ((1 - 0.15) * math.sin(query.anim_time*100 - 50) * 5 + 0.2 * math.sin(query.anim_time*100 - 50 - 0.25) * 25)", 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"L3": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, "-((1 - 0.15) * math.sin(query.anim_time*100 - 100) * 20 + 0.2 * math.sin(query.anim_time*100 - 100 - 0.25) * 1)", 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"R2": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, " 40 + ((1 - 0.15) * math.sin(query.anim_time*100 - 50) * 5 + 0.2 * math.sin(query.anim_time*100 - 50 - 0.25) * 1)", 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"R3": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, "((1 - 0.15) * math.sin(query.anim_time*100 - 100) * 20 + 0.2 * math.sin(query.anim_time*100 - 100 - 0.25) * 35)", 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geckolib_format_version": 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.8.0",
|
||||||
|
"animations": {
|
||||||
|
"animation.wings.idle": {
|
||||||
|
"loop": true,
|
||||||
|
"animation_length": 125.1667,
|
||||||
|
"bones": {
|
||||||
|
"L2": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, "-40 - ((1 - 0.15) * math.sin(query.anim_time*100 - 50) * 5 + 0.2 * math.sin(query.anim_time*100 - 50 - 0.25) * 25)", 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"L3": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, "-((1 - 0.15) * math.sin(query.anim_time*100 - 100) * 20 + 0.2 * math.sin(query.anim_time*100 - 100 - 0.25) * 1)", 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"R2": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, " 40 + ((1 - 0.15) * math.sin(query.anim_time*100 - 50) * 5 + 0.2 * math.sin(query.anim_time*100 - 50 - 0.25) * 1)", 0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"R3": {
|
||||||
|
"rotation": {
|
||||||
|
"vector": [0, "((1 - 0.15) * math.sin(query.anim_time*100 - 100) * 20 + 0.2 * math.sin(query.anim_time*100 - 100 - 0.25) * 35)", 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geckolib_format_version": 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"providers": [
|
||||||
|
{
|
||||||
|
"type": "bitmap",
|
||||||
|
"file": "citrus:ranks/owner.png",
|
||||||
|
"ascent": 7,
|
||||||
|
"height": 8,
|
||||||
|
"chars": ["\uE001"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bitmap",
|
||||||
|
"file": "citrus:ranks/admin.png",
|
||||||
|
"ascent": 7,
|
||||||
|
"height": 8,
|
||||||
|
"chars": ["\uE002"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bitmap",
|
||||||
|
"file": "citrus:ranks/vip.png",
|
||||||
|
"ascent": 7,
|
||||||
|
"height": 8,
|
||||||
|
"chars": ["\uE003"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bitmap",
|
||||||
|
"file": "citrus:ranks/dev.png",
|
||||||
|
"ascent": 7,
|
||||||
|
"height": 8,
|
||||||
|
"chars": ["\uE004"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bitmap",
|
||||||
|
"file": "citrus:ranks/bughunter.png",
|
||||||
|
"ascent": 7,
|
||||||
|
"height": 8,
|
||||||
|
"chars": ["\uE005"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bitmap",
|
||||||
|
"file": "citrus:ranks/default.png",
|
||||||
|
"ascent": 7,
|
||||||
|
"height": 8,
|
||||||
|
"chars": ["\uE099"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.12.0",
|
||||||
|
"minecraft:geometry": [
|
||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"identifier": "geometry.unknown",
|
||||||
|
"texture_width": 100,
|
||||||
|
"texture_height": 100,
|
||||||
|
"visible_bounds_width": 3,
|
||||||
|
"visible_bounds_height": 3.5,
|
||||||
|
"visible_bounds_offset": [0, 1.25, 0]
|
||||||
|
},
|
||||||
|
"bones": [
|
||||||
|
{
|
||||||
|
"name": "bb_main",
|
||||||
|
"pivot": [0, 0, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedHead",
|
||||||
|
"pivot": [0, 24.99, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorHead",
|
||||||
|
"parent": "bipedHead",
|
||||||
|
"pivot": [0, 24.99, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedBody",
|
||||||
|
"pivot": [0, 24.99, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorBody",
|
||||||
|
"parent": "bipedBody",
|
||||||
|
"pivot": [0, 24.99, 1.12857],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-4, 12, -2],
|
||||||
|
"size": [8, 12, 4.1],
|
||||||
|
"uv": {
|
||||||
|
"south": {"uv": [85.15625, 83.59375], "uv_size": [10.15625, 14.0625]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dark_wings",
|
||||||
|
"parent": "armorBody",
|
||||||
|
"pivot": [-4, 22, 4.3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "right",
|
||||||
|
"parent": "dark_wings",
|
||||||
|
"pivot": [1.3, 23, 1.9]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "L2",
|
||||||
|
"parent": "right",
|
||||||
|
"pivot": [1.3, 23, 1.9],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [0, 15, 3.2],
|
||||||
|
"size": [0, 14, 3.1],
|
||||||
|
"pivot": [0, 26, 1.85],
|
||||||
|
"rotation": [0, 90, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [0, 85.9375], "uv_size": [17.1875, 13.28125]},
|
||||||
|
"east": {"uv": [100, 85.9375], "uv_size": [-9.375, 13.28125]},
|
||||||
|
"south": {"uv": [0, 85.9375], "uv_size": [17.1875, 13.28125]},
|
||||||
|
"west": {"uv": [54.9375, 21.78125], "uv_size": [-7.25, 28.125]},
|
||||||
|
"up": {"uv": [0, 89.92188], "uv_size": [17.1875, -3.98437]},
|
||||||
|
"down": {"uv": [0, 85.9375], "uv_size": [17.1875, 3.98438]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "L3",
|
||||||
|
"parent": "L2",
|
||||||
|
"pivot": [4.45, 23, 1.86029],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-2, 15, 8.2],
|
||||||
|
"size": [0, 14, 10.1],
|
||||||
|
"pivot": [-2, 26, 1.85],
|
||||||
|
"rotation": [0, 90, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [0, 85.9375], "uv_size": [17.1875, 13.28125]},
|
||||||
|
"east": {"uv": [82.8125, 86.71875], "uv_size": [12.03125, 13.28125]},
|
||||||
|
"south": {"uv": [0, 85.9375], "uv_size": [17.1875, 13.28125]},
|
||||||
|
"west": {"uv": [47.725, 21.78125], "uv_size": [-19.7875, 28.125]},
|
||||||
|
"up": {"uv": [0, 99.21875], "uv_size": [17.1875, -9.29687]},
|
||||||
|
"down": {"uv": [0, 89.92188], "uv_size": [17.1875, 9.29688]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "left",
|
||||||
|
"parent": "dark_wings",
|
||||||
|
"pivot": [-1.3, 23, 1.8]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "R2",
|
||||||
|
"parent": "left",
|
||||||
|
"pivot": [-1.3, 23, 1.8],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [0, 15, -2.6],
|
||||||
|
"size": [0, 14, 3.1],
|
||||||
|
"pivot": [0, 26, 1.85],
|
||||||
|
"rotation": [0, 90, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [17.1875, 85.9375], "uv_size": [-17.1875, 13.28125]},
|
||||||
|
"east": {"uv": [100, 86.71875], "uv_size": [-9.375, 13.28125]},
|
||||||
|
"south": {"uv": [17.1875, 85.9375], "uv_size": [-17.1875, 13.28125]},
|
||||||
|
"west": {"uv": [47.6875, 21.78125], "uv_size": [7.25, 28.125]},
|
||||||
|
"up": {"uv": [0, 85.9375], "uv_size": [17.1875, 3.98438]},
|
||||||
|
"down": {"uv": [0, 89.92188], "uv_size": [17.1875, -3.98437]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "R3",
|
||||||
|
"parent": "R2",
|
||||||
|
"pivot": [-4.45, 23, 1.86029],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [2, 15, -14.6],
|
||||||
|
"size": [0, 14, 10.1],
|
||||||
|
"pivot": [2, 26, 1.85],
|
||||||
|
"rotation": [0, 90, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [17.1875, 85.9375], "uv_size": [-17.1875, 13.28125]},
|
||||||
|
"east": {"uv": [94.84375, 86.71875], "uv_size": [-12.03125, 13.28125]},
|
||||||
|
"south": {"uv": [17.1875, 85.9375], "uv_size": [-17.1875, 13.28125]},
|
||||||
|
"west": {"uv": [27.9375, 21.78125], "uv_size": [19.7875, 28.125]},
|
||||||
|
"up": {"uv": [0, 89.92188], "uv_size": [17.1875, 9.29688]},
|
||||||
|
"down": {"uv": [0, 99.21875], "uv_size": [17.1875, -9.29687]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedRightArm",
|
||||||
|
"pivot": [3.28, 23.35, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightArm",
|
||||||
|
"parent": "bipedRightArm",
|
||||||
|
"pivot": [3.28, 23.35, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedLeftLeg",
|
||||||
|
"pivot": [-1.64, 15.15, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorLeftLeg",
|
||||||
|
"parent": "bipedLeftLeg",
|
||||||
|
"pivot": [-1.64, 15.15, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorLeftBoot",
|
||||||
|
"parent": "bipedLeftLeg",
|
||||||
|
"pivot": [-1.64, 15.15, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedRightLeg",
|
||||||
|
"pivot": [1.64, 15.15, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightLeg",
|
||||||
|
"parent": "bipedRightLeg",
|
||||||
|
"pivot": [1.64, 15.15, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightBoot",
|
||||||
|
"parent": "bipedRightLeg",
|
||||||
|
"pivot": [1.64, 15.15, 1.12857]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.12.0",
|
||||||
|
"minecraft:geometry": [
|
||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"identifier": "geometry.unknown",
|
||||||
|
"texture_width": 64,
|
||||||
|
"texture_height": 64,
|
||||||
|
"visible_bounds_width": 2,
|
||||||
|
"visible_bounds_height": 3.5,
|
||||||
|
"visible_bounds_offset": [0, 1.25, 0]
|
||||||
|
},
|
||||||
|
"bones": [
|
||||||
|
{
|
||||||
|
"name": "bipedHead",
|
||||||
|
"pivot": [0, 24, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorHead",
|
||||||
|
"parent": "bipedHead",
|
||||||
|
"pivot": [0, 24, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "sparkle",
|
||||||
|
"parent": "armorHead",
|
||||||
|
"pivot": [-0.00129, -9.24, 0.03425],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-6.73398, 30.99279, 0.28539],
|
||||||
|
"size": [13.44642, 3.41442, 0.00002],
|
||||||
|
"inflate": -0.0001,
|
||||||
|
"pivot": [-0.01077, 32.7, 0.2854],
|
||||||
|
"rotation": [0, 60, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [30, 6], "uv_size": [23, 19]},
|
||||||
|
"east": {"uv": [0, 55], "uv_size": [30, 6]},
|
||||||
|
"south": {"uv": [0, 58], "uv_size": [30, 6]},
|
||||||
|
"west": {"uv": [24, 5], "uv_size": [4, 1]},
|
||||||
|
"up": {"uv": [25, 2], "uv_size": [1, 4]},
|
||||||
|
"down": {"uv": [26, 7], "uv_size": [1, -4]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [-6.73398, 30.99279, 0.28539],
|
||||||
|
"size": [13.44642, 3.41442, 0.00002],
|
||||||
|
"inflate": -0.0001,
|
||||||
|
"pivot": [-0.01077, 32.7, 0.2854],
|
||||||
|
"rotation": [0, -60, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [32, 23], "uv_size": [1, 1]},
|
||||||
|
"east": {"uv": [0, 55], "uv_size": [30, 6]},
|
||||||
|
"south": {"uv": [0, 58], "uv_size": [30, 6]},
|
||||||
|
"west": {"uv": [24, 5], "uv_size": [4, 1]},
|
||||||
|
"up": {"uv": [25, 2], "uv_size": [1, 4]},
|
||||||
|
"down": {"uv": [26, 7], "uv_size": [1, -4]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [-6.73398, 30.99279, 0.28539],
|
||||||
|
"size": [13.44642, 3.41442, 0.00002],
|
||||||
|
"inflate": -0.0001,
|
||||||
|
"pivot": [-0.01077, 32.7, 0.2854],
|
||||||
|
"rotation": [0, 30, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [28, 11], "uv_size": [17, 12]},
|
||||||
|
"east": {"uv": [0, 55], "uv_size": [30, 6]},
|
||||||
|
"south": {"uv": [0, 58], "uv_size": [30, 6]},
|
||||||
|
"west": {"uv": [24, 5], "uv_size": [4, 1]},
|
||||||
|
"up": {"uv": [25, 2], "uv_size": [1, 4]},
|
||||||
|
"down": {"uv": [26, 7], "uv_size": [1, -4]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [-6.73398, 30.99279, 0.28539],
|
||||||
|
"size": [13.44642, 3.41442, 0.00002],
|
||||||
|
"inflate": -0.0001,
|
||||||
|
"pivot": [-0.01077, 32.7, 0.2854],
|
||||||
|
"rotation": [0, 30, -180],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [32, 23], "uv_size": [1, 1]},
|
||||||
|
"east": {"uv": [0, 55], "uv_size": [30, 6]},
|
||||||
|
"south": {"uv": [30, 64], "uv_size": [-30, -6]},
|
||||||
|
"west": {"uv": [24, 5], "uv_size": [4, 1]},
|
||||||
|
"up": {"uv": [25, 2], "uv_size": [1, 4]},
|
||||||
|
"down": {"uv": [26, 7], "uv_size": [1, -4]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [-6.73398, 30.99279, 0.28539],
|
||||||
|
"size": [13.44642, 3.41442, 0.00002],
|
||||||
|
"inflate": -0.0001,
|
||||||
|
"pivot": [-0.01077, 32.7, 0.2854],
|
||||||
|
"rotation": [0, 45, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [32, 23], "uv_size": [1, 1]},
|
||||||
|
"east": {"uv": [0, 55], "uv_size": [30, 6]},
|
||||||
|
"south": {"uv": [0, 58], "uv_size": [30, 6]},
|
||||||
|
"west": {"uv": [24, 5], "uv_size": [4, 1]},
|
||||||
|
"up": {"uv": [25, 2], "uv_size": [1, 4]},
|
||||||
|
"down": {"uv": [26, 7], "uv_size": [1, -4]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [-6.73398, 30.99279, 0.28539],
|
||||||
|
"size": [13.44642, 3.41442, 0.00002],
|
||||||
|
"inflate": -0.0001,
|
||||||
|
"pivot": [-0.01077, 32.7, 0.2854],
|
||||||
|
"rotation": [0, -45, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [32, 23], "uv_size": [1, 1]},
|
||||||
|
"east": {"uv": [0, 55], "uv_size": [30, 6]},
|
||||||
|
"south": {"uv": [0, 58], "uv_size": [30, 6]},
|
||||||
|
"west": {"uv": [24, 5], "uv_size": [4, 1]},
|
||||||
|
"up": {"uv": [25, 2], "uv_size": [1, 4]},
|
||||||
|
"down": {"uv": [26, 7], "uv_size": [1, -4]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "halo",
|
||||||
|
"parent": "sparkle",
|
||||||
|
"pivot": [0, 2.5, 0],
|
||||||
|
"cubes": [
|
||||||
|
{"origin": [-2.5, 32.5, -3.5], "size": [5, 1, 1], "uv": [0, 6]},
|
||||||
|
{"origin": [-2.5, 32.5, 2.5], "size": [5, 1, 1], "uv": [0, 6]},
|
||||||
|
{"origin": [-0.5, 32.5, 1.5], "size": [5, 1, 1], "pivot": [0.5, 32.5, -1.5], "rotation": [0, -90, 0], "uv": [0, 6]},
|
||||||
|
{"origin": [0.5, 32.5, -0.5], "size": [5, 1, 1], "pivot": [3, 33, 0], "rotation": [0, -90, 0], "uv": [0, 6]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedBody",
|
||||||
|
"pivot": [0, 24, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorBody",
|
||||||
|
"parent": "bipedBody",
|
||||||
|
"pivot": [0, 24, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedLeftArm",
|
||||||
|
"pivot": [-4, 22, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorLeftArm",
|
||||||
|
"parent": "bipedLeftArm",
|
||||||
|
"pivot": [-4, 22, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedRightArm",
|
||||||
|
"pivot": [4, 22, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightArm",
|
||||||
|
"parent": "bipedRightArm",
|
||||||
|
"pivot": [4, 22, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedLeftLeg",
|
||||||
|
"pivot": [-2, 12, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorLeftLeg",
|
||||||
|
"parent": "bipedLeftLeg",
|
||||||
|
"pivot": [-2, 12, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorLeftBoot",
|
||||||
|
"parent": "bipedLeftLeg",
|
||||||
|
"pivot": [-2, 12, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedRightLeg",
|
||||||
|
"pivot": [2, 12, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightLeg",
|
||||||
|
"parent": "bipedRightLeg",
|
||||||
|
"pivot": [2, 12, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightBoot",
|
||||||
|
"parent": "bipedRightLeg",
|
||||||
|
"pivot": [2, 12, 0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.12.0",
|
||||||
|
"minecraft:geometry": [
|
||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"identifier": "geometry.unknown",
|
||||||
|
"texture_width": 768,
|
||||||
|
"texture_height": 768,
|
||||||
|
"visible_bounds_width": 2,
|
||||||
|
"visible_bounds_height": 3.5,
|
||||||
|
"visible_bounds_offset": [0, 1.25, 0]
|
||||||
|
},
|
||||||
|
"bones": [
|
||||||
|
{
|
||||||
|
"name": "bipedHead",
|
||||||
|
"pivot": [0, 24, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorHead",
|
||||||
|
"parent": "bipedHead",
|
||||||
|
"pivot": [0, 24, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bandana",
|
||||||
|
"parent": "armorHead",
|
||||||
|
"pivot": [-0.05, 29.00016, 0.97874],
|
||||||
|
"rotation": [-7.5, 0, 0],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-1.25, 27.71352, 4.37616],
|
||||||
|
"size": [2.2, 2.2, 0.825],
|
||||||
|
"pivot": [-0.07971, 28.0568, 3.054],
|
||||||
|
"rotation": [-10, 0, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [4, 9.66667], "uv_size": [4, 9.5]},
|
||||||
|
"east": {"uv": [96, 48], "uv_size": [16, 48]},
|
||||||
|
"south": {"uv": [48, 48], "uv_size": [48, 48]},
|
||||||
|
"west": {"uv": [32, 48], "uv_size": [16, 48]},
|
||||||
|
"up": {"uv": [96, 48], "uv_size": [-48, -16]},
|
||||||
|
"down": {"uv": [96, 112], "uv_size": [-48, -16]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [-3.65, 28, 3.5],
|
||||||
|
"size": [8.5, 2.25, 1.45],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [256, 0], "uv_size": [256, 48]},
|
||||||
|
"east": {"uv": [15, 20], "uv_size": [1, 2]},
|
||||||
|
"south": {"uv": [256, 144], "uv_size": [227, 48]},
|
||||||
|
"west": {"uv": [483, 96], "uv_size": [29, 48]},
|
||||||
|
"up": {"uv": [224, 256], "uv_size": [-224, -32]},
|
||||||
|
"down": {"uv": [32, 224], "uv_size": [224, 32]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [-11.4, 28, -3.5],
|
||||||
|
"size": [8.5, 2.25, 1.45],
|
||||||
|
"pivot": [-3.9, 28.25, -3.5],
|
||||||
|
"rotation": [0, 180, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [256, 0], "uv_size": [256, 48]},
|
||||||
|
"east": {"uv": [15, 20], "uv_size": [1, 2]},
|
||||||
|
"south": {"uv": [256, 0], "uv_size": [227, 48]},
|
||||||
|
"west": {"uv": [482, 48], "uv_size": [30, 47]},
|
||||||
|
"up": {"uv": [32, 0], "uv_size": [224, 32]},
|
||||||
|
"down": {"uv": [224, 32], "uv_size": [-224, -32]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [-4.15, 28, -12.45],
|
||||||
|
"size": [1.25, 2.25, 8.45],
|
||||||
|
"pivot": [-3.9, 28.25, -3.75],
|
||||||
|
"rotation": [0, 180, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [483, 144], "uv_size": [29, 48]},
|
||||||
|
"east": {"uv": [256, 48], "uv_size": [256, 48]},
|
||||||
|
"south": {"uv": [256, 48], "uv_size": [231, 48]},
|
||||||
|
"west": {"uv": [256, 48], "uv_size": [226, 47]},
|
||||||
|
"up": {"uv": [224, 32], "uv_size": [32, 224]},
|
||||||
|
"down": {"uv": [32, 256], "uv_size": [-32, -224]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [3.6, 28, -4.95],
|
||||||
|
"size": [1.25, 2.25, 8.45],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [483, 0], "uv_size": [29, 48]},
|
||||||
|
"east": {"uv": [256, 96], "uv_size": [256, 48]},
|
||||||
|
"south": {"uv": [256, 48], "uv_size": [231, 48]},
|
||||||
|
"west": {"uv": [257, 96], "uv_size": [226, 48]},
|
||||||
|
"up": {"uv": [32, 224], "uv_size": [-32, -224]},
|
||||||
|
"down": {"uv": [224, 0], "uv_size": [32, 224]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bandana_ribbon",
|
||||||
|
"parent": "bandana",
|
||||||
|
"pivot": [-0.15, 28.63698, 2.80196],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [1.12389, 27.61925, 4.2772],
|
||||||
|
"size": [2.36, 2.065, 0.295],
|
||||||
|
"pivot": [-0.0746, 27.88698, 2.80196],
|
||||||
|
"rotation": [-1.88319, -18.14976, 6.79956],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [16, 272], "uv_size": [144, 48]},
|
||||||
|
"east": {"uv": [0, 272], "uv_size": [16, 48]},
|
||||||
|
"south": {"uv": [464, 272], "uv_size": [144, 48]},
|
||||||
|
"west": {"uv": [160, 272], "uv_size": [16, 48]},
|
||||||
|
"up": {"uv": [304, 272], "uv_size": [-144, -16]},
|
||||||
|
"down": {"uv": [592, 272], "uv_size": [-144, -16]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [3.37774, 28.06097, 3.3012],
|
||||||
|
"size": [2.36, 2.065, 0.295],
|
||||||
|
"pivot": [0.03114, 27.30877, 2.79839],
|
||||||
|
"rotation": [-6.70517, -34.2775, 16.56847],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [160, 272], "uv_size": [144, 48]},
|
||||||
|
"east": {"uv": [144, 272], "uv_size": [16, 48]},
|
||||||
|
"south": {"uv": [320, 272], "uv_size": [144, 48]},
|
||||||
|
"west": {"uv": [304, 272], "uv_size": [16, 48]},
|
||||||
|
"up": {"uv": [160, 272], "uv_size": [-144, -16]},
|
||||||
|
"down": {"uv": [448, 272], "uv_size": [-144, -16]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [-6.03774, 28.06097, 3.3012],
|
||||||
|
"size": [2.36, 2.065, 0.295],
|
||||||
|
"pivot": [-0.33114, 27.30877, 2.79839],
|
||||||
|
"rotation": [-6.70517, 34.2775, -16.56847],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [304, 272], "uv_size": [-144, 48]},
|
||||||
|
"east": {"uv": [320, 272], "uv_size": [-16, 48]},
|
||||||
|
"south": {"uv": [464, 272], "uv_size": [-144, 48]},
|
||||||
|
"west": {"uv": [160, 272], "uv_size": [-16, 48]},
|
||||||
|
"up": {"uv": [16, 272], "uv_size": [144, -16]},
|
||||||
|
"down": {"uv": [304, 272], "uv_size": [144, -16]}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"origin": [-3.78389, 27.61925, 4.2772],
|
||||||
|
"size": [2.36, 2.065, 0.295],
|
||||||
|
"pivot": [-0.2254, 27.88698, 2.80196],
|
||||||
|
"rotation": [-1.88319, 18.14976, -6.79956],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [160, 272], "uv_size": [-144, 48]},
|
||||||
|
"east": {"uv": [176, 272], "uv_size": [-16, 48]},
|
||||||
|
"south": {"uv": [608, 272], "uv_size": [-144, 48]},
|
||||||
|
"west": {"uv": [16, 272], "uv_size": [-16, 48]},
|
||||||
|
"up": {"uv": [160, 272], "uv_size": [144, -16]},
|
||||||
|
"down": {"uv": [448, 272], "uv_size": [144, -16]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedBody",
|
||||||
|
"pivot": [0, 24, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorBody",
|
||||||
|
"parent": "bipedBody",
|
||||||
|
"pivot": [-0.05, 24, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedRightArm",
|
||||||
|
"pivot": [-4, 22, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightArm",
|
||||||
|
"parent": "bipedRightArm",
|
||||||
|
"pivot": [-4, 22, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedLeftArm",
|
||||||
|
"pivot": [4, 22, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorLeftArm",
|
||||||
|
"parent": "bipedLeftArm",
|
||||||
|
"pivot": [4, 22, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedLeftLeg",
|
||||||
|
"pivot": [2, 12, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorLeftLeg",
|
||||||
|
"parent": "bipedLeftLeg",
|
||||||
|
"pivot": [2, 12, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorLeftBoot",
|
||||||
|
"parent": "bipedLeftLeg",
|
||||||
|
"pivot": [2, 12, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedRightLeg",
|
||||||
|
"pivot": [-2, 12, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightLeg",
|
||||||
|
"parent": "bipedRightLeg",
|
||||||
|
"pivot": [-2, 12, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightBoot",
|
||||||
|
"parent": "bipedRightLeg",
|
||||||
|
"pivot": [-2, 12, 0]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
{
|
||||||
|
"format_version": "1.12.0",
|
||||||
|
"minecraft:geometry": [
|
||||||
|
{
|
||||||
|
"description": {
|
||||||
|
"identifier": "geometry.unknown",
|
||||||
|
"texture_width": 64,
|
||||||
|
"texture_height": 64,
|
||||||
|
"visible_bounds_width": 8,
|
||||||
|
"visible_bounds_height": 5.5,
|
||||||
|
"visible_bounds_offset": [0, 2.25, 0]
|
||||||
|
},
|
||||||
|
"bones": [
|
||||||
|
{
|
||||||
|
"name": "bb_main",
|
||||||
|
"pivot": [0, 0, 0]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedHead",
|
||||||
|
"pivot": [0, 24.99, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorHead",
|
||||||
|
"parent": "bipedHead",
|
||||||
|
"pivot": [0, 24.99, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedBody",
|
||||||
|
"pivot": [0, 24.99, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorBody",
|
||||||
|
"parent": "bipedBody",
|
||||||
|
"pivot": [0, 24.99, 1.12857],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-4, 12, -2],
|
||||||
|
"size": [8, 12, 4.1],
|
||||||
|
"uv": {
|
||||||
|
"south": {"uv": [54.5, 53.5], "uv_size": [6.5, 9]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dark_wings",
|
||||||
|
"parent": "armorBody",
|
||||||
|
"pivot": [-4, 22, 4.3]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "right",
|
||||||
|
"parent": "dark_wings",
|
||||||
|
"pivot": [1.3, 23, 1.9]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "L2",
|
||||||
|
"parent": "right",
|
||||||
|
"pivot": [1.3, 23, 1.9],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [0, 15, 3.2],
|
||||||
|
"size": [0, 14, 3.1],
|
||||||
|
"pivot": [0, 26, 1.85],
|
||||||
|
"rotation": [0, 90, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [0, 55], "uv_size": [11, 8.5]},
|
||||||
|
"east": {"uv": [64, 55], "uv_size": [-6, 8.5]},
|
||||||
|
"south": {"uv": [0, 55], "uv_size": [11, 8.5]},
|
||||||
|
"west": {"uv": [35.16, 13.94], "uv_size": [-4.64, 18]},
|
||||||
|
"up": {"uv": [0, 57.55], "uv_size": [11, -2.55]},
|
||||||
|
"down": {"uv": [0, 55], "uv_size": [11, 2.55]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "L3",
|
||||||
|
"parent": "L2",
|
||||||
|
"pivot": [4.45, 23, 1.86029],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [-2, 15, 8.2],
|
||||||
|
"size": [0, 14, 10.1],
|
||||||
|
"pivot": [-2, 26, 1.85],
|
||||||
|
"rotation": [0, 90, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [0, 55], "uv_size": [11, 8.5]},
|
||||||
|
"east": {"uv": [53, 55.5], "uv_size": [7.7, 8.5]},
|
||||||
|
"south": {"uv": [0, 55], "uv_size": [11, 8.5]},
|
||||||
|
"west": {"uv": [30.544, 13.94], "uv_size": [-12.664, 18]},
|
||||||
|
"up": {"uv": [0, 63.5], "uv_size": [11, -5.95]},
|
||||||
|
"down": {"uv": [0, 57.55], "uv_size": [11, 5.95]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "left",
|
||||||
|
"parent": "dark_wings",
|
||||||
|
"pivot": [-1.3, 23, 1.8]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "R2",
|
||||||
|
"parent": "left",
|
||||||
|
"pivot": [-1.3, 23, 1.8],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [0, 15, -2.6],
|
||||||
|
"size": [0, 14, 3.1],
|
||||||
|
"pivot": [0, 26, 1.85],
|
||||||
|
"rotation": [0, 90, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [11, 55], "uv_size": [-11, 8.5]},
|
||||||
|
"east": {"uv": [64, 55.5], "uv_size": [-6, 8.5]},
|
||||||
|
"south": {"uv": [11, 55], "uv_size": [-11, 8.5]},
|
||||||
|
"west": {"uv": [30.52, 13.94], "uv_size": [4.64, 18]},
|
||||||
|
"up": {"uv": [0, 55], "uv_size": [11, 2.55]},
|
||||||
|
"down": {"uv": [0, 57.55], "uv_size": [11, -2.55]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "R3",
|
||||||
|
"parent": "R2",
|
||||||
|
"pivot": [-4.45, 23, 1.86029],
|
||||||
|
"cubes": [
|
||||||
|
{
|
||||||
|
"origin": [2, 15, -14.6],
|
||||||
|
"size": [0, 14, 10.1],
|
||||||
|
"pivot": [2, 26, 1.85],
|
||||||
|
"rotation": [0, 90, 0],
|
||||||
|
"uv": {
|
||||||
|
"north": {"uv": [11, 55], "uv_size": [-11, 8.5]},
|
||||||
|
"east": {"uv": [60.7, 55.5], "uv_size": [-7.7, 8.5]},
|
||||||
|
"south": {"uv": [11, 55], "uv_size": [-11, 8.5]},
|
||||||
|
"west": {"uv": [17.88, 13.94], "uv_size": [12.664, 18]},
|
||||||
|
"up": {"uv": [0, 57.55], "uv_size": [11, 5.95]},
|
||||||
|
"down": {"uv": [0, 63.5], "uv_size": [11, -5.95]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedRightArm",
|
||||||
|
"pivot": [3.28, 23.35, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightArm",
|
||||||
|
"parent": "bipedRightArm",
|
||||||
|
"pivot": [3.28, 23.35, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedLeftLeg",
|
||||||
|
"pivot": [-1.64, 15.15, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorLeftLeg",
|
||||||
|
"parent": "bipedLeftLeg",
|
||||||
|
"pivot": [-1.64, 15.15, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorLeftBoot",
|
||||||
|
"parent": "bipedLeftLeg",
|
||||||
|
"pivot": [-1.64, 15.15, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bipedRightLeg",
|
||||||
|
"pivot": [1.64, 15.15, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightLeg",
|
||||||
|
"parent": "bipedRightLeg",
|
||||||
|
"pivot": [1.64, 15.15, 1.12857]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "armorRightBoot",
|
||||||
|
"parent": "bipedRightLeg",
|
||||||
|
"pivot": [1.64, 15.15, 1.12857]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user