commit 6021a7c60be30ed2ec1cdb35725fd5fce4d5f0cd Author: SashegDev Date: Mon Aug 24 02:54:49 2026 +0300 initial: JustASplash 1.21.1 NeoForge me.sashegdev - hotkey Z fullscreen png/gif ogg/mp3 fade 3s diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4788a3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +build/ +.gradle/ +run/ +logs/ +out/ +*.log +.idea/ +*.iml diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6339e3 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Just A Splash - NeoForge 1.21.1 + +Client-only: hotkey Z -> fullscreen png/gif + ogg/mp3 splash, 100% -> 0% fade (default 3s, -1 = audio duration), non-blocking overlay over HUD, fully configurable. + +- `me.sashegdev.justasplash` `1.21.1 NeoForge 21.1.133` `Java 21` `CLIENT` only +- `png,gif` (gif delay from file) + `ogg,mp3` (JLayer + MP3SPI) +- Resource packs: `assets/justasplash/textures/gui/splashes/` + `assets/justasplash/sounds/` +- Config `justasplash-client.toml`: `splashImage`, `splashSound`, `fade`, `volume` +- Hotkey Z rebindable in Controls + +## Build +```bash +./gradlew build +``` +Jar in `build/libs/justasplash-1.0.0.jar` -> put in `mods/` (client only). + +## License +MIT diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..226545e --- /dev/null +++ b/build.gradle @@ -0,0 +1,55 @@ +plugins { + id 'eclipse' + id 'idea' + id 'maven-publish' + id 'net.neoforged.gradle.userdev' version '7.0.163' +} + +version = findProperty('mod_version') ?: '1.0.0' +group = findProperty('group') ?: 'me.sashegdev.justasplash' +archivesBaseName = findProperty('archives_base_name') ?: 'justasplash' + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) +println "Java: ${java.toolchain.languageVersion.get()} - NeoForge ${findProperty('neo_version')}" + +minecraft { + mappings channel: findProperty('mappings_channel') ?: 'official', version: findProperty('mappings_version') ?: '1.21.1' + copyIdeResources = true +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "net.neoforged:neoforge:${findProperty('neo_version')}" + implementation "javazoom:jlayer:1.0.1" + implementation "com.googlecode.soundlibs:mp3spi:1.9.5.4" + implementation "com.googlecode.soundlibs:tritonus-share:0.3.7.4" + implementation "com.googlecode.soundlibs:jorbis:0.0.17.4" +} + +tasks.named('processResources', DuplicatesStrategy.INCLUDE).configure { + var replaceProperties = [ + minecraft_version: findProperty('minecraft_version'), neo_version: findProperty('neo_version'), + loader_version: findProperty('loader_version'), mod_version: findProperty('mod_version') + ] + inputs.properties(replaceProperties) + filesMatching(['META-INF/neoforge.mods.toml', 'pack.mcmeta']) { + expand(replaceProperties) + } +} + +jar { + manifest { + attributes([ + 'Specification-Title' : 'justasplash', + 'Specification-Vendor' : 'SashegDev', + 'Specification-Version' : '1', + 'Implementation-Title' : project.name, + 'Implementation-Version' : project.jar.archiveVersion, + 'Implementation-Vendor' : 'SashegDev', + 'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ") + ]) + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e356c45 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,10 @@ +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false +minecraft_version=1.21.1 +neo_version=21.1.133 +loader_version=4.0.24 +mappings_channel=official +mappings_version=1.21.1 +mod_version=1.0.0 +group=me.sashegdev.justasplash +archives_base_name=justasplash diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..98555b6 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = "https://maven.neoforged.net/releases" } + } +} +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} diff --git a/src/main/java/me/sashegdev/justasplash/JustASplash.java b/src/main/java/me/sashegdev/justasplash/JustASplash.java new file mode 100644 index 0000000..2794d58 --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/JustASplash.java @@ -0,0 +1,23 @@ +package me.sashegdev.justasplash; + +import me.sashegdev.justasplash.client.Hotkey; +import me.sashegdev.justasplash.client.SplashManager; +import me.sashegdev.justasplash.client.SplashOverlay; +import me.sashegdev.justasplash.config.JustASplashConfig; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.config.ModConfig; +import net.neoforged.neoforge.client.gui.IConfigScreenFactory; + +@Mod(value = "justasplash", dist = Dist.CLIENT) +public class JustASplash { + public JustASplash(IEventBus modBus, ModContainer container) { + container.registerConfig(ModConfig.Type.CLIENT, JustASplashConfig.SPEC); + container.registerExtensionPoint(IConfigScreenFactory.class, (c, s) -> JustASplashConfig.createScreen(s)); + Hotkey.register(modBus); + SplashOverlay.register(); + SplashManager.init(); + } +} diff --git a/src/main/java/me/sashegdev/justasplash/client/AudioPlayer.java b/src/main/java/me/sashegdev/justasplash/client/AudioPlayer.java new file mode 100644 index 0000000..f389475 --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/AudioPlayer.java @@ -0,0 +1,60 @@ +package me.sashegdev.justasplash.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.client.sounds.SoundManager; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.AudioInputStream; +import java.io.InputStream; + +@OnlyIn(Dist.CLIENT) +public class AudioPlayer { + private static SoundInstance current; + + public static double play(ResourceLocation loc, float vol) { + try { + var mc = Minecraft.getInstance(); + var rm = mc.getResourceManager(); + var res = rm.getResource(loc).orElse(null); + double dur = 3.0; + if (res != null) { + try (InputStream in = res.open()) { + dur = probeDuration(in, loc.getPath()); + } catch (Exception ignored) {} + } + SoundEvent ev = SoundEvent.createVariableRangeEvent(loc); + current = new SimpleSoundInstance(ev.getLocation(), SoundSource.MASTER, vol, 1.0f, SoundInstance.createUnseededRandom(), false, 0, SoundInstance.Attenuation.NONE, 0, 0, 0, true); + mc.getSoundManager().play(current); + return dur; + } catch (Exception e) { + e.printStackTrace(); + return 3.0; + } + } + + private static double probeDuration(InputStream in, String path) { + try { + in.mark(Integer.MAX_VALUE); + AudioInputStream ais = AudioSystem.getAudioInputStream(in); + long frames = ais.getFrameLength(); + float rate = ais.getFormat().getFrameRate(); + double sec = frames / rate; + if (sec > 0 && sec < 300) return sec; + } catch (Exception ignored) {} + return path.endsWith(".mp3") ? 5.0 : 3.0; + } + + public static void stop() { + if (current != null) { + Minecraft.getInstance().getSoundManager().stop(current); + current = null; + } + } +} diff --git a/src/main/java/me/sashegdev/justasplash/client/Hotkey.java b/src/main/java/me/sashegdev/justasplash/client/Hotkey.java new file mode 100644 index 0000000..9b64c67 --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/Hotkey.java @@ -0,0 +1,34 @@ +package me.sashegdev.justasplash.client; + +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.KeyMapping; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.ClientTickEvent; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import org.lwjgl.glfw.GLFW; + +@Mod.EventBusSubscriber(value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD) +public class Hotkey { + public static KeyMapping SPLASH = new KeyMapping("key.justasplash.splash", GLFW.GLFW_KEY_Z, "key.categories.justasplash"); + + public static void register(IEventBus bus) { + bus.addListener(Hotkey::onRegister); + } + + private static void onRegister(RegisterKeyMappingsEvent e) { + e.register(SPLASH); + } + + @Mod.EventBusSubscriber(Dist.CLIENT) + public static class Tick { + @SubscribeEvent + public static void onTick(ClientTickEvent.Post e) { + while (SPLASH.consumeClick()) { + SplashManager.trigger(); + } + } + } +} diff --git a/src/main/java/me/sashegdev/justasplash/client/ImageLoader.java b/src/main/java/me/sashegdev/justasplash/client/ImageLoader.java new file mode 100644 index 0000000..9b6502a --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/ImageLoader.java @@ -0,0 +1,108 @@ +package me.sashegdev.justasplash.client; + +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +@OnlyIn(Dist.CLIENT) +public class ImageLoader { + private static ResourceLocation texLoc; + private static int w, h; + private static List frames = List.of(); + private static int frameIdx = 0; + private static long lastFrameMs = 0; + + record Frame(DynamicTexture tex, int delayMs) {} + + public static void load(ResourceLocation loc) { + try { + var mc = Minecraft.getInstance(); + var rm = mc.getResourceManager(); + var res = rm.getResource(loc).orElse(null); + if (res == null) return; + try (InputStream in = res.open()) { + if (loc.getPath().endsWith(".gif")) loadGif(in); + else loadPng(in); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void loadPng(InputStream in) throws Exception { + NativeImage img = NativeImage.read(in); + w = img.getWidth(); + h = img.getHeight(); + DynamicTexture tex = new DynamicTexture(img); + texLoc = new ResourceLocation("justasplash", "splash_" + System.nanoTime()); + Minecraft.getInstance().getTextureManager().register(texLoc, tex); + frames = List.of(new Frame(tex, 0)); + } + + private static void loadGif(InputStream in) throws Exception { + ImageReader r = ImageIO.getImageReadersByFormatName("gif").next(); + r.setInput(ImageIO.createImageInputStream(in)); + int n = r.getNumImages(true); + List list = new ArrayList<>(); + for (int i = 0; i < n; i++) { + var bi = r.read(i); + int delay = 100; + try { + var meta = r.getImageMetadata(i); + var tree = meta.getAsTree("javax_imageio_gif_image_1.0"); + var gce = tree.getChildNodes(); + for (int j = 0; j < gce.getLength(); j++) { + var node = gce.item(j); + if ("GraphicControlExtension".equals(node.getNodeName())) { + delay = Integer.parseInt(node.getAttributes().getNamedItem("delayTime").getNodeValue()) * 10; + } + } + } catch (Exception ignored) {} + NativeImage img = toNativeImage(bi); + w = img.getWidth(); + h = img.getHeight(); + DynamicTexture tex = new DynamicTexture(img); + ResourceLocation loc = new ResourceLocation("justasplash", "splash_gif_" + i + "_" + System.nanoTime()); + Minecraft.getInstance().getTextureManager().register(loc, tex); + list.add(new Frame(tex, delay == 0 ? 100 : delay)); + } + if (!list.isEmpty()) { + frames = list; + texLoc = frames.get(0).tex.getTextureLocation(); + } + } + + private static NativeImage toNativeImage(java.awt.image.BufferedImage bi) { + int wi = bi.getWidth(), hi = bi.getHeight(); + NativeImage img = new NativeImage(wi, hi, false); + for (int y = 0; y < hi; y++) for (int x = 0; x < wi; x++) { + int argb = bi.getRGB(x, y); + img.setPixelRGBA(x, y, argb); + } + return img; + } + + public static ResourceLocation getTexture() { + if (frames.size() <= 1) return texLoc; + long now = System.currentTimeMillis(); + int delay = frames.get(frameIdx).delayMs; + if (now - lastFrameMs > delay) { + frameIdx = (frameIdx + 1) % frames.size(); + lastFrameMs = now; + texLoc = frames.get(frameIdx).tex.getTextureLocation(); + } + return texLoc; + } + + public static int getWidth() { return w; } + public static int getHeight() { return h; } +} diff --git a/src/main/java/me/sashegdev/justasplash/client/SplashManager.java b/src/main/java/me/sashegdev/justasplash/client/SplashManager.java new file mode 100644 index 0000000..35283ca --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/SplashManager.java @@ -0,0 +1,58 @@ +package me.sashegdev.justasplash.client; + +import me.sashegdev.justasplash.config.JustASplashConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; + +@OnlyIn(Dist.CLIENT) +public class SplashManager { + private static long startMs = 0; + private static boolean showing = false; + private static double fadeSec = 3.0; + private static double audioSec = 0; + + public static void init() { + } + + public static void trigger() { + var mc = Minecraft.getInstance(); + if (mc.level == null) return; + String img = JustASplashConfig.SPLASH_IMAGE.get(); + String snd = JustASplashConfig.SPLASH_SOUND.get(); + fadeSec = JustASplashConfig.FADE.get(); + double vol = JustASplashConfig.VOLUME.get(); + ResourceLocation imgLoc = ResourceLocation.tryParse(img); + ResourceLocation sndLoc = snd == null || snd.isEmpty() ? null : ResourceLocation.tryParse(snd); + if (imgLoc != null) ImageLoader.load(imgLoc); + if (sndLoc != null) { + audioSec = AudioPlayer.play(sndLoc, (float) vol); + } else audioSec = 0; + if (fadeSec < 0) fadeSec = audioSec > 0 ? audioSec : 3.0; + startMs = System.currentTimeMillis(); + showing = true; + } + + public static boolean isShowing() { + if (!showing) return false; + double elapsed = (System.currentTimeMillis() - startMs) / 1000.0; + if (elapsed >= fadeSec && elapsed >= audioSec) { + showing = false; + if (audioSec > 0 && elapsed >= audioSec) AudioPlayer.stop(); + return false; + } + return true; + } + + public static float getFadeAlpha() { + double elapsed = (System.currentTimeMillis() - startMs) / 1000.0; + if (elapsed >= fadeSec) return 0f; + return (float) (1.0 - elapsed / fadeSec); + } + + public static boolean shouldKeepAudio() { + double elapsed = (System.currentTimeMillis() - startMs) / 1000.0; + return elapsed < audioSec; + } +} diff --git a/src/main/java/me/sashegdev/justasplash/client/SplashOverlay.java b/src/main/java/me/sashegdev/justasplash/client/SplashOverlay.java new file mode 100644 index 0000000..8dad914 --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/SplashOverlay.java @@ -0,0 +1,48 @@ +package me.sashegdev.justasplash.client; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.LayeredDraw; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; + +public class SplashOverlay { + public static void register() { + } + + @Mod.EventBusSubscriber(value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD) + public static class Reg { + @SubscribeEvent + public static void onLayers(RegisterGuiLayersEvent e) { + e.registerAboveAll(new net.minecraft.resources.ResourceLocation("justasplash", "splash"), SplashOverlay::render); + } + } + + private static void render(GuiGraphics g, DeltaTracker d) { + if (!SplashManager.isShowing()) return; + var mc = Minecraft.getInstance(); + int w = mc.getWindow().getGuiScaledWidth(); + int h = mc.getWindow().getGuiScaledHeight(); + float a = SplashManager.getFadeAlpha(); + if (a <= 0.01f) return; + RenderSystem.enableBlend(); + RenderSystem.setShaderColor(1, 1, 1, a); + var tex = ImageLoader.getTexture(); + if (tex != null) { + int iw = ImageLoader.getWidth(); + int ih = ImageLoader.getHeight(); + float scale = Math.min((float) w / iw, (float) h / ih); + int dw = (int) (iw * scale); + int dh = (int) (ih * scale); + int x = (w - dw) / 2; + int y = (h - dh) / 2; + g.blit(tex, x, y, 0, 0, dw, dh, dw, dh); + } + RenderSystem.setShaderColor(1, 1, 1, 1); + RenderSystem.disableBlend(); + } +} diff --git a/src/main/java/me/sashegdev/justasplash/config/JustASplashConfig.java b/src/main/java/me/sashegdev/justasplash/config/JustASplashConfig.java new file mode 100644 index 0000000..d89512f --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/config/JustASplashConfig.java @@ -0,0 +1,32 @@ +package me.sashegdev.justasplash.config; + +import net.minecraft.client.gui.screens.Screen; +import net.neoforged.common.ModConfigSpec; +import net.neoforged.neoforge.common.ModConfigSpec.*; + +public class JustASplashConfig { + public static final ModConfigSpec SPEC; + public static final ConfigValue SPLASH_IMAGE; + public static final ConfigValue SPLASH_SOUND; + public static final DoubleValue FADE; + public static final DoubleValue VOLUME; + + static { + Builder b = new Builder(); + b.push("splash"); + SPLASH_IMAGE = b.comment("ResourceLocation png|gif in assets/justasplash/ or config/justasplash/ - e.g. justasplash:textures/gui/splashes/splash.png") + .define("splashImage", "justasplash:textures/gui/splashes/splash.png"); + SPLASH_SOUND = b.comment("ResourceLocation ogg|mp3 - e.g. justasplash:sounds/splash.ogg, empty to disable") + .define("splashSound", "justasplash:sounds/splash.ogg"); + FADE = b.comment("Fade duration seconds 100%->0%, -1 = use audio duration, default 3.0") + .defineInRange("fade", 3.0, -1.0, 60.0); + VOLUME = b.comment("Sound volume 0.0-1.0") + .defineInRange("volume", 1.0, 0.0, 1.0); + b.pop(); + SPEC = b.build(); + } + + public static Screen createScreen(Screen parent) { + return new net.neoforged.neoforge.client.gui.ConfigurationScreen(parent, SPEC, "justasplash"); + } +} diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..dfed8d3 --- /dev/null +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,25 @@ +modLoader="javafml" +loaderVersion="[4,)" +license="MIT" +issueTrackerURL="https://git.swe.zern.cc/sasheg/justasplash-neo/issues" +[[mods]] +modId="justasplash" +version="${mod_version}" +displayName="Just A Splash" +updateJSONURL="https://git.swe.zern.cc/sasheg/justasplash-neo" +displayURL="https://git.swe.zern.cc/sasheg/justasplash-neo" +logoFile="justasplash.png" +authors="SashegDev" +description="Hotkey Z -> fullscreen png/gif + ogg/mp3 splash, 100% -> 0% fade, client only. Fully configurable." +[[dependencies.justasplash]] + modId="neoforge" + type="required" + versionRange="[21.1.133,21.2)" + ordering="NONE" + side="CLIENT" +[[dependencies.justasplash]] + modId="minecraft" + type="required" + versionRange="[1.21.1,1.22)" + ordering="NONE" + side="CLIENT" diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..e5298bb --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "Just A Splash resources", + "pack_format": 34, + "supported_formats": {"min_inclusive": 34, "max_inclusive": 34} + } +}