diff --git a/.gitignore b/.gitignore index a7e4258..1d01148 100644 --- a/.gitignore +++ b/.gitignore @@ -1,45 +1,38 @@ -# ---> Java -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # +# Gradle +build/ +.gradle/ *.jar *.war *.nar *.ear -*.zip -*.tar.gz -*.rar -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +# IDE +.idea/ +*.iml +*.ipr +*.iws +.vscode/ +.settings/ +.project +.classpath +bin/ + +# OS +.DS_Store +Thumbs.db +*.swp +*.swo +*~ + +# Env +.env +.env.local + +# Logs +*.log hs_err_pid* replay_pid* -# ---> Maven -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar - -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) -.classpath - +# Misc +*.ctxt +.mtj.tmp/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..bde5e8b --- /dev/null +++ b/build.gradle @@ -0,0 +1,58 @@ +plugins { + id "fabric-loom" version "1.9.1" + id "maven-publish" +} + +def mcVersion = project.findProperty("mcVersion") ?: "1.21.1" + +def ver = [ + "1.20.1": [mappings: "1.20.1+build.10", loader: "0.15.11", fabricApi: "0.92.0+1.20.1"], + "1.21": [mappings: "1.21+build.8", loader: "0.15.11", fabricApi: "0.100.0+1.21"], + "1.21.1": [mappings: "1.21.1+build.3", loader: "0.15.11", fabricApi: "0.101.2+1.21.1"], +] + +def v = ver[mcVersion] +if (v == null) throw new GradleException("Unsupported MC version: $mcVersion") + +version = "${project.findProperty("modVersion") ?: "1.0.0"}+mc${mcVersion}" +group = project.findProperty("mavenGroup") ?: "net.anomaly" + +base { + archivesName = "JustASplash" +} + +repositories { + mavenCentral() + maven { url "https://maven.fabricmc.net/" } + +} + +dependencies { + minecraft "com.mojang:minecraft:${mcVersion}" + mappings "net.fabricmc:yarn:${v.mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${v.loader}" + modImplementation "net.fabricmc.fabric-api:fabric-api:${v.fabricApi}" + + modImplementation("com.github.usefulness:webp-imageio:0.10.2") + include("com.github.usefulness:webp-imageio:0.10.2") +} + +loom { + accessWidenerPath = file("src/main/resources/justasplash.accesswidener") +} + +processResources { + inputs.property "version", project.version + filesMatching("fabric.mod.json") { + expand "version": project.version + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType(JavaCompile).configureEach { + it.options.release = 17 +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..24460d8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx2G -XX:+UseG1GC +org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..cea7a79 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f3b75f3 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/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\n' "$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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9b42019 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..56d675e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + mavenCentral() + maven { url "https://maven.fabricmc.net/" } + maven { url "https://maven.quiltmc.org/repository/release/" } + gradlePluginPortal() + } +} + +rootProject.name = "JustASplash" diff --git a/src/main/java/net/anomaly/justasplash/JustASplash.java b/src/main/java/net/anomaly/justasplash/JustASplash.java new file mode 100644 index 0000000..721863f --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/JustASplash.java @@ -0,0 +1,35 @@ +package net.anomaly.justasplash; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.Identifier; +import net.anomaly.justasplash.config.SplashConfig; +import net.anomaly.justasplash.splash.SplashAssetCache; +import net.anomaly.justasplash.splash.SplashKeybind; +import net.anomaly.justasplash.splash.SplashManager; +import net.anomaly.justasplash.splash.SplashSound; +import net.anomaly.justasplash.util.Compat; + +public class JustASplash implements ClientModInitializer { + public static final Identifier SPLASH_SOUND_ID = Compat.id("justasplash", "splash"); + public static SoundEvent SPLASH_SOUND_EVENT; + + @Override + public void onInitializeClient() { + SPLASH_SOUND_EVENT = SoundEvent.of(SPLASH_SOUND_ID); + Registry.register(Registries.SOUND_EVENT, SPLASH_SOUND_ID, SPLASH_SOUND_EVENT); + + SplashConfig.load(); + SplashAssetCache.preload(); + SplashKeybind.register(); + SplashSound.setup(); + + ClientTickEvents.END_CLIENT_TICK.register(client -> { + if (SplashKeybind.wasPressed()) SplashManager.show(); + SplashManager.tick(); + }); + } +} diff --git a/src/main/java/net/anomaly/justasplash/config/SplashConfig.java b/src/main/java/net/anomaly/justasplash/config/SplashConfig.java new file mode 100644 index 0000000..75112cd --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/config/SplashConfig.java @@ -0,0 +1,53 @@ +package net.anomaly.justasplash.config; + +import java.nio.file.Files; +import java.nio.file.Path; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import net.fabricmc.loader.api.FabricLoader; + +public class SplashConfig { + public String splashFile = "splash.png"; + public String soundFile = "splash.ogg"; + public float duration = 3.0f; + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + private static SplashConfig instance; + + public static SplashConfig get() { + if (instance == null) instance = load(); + return instance; + } + + public static SplashConfig load() { + Path path = getConfigPath(); + if (Files.exists(path)) { + try { + instance = GSON.fromJson(Files.readString(path), SplashConfig.class); + } catch (Exception e) { + instance = new SplashConfig(); + } + } else { + instance = new SplashConfig(); + } + save(); + return instance; + } + + public static void save() { + try { + Files.writeString(getConfigPath(), GSON.toJson(instance)); + } catch (Exception ignored) { + } + } + + public static Path getConfigPath() { + return FabricLoader.getInstance().getConfigDir().resolve("justasplash.json"); + } + + public static Path getAssetDir() { + return FabricLoader.getInstance().getConfigDir().resolve("justasplash"); + } +} diff --git a/src/main/java/net/anomaly/justasplash/image/AnimatedWebP.java b/src/main/java/net/anomaly/justasplash/image/AnimatedWebP.java new file mode 100644 index 0000000..6d613a8 --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/image/AnimatedWebP.java @@ -0,0 +1,134 @@ +package net.anomaly.justasplash.image; + +import net.minecraft.client.texture.NativeImage; + +import org.w3c.dom.Element; + +import javax.imageio.*; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.IIORegistry; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class AnimatedWebP implements SplashImage { + private final List frames = new ArrayList<>(); + private final int[] delays; + private final int totalDuration; + + public AnimatedWebP(Path path) throws IOException { + registerWebPIfNeeded(); + + try (InputStream in = Files.newInputStream(path); + ImageInputStream iis = ImageIO.createImageInputStream(in)) { + + Iterator readers = ImageIO.getImageReadersByFormatName("webp"); + if (!readers.hasNext()) { + throw new IOException("No WebP reader available"); + } + + ImageReader reader = readers.next(); + reader.setInput(iis); + + int numFrames = reader.getNumImages(true); + delays = new int[numFrames]; + int total = 0; + + for (int i = 0; i < numFrames; i++) { + BufferedImage bi = reader.read(i); + delays[i] = readFrameDelay(reader, i); + total += delays[i]; + frames.add(toNativeImage(bi)); + } + totalDuration = total; + reader.dispose(); + } + } + + private static boolean webpRegistered; + + private static void registerWebPIfNeeded() { + if (webpRegistered) return; + if (ImageIO.getImageReadersByFormatName("webp").hasNext()) { + webpRegistered = true; + return; + } + try { + Class spiClass = Class.forName("com.luciad.imageio.webp.WebPImageReaderSpi"); + IIORegistry registry = IIORegistry.getDefaultInstance(); + registry.registerServiceProvider(spiClass.getDeclaredConstructor().newInstance()); + webpRegistered = true; + } catch (Exception e) { + throw new RuntimeException("Failed to register WebP reader", e); + } + } + + private static int readFrameDelay(ImageReader reader, int index) { + try { + IIOMetadata meta = reader.getImageMetadata(index); + String[] names = meta.getMetadataFormatNames(); + if (names != null) { + for (String fmt : names) { + if (fmt.contains("webp") || fmt.contains("WEBP")) { + Element root = (Element) meta.getAsTree(fmt); + String delay = root.getElementsByTagName("delay") + .item(0).getTextContent(); + return Integer.parseInt(delay); + } + } + } + } catch (Exception ignored) { + } + return 100; + } + + private static NativeImage toNativeImage(BufferedImage bi) { + NativeImage ni = new NativeImage(NativeImage.Format.RGBA, bi.getWidth(), bi.getHeight(), false); + int[] rgb = bi.getRGB(0, 0, bi.getWidth(), bi.getHeight(), null, 0, bi.getWidth()); + for (int y = 0; y < bi.getHeight(); y++) { + for (int x = 0; x < bi.getWidth(); x++) { + int argb = rgb[y * bi.getWidth() + x]; + int a = (argb >> 24) & 0xFF; + int r = (argb >> 16) & 0xFF; + int g = (argb >> 8) & 0xFF; + int b = argb & 0xFF; + int color = (a & 0xFF) << 24 | (b & 0xFF) << 16 | (g & 0xFF) << 8 | (r & 0xFF); + ni.setColor(x, y, color); + } + } + return ni; + } + + @Override + public NativeImage getFrame(int index) { + return index >= 0 && index < frames.size() ? frames.get(index) : null; + } + + @Override + public int getFrameCount() { + return frames.size(); + } + + @Override + public boolean isAnimated() { + return frames.size() > 1; + } + + @Override + public int getFrameIndex(long timeMillis) { + if (frames.size() <= 1) return 0; + long elapsed = timeMillis % totalDuration; + long accum = 0; + for (int i = 0; i < delays.length; i++) { + accum += delays[i]; + if (elapsed < accum) return i; + } + return delays.length - 1; + } +} diff --git a/src/main/java/net/anomaly/justasplash/image/SplashImage.java b/src/main/java/net/anomaly/justasplash/image/SplashImage.java new file mode 100644 index 0000000..8cf7b54 --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/image/SplashImage.java @@ -0,0 +1,10 @@ +package net.anomaly.justasplash.image; + +import net.minecraft.client.texture.NativeImage; + +public interface SplashImage { + NativeImage getFrame(int index); + int getFrameCount(); + boolean isAnimated(); + int getFrameIndex(long timeMillis); +} diff --git a/src/main/java/net/anomaly/justasplash/image/StaticImage.java b/src/main/java/net/anomaly/justasplash/image/StaticImage.java new file mode 100644 index 0000000..3e002ac --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/image/StaticImage.java @@ -0,0 +1,38 @@ +package net.anomaly.justasplash.image; + +import net.minecraft.client.texture.NativeImage; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public class StaticImage implements SplashImage { + private final NativeImage image; + + public StaticImage(Path path) throws IOException { + try (InputStream in = Files.newInputStream(path)) { + this.image = NativeImage.read(in); + } + } + + @Override + public NativeImage getFrame(int index) { + return index == 0 ? image : null; + } + + @Override + public int getFrameCount() { + return 1; + } + + @Override + public boolean isAnimated() { + return false; + } + + @Override + public int getFrameIndex(long timeMillis) { + return 0; + } +} diff --git a/src/main/java/net/anomaly/justasplash/mixin/WindowMixin.java b/src/main/java/net/anomaly/justasplash/mixin/WindowMixin.java new file mode 100644 index 0000000..7946622 --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/mixin/WindowMixin.java @@ -0,0 +1,16 @@ +package net.anomaly.justasplash.mixin; + +import net.anomaly.justasplash.splash.SplashManager; +import net.minecraft.client.util.Window; +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(Window.class) +public class WindowMixin { + @Inject(method = "swapBuffers", at = @At("HEAD")) + private void onSwapBuffers(CallbackInfo ci) { + SplashManager.renderOverlay(); + } +} diff --git a/src/main/java/net/anomaly/justasplash/splash/SplashAssetCache.java b/src/main/java/net/anomaly/justasplash/splash/SplashAssetCache.java new file mode 100644 index 0000000..8f6bac5 --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/splash/SplashAssetCache.java @@ -0,0 +1,146 @@ +package net.anomaly.justasplash.splash; + +import net.anomaly.justasplash.config.SplashConfig; +import net.anomaly.justasplash.image.AnimatedWebP; +import net.anomaly.justasplash.image.SplashImage; +import net.anomaly.justasplash.image.StaticImage; +import net.anomaly.justasplash.util.Compat; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.resource.Resource; +import net.minecraft.util.Identifier; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +public class SplashAssetCache { + private static Identifier textureId; + private static NativeImageBackedTexture texture; + private static SplashImage splashImage; + + public static void preload() { + SplashConfig config = SplashConfig.get(); + load(config); + } + + public static void load(SplashConfig config) { + if (texture != null && splashImage != null) return; + + String splashFile = config.splashFile; + Path customPath = config.getAssetDir().resolve(splashFile); + + try { + if (Files.exists(customPath)) { + loadFromPath(customPath); + } else { + loadFromResources(splashFile); + } + } catch (Exception e) { + createFallbackTexture(); + } + } + + private static void loadFromPath(Path path) throws Exception { + String name = path.getFileName().toString().toLowerCase(); + if (name.endsWith(".webp")) { + splashImage = new AnimatedWebP(path); + } else { + splashImage = new StaticImage(path); + } + registerFirstFrame(); + } + + private static void loadFromResources(String splashFile) throws Exception { + Identifier resId = Compat.id("justasplash", "default/" + splashFile); + MinecraftClient client = MinecraftClient.getInstance(); + Optional opt = client.getResourceManager().getResource(resId); + + if (opt.isPresent()) { + Path temp = Files.createTempFile("jas_", splashFile); + try (InputStream in = opt.get().getInputStream()) { + Files.copy(in, temp, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + loadFromPath(temp); + Files.deleteIfExists(temp); + } else { + loadFromClasspath(splashFile); + } + } + + private static void loadFromClasspath(String splashFile) throws Exception { + try (InputStream in = SplashAssetCache.class.getResourceAsStream( + "/assets/justasplash/default/" + splashFile)) { + if (in != null) { + NativeImage raw = NativeImage.read(in); + splashImage = new SplashImage() { + private final NativeImage img = raw; + @Override public NativeImage getFrame(int i) { return i == 0 ? img : null; } + @Override public int getFrameCount() { return 1; } + @Override public boolean isAnimated() { return false; } + @Override public int getFrameIndex(long t) { return 0; } + }; + registerFirstFrame(); + } else { + throw new IOException("Asset not found: " + splashFile); + } + } + } + + private static void registerFirstFrame() { + NativeImage frame = splashImage.getFrame(0); + if (frame == null) throw new RuntimeException("No initial frame"); + + texture = Compat.createTexture(frame); + textureId = Compat.id("justasplash", "splash_tex"); + MinecraftClient.getInstance().getTextureManager().registerTexture(textureId, texture); + } + + public static Identifier getTexture() { + if (texture == null) return null; + + if (splashImage != null && splashImage.isAnimated()) { + long now = System.currentTimeMillis(); + int idx = splashImage.getFrameIndex(now); + NativeImage frame = splashImage.getFrame(idx); + if (frame != null) { + updateTexture(frame); + } + } + return textureId; + } + + private static void updateTexture(NativeImage frame) { + try { + NativeImage current = Compat.getImage(texture); + if (current != null && current.getWidth() == frame.getWidth() + && current.getHeight() == frame.getHeight()) { + int h = frame.getHeight(), w = frame.getWidth(); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + current.setColor(x, y, frame.getColor(x, y)); + } + } + Compat.uploadTexture(texture); + } + } catch (Exception ignored) { + } + } + + private static void createFallbackTexture() { + NativeImage fb = new NativeImage(NativeImage.Format.RGBA, 1, 1, false); + fb.setColor(0, 0, Compat.packPixel(255, 0, 255, 255)); + texture = Compat.createTexture(fb); + textureId = Compat.id("justasplash", "splash_fallback"); + MinecraftClient.getInstance().getTextureManager().registerTexture(textureId, texture); + } + + public static void invalidate() { + texture = null; + textureId = null; + splashImage = null; + } +} diff --git a/src/main/java/net/anomaly/justasplash/splash/SplashKeybind.java b/src/main/java/net/anomaly/justasplash/splash/SplashKeybind.java new file mode 100644 index 0000000..16b390a --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/splash/SplashKeybind.java @@ -0,0 +1,23 @@ +package net.anomaly.justasplash.splash; + +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 SplashKeybind { + private static final KeyBinding KEY = new KeyBinding( + "key.justasplash.splash", + InputUtil.Type.KEYSYM, + GLFW.GLFW_KEY_F6, + "category.justasplash" + ); + + public static void register() { + KeyBindingHelper.registerKeyBinding(KEY); + } + + public static boolean wasPressed() { + return KEY.wasPressed(); + } +} diff --git a/src/main/java/net/anomaly/justasplash/splash/SplashManager.java b/src/main/java/net/anomaly/justasplash/splash/SplashManager.java new file mode 100644 index 0000000..78ee00e --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/splash/SplashManager.java @@ -0,0 +1,44 @@ +package net.anomaly.justasplash.splash; + +import net.anomaly.justasplash.config.SplashConfig; + +public class SplashManager { + private static State state = State.IDLE; + private static float alpha = 0f; + private static long startTime = 0; + private static float duration = 3.0f; + + enum State { IDLE, SHOWING } + + public static void show() { + if (state == State.SHOWING) return; + + SplashAssetCache.load(SplashConfig.get()); + state = State.SHOWING; + alpha = 1.0f; + startTime = System.currentTimeMillis(); + duration = SplashConfig.get().duration; + SplashSound.play(); + } + + public static void tick() { + if (state != State.SHOWING) return; + + float elapsed = (System.currentTimeMillis() - startTime) / 1000f; + alpha = 1.0f - (elapsed / duration); + + if (alpha <= 0f) { + alpha = 0f; + state = State.IDLE; + SplashSound.stop(); + } + } + + public static void renderOverlay() { + if (state != State.SHOWING) return; + if (alpha <= 0f) return; + SplashRenderer.render(alpha); + } + + public static boolean isShowing() { return state == State.SHOWING; } +} diff --git a/src/main/java/net/anomaly/justasplash/splash/SplashRenderer.java b/src/main/java/net/anomaly/justasplash/splash/SplashRenderer.java new file mode 100644 index 0000000..ab6a911 --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/splash/SplashRenderer.java @@ -0,0 +1,249 @@ +package net.anomaly.justasplash.splash; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.Identifier; + +import java.lang.reflect.Method; + +public class SplashRenderer { + private static boolean modern; + private static Object tessellatorInstance; + private static Method tessellatorBegin; + private static Method tessellatorDraw; + private static Object quadsConstant; + private static Object positionTextureColorFormat; + private static Method vertexMethod; + private static Method textureMethod; + private static Method colorMethod; + private static Method nextMethod; + private static Object shaderSupplier; + + static { + try { + Class.forName("net.minecraft.client.gui.DrawContext"); + modern = true; + initModern(); + } catch (ClassNotFoundException e) { + modern = false; + initLegacy(); + } + } + + private static void initModern() { + try { + Class tessellatorClass = Class.forName("net.minecraft.client.render.Tessellator"); + tessellatorInstance = tessellatorClass.getMethod("getInstance").invoke(null); + tessellatorDraw = tessellatorClass.getMethod("draw"); + + Class vertexFormatClass = Class.forName("net.minecraft.client.render.VertexFormat"); + Class drawModeClass = findInnerClass(vertexFormatClass, "DrawMode"); + for (Object c : drawModeClass.getEnumConstants()) { + if (c.toString().equals("QUADS")) { + quadsConstant = c; + break; + } + } + + Class vertexFormatsClass = Class.forName("net.minecraft.client.render.VertexFormats"); + positionTextureColorFormat = vertexFormatsClass.getField("POSITION_TEXTURE_COLOR").get(null); + + for (Method m : tessellatorClass.getMethods()) { + if (m.getName().equals("begin") && m.getParameterCount() == 2) { + tessellatorBegin = m; + break; + } + } + + Class bufferBuilderClass = Class.forName("net.minecraft.client.render.BufferBuilder"); + vertexMethod = findMethodByName(bufferBuilderClass, "vertex"); + if (vertexMethod != null) { + Class vertexConsumerClass = vertexMethod.getReturnType(); + textureMethod = findMethodByName(vertexConsumerClass, "texture"); + colorMethod = findMethodByName(vertexConsumerClass, "color"); + nextMethod = findMethodByName(vertexConsumerClass, "next"); + } + } catch (Exception e) { + throw new RuntimeException("Failed to init modern renderer", e); + } + } + + private static void initLegacy() { + try { + Class tessellatorClass = Class.forName("com.mojang.blaze3d.vertex.Tessellator"); + tessellatorInstance = tessellatorClass.getMethod("getInstance").invoke(null); + tessellatorDraw = tessellatorClass.getMethod("draw"); + + Class vertexFormatClass = Class.forName("com.mojang.blaze3d.vertex.VertexFormat"); + Class drawModeClass = findInnerClass(vertexFormatClass, "DrawMode"); + for (Object c : drawModeClass.getEnumConstants()) { + if (c.toString().equals("QUADS")) { + quadsConstant = c; + break; + } + } + + Class vertexFormatsClass = Class.forName("com.mojang.blaze3d.vertex.VertexFormats"); + positionTextureColorFormat = vertexFormatsClass.getField("POSITION_TEXTURE_COLOR").get(null); + + for (Method m : tessellatorClass.getMethods()) { + if (m.getName().equals("begin") && m.getParameterCount() == 2) { + tessellatorBegin = m; + break; + } + } + + Class bufferBuilderClass = Class.forName("com.mojang.blaze3d.vertex.BufferBuilder"); + vertexMethod = findMethodByName(bufferBuilderClass, "vertex"); + if (vertexMethod != null) { + Class vertexConsumerClass = vertexMethod.getReturnType(); + textureMethod = findMethodByName(vertexConsumerClass, "texture"); + colorMethod = findMethodByName(vertexConsumerClass, "color"); + nextMethod = findMethodByName(vertexConsumerClass, "next"); + } + } catch (Exception e) { + throw new RuntimeException("Failed to init legacy renderer", e); + } + } + + private static Class findInnerClass(Class parent, String name) { + for (Class inner : parent.getDeclaredClasses()) { + if (inner.getSimpleName().equals(name)) return inner; + } + return null; + } + + private static Method findMethodByName(Class clazz, String name) { + for (Method m : clazz.getMethods()) { + if (m.getName().equals(name)) return m; + } + return null; + } + + private static boolean shaderChecked; + + private static Object getShader() { + if (!shaderChecked) { + shaderChecked = true; + shaderSupplier = findShader(); + } + return shaderSupplier; + } + + private static Object findShader() { + for (String name : new String[]{"getPositionTexColorShader", "getPositionTextureColorShader"}) { + try { + Class gameRendererClass = MinecraftClient.class.getField("gameRenderer").getType(); + Method m = gameRendererClass.getDeclaredMethod(name); + m.setAccessible(true); + + Object test = m.invoke(null); + if (test != null) { + Class shaderClass = findShaderClass(); + if (shaderClass != null && shaderClass.isInstance(test)) { + return (java.util.function.Supplier) () -> { + try { return m.invoke(null); } + catch (Exception e) { return null; } + }; + } + } + } catch (Exception ignored) { + } + + try { + Class gameRendererClass = MinecraftClient.class.getField("gameRenderer").getType(); + Method m = gameRendererClass.getDeclaredMethod(name); + m.setAccessible(true); + MinecraftClient client = MinecraftClient.getInstance(); + if (client != null && client.gameRenderer != null) { + Object test; + try { + test = m.invoke(client.gameRenderer); + } catch (Exception e) { + test = m.invoke(gameRendererClass.cast(client.gameRenderer)); + } + if (test != null) { + Class shaderClass = findShaderClass(); + if (shaderClass != null && shaderClass.isInstance(test)) { + return (java.util.function.Supplier) () -> { + try { return m.invoke(MinecraftClient.getInstance().gameRenderer); } + catch (Exception e) { return null; } + }; + } + } + } + } catch (Exception ignored) { + } + } + return null; + } + + private static Class findShaderClass() { + try { + return Class.forName("net.minecraft.client.gl.ShaderProgram"); + } catch (ClassNotFoundException e) { + try { + return Class.forName("net.minecraft.client.render.ShaderProgram"); + } catch (ClassNotFoundException e2) { + return null; + } + } + } + + public static void render(float alpha) { + Identifier tex = SplashAssetCache.getTexture(); + if (tex == null) return; + + MinecraftClient client = MinecraftClient.getInstance(); + int w = client.getWindow().getScaledWidth(); + int h = client.getWindow().getScaledHeight(); + + RenderSystem.enableBlend(); + RenderSystem.defaultBlendFunc(); + + Object shader = getShader(); + if (shader != null) { + try { + Class renderSystemClass = RenderSystem.class; + Method setShader = renderSystemClass.getMethod("setShader", java.util.function.Supplier.class); + setShader.invoke(null, shader); + } catch (Exception ignored) { + } + } + RenderSystem.setShaderTexture(0, tex); + + try { + Object builder = tessellatorBegin.invoke(tessellatorInstance, quadsConstant, positionTextureColorFormat); + addVertices(builder, w, h, alpha); + tessellatorDraw.invoke(tessellatorInstance); + } catch (Exception ignored) { + } + + RenderSystem.disableBlend(); + } + + private static void addVertices(Object builder, int w, int h, float alpha) { + try { + Object v1 = vertexMethod.invoke(builder, 0f, 0f, 0f); + textureMethod.invoke(v1, 0f, 0f); + Object c1 = colorMethod.invoke(v1, 1f, 1f, 1f, alpha); + nextMethod.invoke(c1); + + Object v2 = vertexMethod.invoke(builder, 0f, (float) h, 0f); + textureMethod.invoke(v2, 0f, 1f); + Object c2 = colorMethod.invoke(v2, 1f, 1f, 1f, alpha); + nextMethod.invoke(c2); + + Object v3 = vertexMethod.invoke(builder, (float) w, (float) h, 0f); + textureMethod.invoke(v3, 1f, 1f); + Object c3 = colorMethod.invoke(v3, 1f, 1f, 1f, alpha); + nextMethod.invoke(c3); + + Object v4 = vertexMethod.invoke(builder, (float) w, 0f, 0f); + textureMethod.invoke(v4, 1f, 0f); + Object c4 = colorMethod.invoke(v4, 1f, 1f, 1f, alpha); + nextMethod.invoke(c4); + } catch (Exception ignored) { + } + } +} diff --git a/src/main/java/net/anomaly/justasplash/splash/SplashSound.java b/src/main/java/net/anomaly/justasplash/splash/SplashSound.java new file mode 100644 index 0000000..db1aba8 --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/splash/SplashSound.java @@ -0,0 +1,111 @@ +package net.anomaly.justasplash.splash; + +import net.anomaly.justasplash.JustASplash; +import net.anomaly.justasplash.config.SplashConfig; +import net.anomaly.justasplash.util.Compat; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.ResourcePackActivationType; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public class SplashSound { + private static boolean packRegistered; + + public static void setup() { + Path assetDir = SplashConfig.getAssetDir(); + + if (!Files.exists(assetDir)) { + try { + Files.createDirectories(assetDir); + } catch (IOException ignored) { + } + return; + } + + boolean hasCustomOgg; + try (Stream files = Files.list(assetDir)) { + hasCustomOgg = files.anyMatch(p -> p.toString().endsWith(".ogg")); + } catch (IOException e) { + hasCustomOgg = false; + } + + if (hasCustomOgg && !packRegistered) { + ensurePackMeta(assetDir); + ensureAssetStructure(assetDir); + + Compat.registerBuiltinResourcePack( + Compat.id("justasplash", "custom_assets"), + assetDir, + "JustASplash Assets" + ); + packRegistered = true; + } + } + + private static void ensurePackMeta(Path assetDir) { + Path mcmeta = assetDir.resolve("pack.mcmeta"); + if (Files.exists(mcmeta)) return; + + int format = getPackFormat(); + try { + Files.writeString(mcmeta, + "{\"pack\":{\"pack_format\":" + format + ",\"description\":\"JustASplash\"}}"); + } catch (IOException ignored) { + } + } + + private static void ensureAssetStructure(Path assetDir) { + String soundFile = SplashConfig.get().soundFile; + Path src = assetDir.resolve(soundFile); + if (!Files.exists(src)) return; + + try { + Path soundsDir = assetDir.resolve("assets/justasplash/sounds"); + Files.createDirectories(soundsDir); + + Path dst = soundsDir.resolve(soundFile); + if (!Files.exists(dst)) { + Files.copy(src, dst); + } + + String name = soundFile.replaceAll("\\.\\w+$", ""); + Path soundsJson = assetDir.resolve("assets/justasplash/sounds.json"); + if (!Files.exists(soundsJson)) { + String json = "{\"splash\":{\"sounds\":[{\"name\":\"justasplash:" + + name + "\",\"stream\":true}]}}"; + Files.writeString(soundsJson, json); + } + } catch (IOException ignored) { + } + } + + private static int getPackFormat() { + String mv = FabricLoader.getInstance().getModContainer("minecraft") + .map(m -> m.getMetadata().getVersion().getFriendlyString()) + .orElse("1.20.1"); + if (mv.startsWith("1.20")) return 15; + return 18; + } + + public static void play() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) return; + client.getSoundManager().play( + PositionedSoundInstance.master(JustASplash.SPLASH_SOUND_EVENT, 1f, 1f) + ); + } + + public static void stop() { + MinecraftClient client = MinecraftClient.getInstance(); + if (client == null) return; + client.getSoundManager().stopSounds(JustASplash.SPLASH_SOUND_ID, null); + } +} diff --git a/src/main/java/net/anomaly/justasplash/util/Compat.java b/src/main/java/net/anomaly/justasplash/util/Compat.java new file mode 100644 index 0000000..592f4b6 --- /dev/null +++ b/src/main/java/net/anomaly/justasplash/util/Compat.java @@ -0,0 +1,89 @@ +package net.anomaly.justasplash.util; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.util.Identifier; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +public class Compat { + public static Identifier id(String ns, String path) { + try { + Method of = Identifier.class.getMethod("of", String.class, String.class); + return (Identifier) of.invoke(null, ns, path); + } catch (Exception e) { + try { + Constructor c = Identifier.class.getConstructor(String.class, String.class); + return c.newInstance(ns, path); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + } + + public static int packPixel(int r, int g, int b, int a) { + return (a & 0xFF) << 24 | (b & 0xFF) << 16 | (g & 0xFF) << 8 | (r & 0xFF); + } + + public static NativeImageBackedTexture createTexture(NativeImage image) { + return new NativeImageBackedTexture(image); + } + + public static void uploadTexture(NativeImageBackedTexture texture) { + texture.upload(); + } + + public static NativeImage getImage(NativeImageBackedTexture texture) { + return texture.getImage(); + } + + public static void registerBuiltinResourcePack(Identifier id, java.nio.file.Path path, String displayName) { + try { + Class helper = Class.forName("net.fabricmc.fabric.api.resource.ResourceManagerHelper"); + Class activationType = Class.forName("net.fabricmc.fabric.api.resource.ResourcePackActivationType"); + Object alwaysEnabled = null; + for (Object c : activationType.getEnumConstants()) { + if (c.toString().equals("ALWAYS_ENABLED")) { alwaysEnabled = c; break; } + } + try { + helper.getMethod("registerBuiltinResourcePack", Identifier.class, + java.nio.file.Path.class, + Class.forName("net.minecraft.text.Text"), activationType) + .invoke(null, id, path, + Class.forName("net.minecraft.text.Text").getMethod("literal", String.class) + .invoke(null, displayName), + alwaysEnabled); + return; + } catch (NoSuchMethodException ignored) {} + + try { + String ver = net.fabricmc.loader.api.FabricLoader.getInstance() + .getModContainer("justasplash").get() + .getMetadata().getVersion().getFriendlyString(); + helper.getMethod("registerBuiltinResourcePack", Identifier.class, + String.class, + Class.forName("net.fabricmc.loader.api.ModContainer"), + boolean.class) + .invoke(null, id, displayName, + net.fabricmc.loader.api.FabricLoader.getInstance() + .getModContainer("justasplash").get(), + true); + return; + } catch (NoSuchMethodException ignored) {} + + helper.getMethod("registerBuiltinResourcePack", Identifier.class, + Class.forName("net.fabricmc.loader.api.ModContainer"), + Class.forName("net.minecraft.text.Text"), activationType) + .invoke(null, id, + net.fabricmc.loader.api.FabricLoader.getInstance() + .getModContainer("justasplash").get(), + Class.forName("net.minecraft.text.Text").getMethod("literal", String.class) + .invoke(null, displayName), + alwaysEnabled); + } catch (Exception e) { + throw new RuntimeException("Cannot register resource pack", e); + } + } +} diff --git a/src/main/resources/assets/justasplash/default/splash.png b/src/main/resources/assets/justasplash/default/splash.png new file mode 100644 index 0000000..25c1a21 Binary files /dev/null and b/src/main/resources/assets/justasplash/default/splash.png differ diff --git a/src/main/resources/assets/justasplash/icon.png b/src/main/resources/assets/justasplash/icon.png new file mode 100644 index 0000000..f5eb712 Binary files /dev/null and b/src/main/resources/assets/justasplash/icon.png differ diff --git a/src/main/resources/assets/justasplash/lang/en_us.json b/src/main/resources/assets/justasplash/lang/en_us.json new file mode 100644 index 0000000..a2e5e46 --- /dev/null +++ b/src/main/resources/assets/justasplash/lang/en_us.json @@ -0,0 +1,4 @@ +{ + "key.justasplash.splash": "Show Splash", + "category.justasplash": "JustASplash" +} diff --git a/src/main/resources/assets/justasplash/lang/ru_ru.json b/src/main/resources/assets/justasplash/lang/ru_ru.json new file mode 100644 index 0000000..f2e873f --- /dev/null +++ b/src/main/resources/assets/justasplash/lang/ru_ru.json @@ -0,0 +1,4 @@ +{ + "key.justasplash.splash": "Показать сплеш", + "category.justasplash": "JustASplash" +} diff --git a/src/main/resources/assets/justasplash/sounds.json b/src/main/resources/assets/justasplash/sounds.json new file mode 100644 index 0000000..d9eec9e --- /dev/null +++ b/src/main/resources/assets/justasplash/sounds.json @@ -0,0 +1,10 @@ +{ + "splash": { + "sounds": [ + { + "name": "justasplash:splash", + "stream": true + } + ] + } +} diff --git a/src/main/resources/assets/justasplash/sounds/splash.ogg b/src/main/resources/assets/justasplash/sounds/splash.ogg new file mode 100644 index 0000000..8d649da Binary files /dev/null and b/src/main/resources/assets/justasplash/sounds/splash.ogg differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..716b201 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,23 @@ +{ + "schemaVersion": 1, + "id": "justasplash", + "version": "${version}", + "name": "JustASplash", + "description": "Fullscreen splash on key press with fade-out", + "authors": ["sasheg"], + "contact": {}, + "license": "MIT", + "icon": "assets/justasplash/icon.png", + "environment": "client", + "entrypoints": { + "client": ["net.anomaly.justasplash.JustASplash"] + }, + "mixins": ["justasplash.mixins.json"], + "accessWidener": "justasplash.accesswidener", + "depends": { + "fabricloader": ">=0.15.0", + "fabric-api": ">=0.92.0", + "minecraft": ">=1.20.1 <=1.21.1", + "java": ">=17" + } +} diff --git a/src/main/resources/justasplash.accesswidener b/src/main/resources/justasplash.accesswidener new file mode 100644 index 0000000..f649fd8 --- /dev/null +++ b/src/main/resources/justasplash.accesswidener @@ -0,0 +1,3 @@ +accessWidener v2 named + +# No access widener entries needed currently diff --git a/src/main/resources/justasplash.mixins.json b/src/main/resources/justasplash.mixins.json new file mode 100644 index 0000000..52f3c87 --- /dev/null +++ b/src/main/resources/justasplash.mixins.json @@ -0,0 +1,10 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.anomaly.justasplash.mixin", + "compatibilityLevel": "JAVA_17", + "client": ["WindowMixin"], + "injectors": { + "defaultRequire": 1 + } +}