diff --git a/build.gradle b/build.gradle index 741e9b7..638a5ec 100644 --- a/build.gradle +++ b/build.gradle @@ -34,10 +34,8 @@ configurations { dependencies { implementation "net.neoforged:neoforge:${neo_version}" - implementation "javazoom:jlayer:1.0.1" - localRuntime "com.googlecode.soundlibs:mp3spi:1.9.5.4" - localRuntime "com.googlecode.soundlibs:tritonus-share:0.3.7.4" - localRuntime "com.googlecode.soundlibs:jorbis:0.0.17.4" + localRuntime "com.googlecode.soundlibs:vorbisspi:1.0.3.3" + jarJar "com.googlecode.soundlibs:vorbisspi:1.0.3.3" } tasks.named('processResources', Copy).configure { diff --git a/src/main/java/me/sashegdev/justasplash/JustASplash.java b/src/main/java/me/sashegdev/justasplash/JustASplash.java index 84f081c..be9e966 100644 --- a/src/main/java/me/sashegdev/justasplash/JustASplash.java +++ b/src/main/java/me/sashegdev/justasplash/JustASplash.java @@ -1,15 +1,55 @@ package me.sashegdev.justasplash; +import me.sashegdev.justasplash.client.ConfigScreen; 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, p) -> new ConfigScreen(p)); + ensureAssets(); + } + + private static void ensureAssets() { + try { + java.nio.file.Path dir = java.nio.file.Paths.get("config/justasplash/assets"); + java.nio.file.Files.createDirectories(dir); + String txt = "JustASplash assets:\n" + + "1) Кидай png/jpg/gif сюда и выбирай в конфиге 'следующий' (config/justasplash/assets/)\n" + + "2) Или укажи ResourceLocation вроде justasplash:textures/gui/splashes/splash.jpg (ресурспак)\n" + + "EN: Drop png/jpg/gif here and pick 'Next' in config, or use ResourceLocation\n" + + "Sounds: ogg/wav сюда. Пример: overlay.png, sound.ogg\n"; + java.nio.file.Files.writeString(dir.resolve("README.txt"), txt); + copyDefault("assets/justasplash/textures/gui/splashes/splash.jpg", dir.resolve("splash.jpg")); + copyDefault("assets/justasplash/sounds/splash.ogg", dir.resolve("splash.ogg")); + try { + String snd = JustASplashConfig.SPLASH_SOUND.get(); + if (snd != null && snd.toLowerCase().endsWith(".mp3")) { + String ogg = snd.substring(0, snd.length() - 4) + ".ogg"; + java.nio.file.Path oggFile = dir.resolve(ogg.substring(ogg.lastIndexOf('/') + 1)); + java.nio.file.Path wavFile = dir.resolve(ogg.substring(ogg.lastIndexOf('/') + 1).replace(".ogg", ".wav")); + if (java.nio.file.Files.exists(oggFile) || java.nio.file.Files.exists(wavFile)) { + JustASplashConfig.SPLASH_SOUND.set(ogg); + JustASplashConfig.SPEC.save(); + } + } + } catch (Exception ignored) {} + } catch (Exception ignored) {} + } + + private static void copyDefault(String res, java.nio.file.Path dst) { + try { + if (java.nio.file.Files.exists(dst)) return; + try (var in = JustASplash.class.getClassLoader().getResourceAsStream(res)) { + if (in != null) java.nio.file.Files.copy(in, dst); + } + } catch (Exception ignored) {} } } diff --git a/src/main/java/me/sashegdev/justasplash/JustASplashCommands.java b/src/main/java/me/sashegdev/justasplash/JustASplashCommands.java new file mode 100644 index 0000000..b65a464 --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/JustASplashCommands.java @@ -0,0 +1,30 @@ +package me.sashegdev.justasplash; + +import com.mojang.brigadier.CommandDispatcher; +import me.sashegdev.justasplash.client.ImageLoader; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.RegisterCommandsEvent; + +@EventBusSubscriber(modid = "justasplash", value = Dist.CLIENT) +public class JustASplashCommands { + @SubscribeEvent + public static void onRegister(RegisterCommandsEvent e) { + CommandDispatcher d = e.getDispatcher(); + d.register(Commands.literal("justasplash") + .then(Commands.literal("reload").executes(ctx -> { + ImageLoader.clearCache(); + ctx.getSource().sendSuccess(() -> Component.translatable("command.justasplash.reload.success"), false); + return 1; + })) + .then(Commands.literal("test").executes(ctx -> { + net.minecraft.client.Minecraft.getInstance().execute(() -> me.sashegdev.justasplash.client.SplashManager.trigger()); + ctx.getSource().sendSuccess(() -> Component.translatable("toast.justasplash.ok"), false); + return 1; + }))); + } +} diff --git a/src/main/java/me/sashegdev/justasplash/client/AudioPlayer.java b/src/main/java/me/sashegdev/justasplash/client/AudioPlayer.java index f389475..bfece23 100644 --- a/src/main/java/me/sashegdev/justasplash/client/AudioPlayer.java +++ b/src/main/java/me/sashegdev/justasplash/client/AudioPlayer.java @@ -3,7 +3,6 @@ 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; @@ -18,18 +17,35 @@ import java.io.InputStream; public class AudioPlayer { private static SoundInstance current; - public static double play(ResourceLocation loc, float vol) { + public static double play(ResourceLocation fileLoc, float vol) { try { var mc = Minecraft.getInstance(); var rm = mc.getResourceManager(); - var res = rm.getResource(loc).orElse(null); + var res = rm.getResource(fileLoc).orElse(null); double dur = 3.0; if (res != null) { try (InputStream in = res.open()) { - dur = probeDuration(in, loc.getPath()); + byte[] data = in.readAllBytes(); + try (InputStream in2 = new java.io.ByteArrayInputStream(data)) { dur = probeDuration(in2, fileLoc.getPath()); } catch (Exception ignored) {} } catch (Exception ignored) {} + ResourceLocation evLoc = toEventId(fileLoc); + SoundEvent ev = SoundEvent.createVariableRangeEvent(evLoc); + 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; + } else { + java.nio.file.Path fp = ImageLoader.resolveConfigFile(fileLoc); + if (fp != null && java.nio.file.Files.exists(fp)) { + try (InputStream in = java.nio.file.Files.newInputStream(fp)) { + byte[] data = in.readAllBytes(); + try (InputStream in2 = new java.io.ByteArrayInputStream(data)) { dur = probeDuration(in2, fp.toString()); } catch (Exception ignored) {} + } catch (Exception ignored) {} + playFileClip(fp, vol); + return dur; + } } - SoundEvent ev = SoundEvent.createVariableRangeEvent(loc); + ResourceLocation evLoc = toEventId(fileLoc); + SoundEvent ev = SoundEvent.createVariableRangeEvent(evLoc); 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; @@ -39,6 +55,47 @@ public class AudioPlayer { } } + private static void playFileClip(java.nio.file.Path fp, float vol) { + new Thread(() -> { + try { + javax.sound.sampled.AudioInputStream ais = AudioSystem.getAudioInputStream(fp.toFile()); + javax.sound.sampled.AudioFormat base = ais.getFormat(); + javax.sound.sampled.AudioFormat decoded = new javax.sound.sampled.AudioFormat(javax.sound.sampled.AudioFormat.Encoding.PCM_SIGNED, base.getSampleRate(), 16, base.getChannels(), base.getChannels() * 2, base.getSampleRate(), false); + javax.sound.sampled.AudioInputStream dais = AudioSystem.getAudioInputStream(decoded, ais); + javax.sound.sampled.Clip clip = AudioSystem.getClip(); + clip.open(dais); + if (clip.isControlSupported(javax.sound.sampled.FloatControl.Type.MASTER_GAIN)) { + float gain = 20f * (float) Math.log10(Math.max(0.0001, vol)); + gain = Math.max(-80f, Math.min(6f, gain)); + ((javax.sound.sampled.FloatControl) clip.getControl(javax.sound.sampled.FloatControl.Type.MASTER_GAIN)).setValue(gain); + } + clip.start(); + Thread.sleep(clip.getMicrosecondLength() / 1000 + 100); + clip.close(); + dais.close(); + ais.close(); + } catch (Exception e) { + try { + javax.sound.sampled.Clip clip2 = AudioSystem.getClip(); + javax.sound.sampled.AudioInputStream ais2 = AudioSystem.getAudioInputStream(fp.toFile()); + clip2.open(ais2); + clip2.start(); + } catch (Exception ex) { ex.printStackTrace(); } + } + }, "justasplash-audio").start(); + } + + private static ResourceLocation toEventId(ResourceLocation fileLoc) { + String p = fileLoc.getPath(); + if (p.contains("/")) { + String name = p.substring(p.lastIndexOf('/') + 1); + int dot = name.lastIndexOf('.'); + if (dot > 0) name = name.substring(0, dot); + return ResourceLocation.fromNamespaceAndPath(fileLoc.getNamespace(), name); + } + return fileLoc; + } + private static double probeDuration(InputStream in, String path) { try { in.mark(Integer.MAX_VALUE); @@ -48,7 +105,7 @@ public class AudioPlayer { double sec = frames / rate; if (sec > 0 && sec < 300) return sec; } catch (Exception ignored) {} - return path.endsWith(".mp3") ? 5.0 : 3.0; + return 3.0; } public static void stop() { diff --git a/src/main/java/me/sashegdev/justasplash/client/ConfigPackHandler.java b/src/main/java/me/sashegdev/justasplash/client/ConfigPackHandler.java new file mode 100644 index 0000000..68536e1 --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/ConfigPackHandler.java @@ -0,0 +1,33 @@ +package me.sashegdev.justasplash.client; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.PackSelectionConfig; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.repository.Pack; +import net.minecraft.server.packs.repository.PackCompatibility; +import net.minecraft.server.packs.repository.PackSource; +import net.minecraft.world.flag.FeatureFlagSet; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.AddPackFindersEvent; + +import java.util.Optional; + +@EventBusSubscriber(value = Dist.CLIENT, modid = "justasplash", bus = EventBusSubscriber.Bus.MOD) +public class ConfigPackHandler { + @SubscribeEvent + public static void onAddPackFinders(AddPackFindersEvent e) { + if (e.getPackType() != PackType.CLIENT_RESOURCES) return; + PackLocationInfo info = new PackLocationInfo("justasplash_config", Component.literal("JustASplash Config Assets"), PackSource.BUILT_IN, Optional.empty()); + PackSelectionConfig sel = new PackSelectionConfig(true, Pack.Position.TOP, false); + Pack.Metadata meta = new Pack.Metadata(Component.literal("JustASplash Config Assets"), PackCompatibility.COMPATIBLE, FeatureFlagSet.of(), java.util.List.of(), false); + Pack pack = new Pack(info, new Pack.ResourcesSupplier() { + @Override public PackResources openPrimary(PackLocationInfo loc) { return new ConfigPackResources(loc); } + @Override public PackResources openFull(PackLocationInfo loc, Pack.Metadata m) { return new ConfigPackResources(loc); } + }, meta, sel); + e.addRepositorySource(c -> c.accept(pack)); + } +} diff --git a/src/main/java/me/sashegdev/justasplash/client/ConfigPackResources.java b/src/main/java/me/sashegdev/justasplash/client/ConfigPackResources.java new file mode 100644 index 0000000..562a983 --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/ConfigPackResources.java @@ -0,0 +1,150 @@ +package me.sashegdev.justasplash.client; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackLocationInfo; +import net.minecraft.server.packs.PackResources; +import net.minecraft.server.packs.PackType; +import net.minecraft.server.packs.metadata.MetadataSectionSerializer; +import net.minecraft.server.packs.resources.IoSupplier; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Set; + +public class ConfigPackResources implements PackResources { + private final PackLocationInfo loc; + + public ConfigPackResources(PackLocationInfo loc) { + this.loc = loc; + } + + private Path configDir() { + return Paths.get("config/justasplash/assets"); + } + + @Override + public IoSupplier getRootResource(String... path) { + String joined = String.join("/", path); + if (joined.equals("pack.mcmeta")) { + String json = "{\"pack\":{\"pack_format\":34,\"description\":\"JustASplash Config Assets\"}}"; + return () -> new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)); + } + if (joined.equals("pack.png")) return null; + return null; + } + + @Override + public IoSupplier getResource(PackType type, ResourceLocation location) { + if (type != PackType.CLIENT_RESOURCES) return null; + if (!location.getNamespace().equals("justasplash")) return null; + String p = location.getPath(); + if (p.equals("sounds.json")) { + return this::openSoundsJson; + } + Path file = null; + if (p.startsWith("textures/gui/splashes/")) { + String name = p.substring("textures/gui/splashes/".length()); + file = configDir().resolve(name); + } else if (p.startsWith("sounds/")) { + String name = p.substring("sounds/".length()); + file = configDir().resolve(name); + } else if (p.startsWith("textures/")) { + String name = p.substring(p.lastIndexOf('/') + 1); + file = configDir().resolve(name); + } + if (file != null && Files.exists(file)) { + Path f2 = file; + return () -> Files.newInputStream(f2); + } + return null; + } + + private InputStream openSoundsJson() throws IOException { + StringBuilder sb = new StringBuilder("{\"splash\":{\"sounds\":[{\"name\":\"justasplash:splash\",\"stream\":true}]},"); + try { + Path dir = configDir(); + if (Files.exists(dir)) { + var files = Files.list(dir).filter(pp -> { + String n = pp.getFileName().toString().toLowerCase(); + return n.endsWith(".ogg") || n.endsWith(".wav"); + }).toList(); + for (Path f : files) { + String name = f.getFileName().toString(); + int dot = name.lastIndexOf('.'); + String base = dot > 0 ? name.substring(0, dot) : name; + if (base.equals("splash")) continue; + sb.append("\"").append(base).append("\":{\"sounds\":[{\"name\":\"justasplash:").append(base).append("\",\"stream\":true}]},"); + } + } + } catch (Exception ignored) {} + if (sb.charAt(sb.length() - 1) == ',') sb.setLength(sb.length() - 1); + sb.append("}"); + return new ByteArrayInputStream(sb.toString().getBytes(StandardCharsets.UTF_8)); + } + + @Override + public void listResources(PackType type, String namespace, String path, ResourceOutput out) { + if (type != PackType.CLIENT_RESOURCES || !namespace.equals("justasplash")) return; + Path dir = configDir(); + if (!Files.exists(dir)) return; + try { + if (path.equals("textures/gui/splashes")) { + Files.list(dir).forEach(p -> { + String n = p.getFileName().toString().toLowerCase(); + if (n.endsWith(".png") || n.endsWith(".jpg") || n.endsWith(".jpeg") || n.endsWith(".gif")) { + ResourceLocation loc = ResourceLocation.fromNamespaceAndPath("justasplash", "textures/gui/splashes/" + p.getFileName().toString()); + out.accept(loc, () -> Files.newInputStream(p)); + } + }); + } else if (path.equals("sounds")) { + Files.list(dir).forEach(p -> { + String n = p.getFileName().toString().toLowerCase(); + if (n.endsWith(".ogg") || n.endsWith(".wav")) { + ResourceLocation loc = ResourceLocation.fromNamespaceAndPath("justasplash", "sounds/" + p.getFileName().toString()); + out.accept(loc, () -> Files.newInputStream(p)); + } + }); + } else if (path.equals("")) { + out.accept(ResourceLocation.fromNamespaceAndPath("justasplash", "sounds.json"), this::openSoundsJson); + out.accept(ResourceLocation.fromNamespaceAndPath("justasplash", "pack.mcmeta"), () -> new ByteArrayInputStream("{\"pack\":{\"pack_format\":34,\"description\":\"JustASplash Config Assets\"}}".getBytes(StandardCharsets.UTF_8))); + } + } catch (Exception ignored) {} + } + + @Override + public Set getNamespaces(PackType type) { + if (type == PackType.CLIENT_RESOURCES) { + Set s = new HashSet<>(); + s.add("justasplash"); + return s; + } + return Set.of(); + } + + @Override + public T getMetadataSection(MetadataSectionSerializer serializer) throws IOException { + if (serializer != null && "pack".equals(serializer.getMetadataSectionName())) { + net.minecraft.server.packs.metadata.pack.PackMetadataSection sec = new net.minecraft.server.packs.metadata.pack.PackMetadataSection(net.minecraft.network.chat.Component.literal("JustASplash Config Assets"), 34, java.util.Optional.empty()); + return (T) sec; + } + try { + if (serializer == net.minecraft.server.packs.metadata.pack.PackMetadataSection.TYPE) { + net.minecraft.server.packs.metadata.pack.PackMetadataSection sec = new net.minecraft.server.packs.metadata.pack.PackMetadataSection(net.minecraft.network.chat.Component.literal("JustASplash Config Assets"), 34, java.util.Optional.empty()); + return (T) sec; + } + } catch (Exception ignored) {} + return null; + } + + @Override + public PackLocationInfo location() { return loc; } + + @Override + public void close() {} +} diff --git a/src/main/java/me/sashegdev/justasplash/client/ConfigScreen.java b/src/main/java/me/sashegdev/justasplash/client/ConfigScreen.java new file mode 100644 index 0000000..218512e --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/ConfigScreen.java @@ -0,0 +1,193 @@ +package me.sashegdev.justasplash.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractSliderButton; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import me.sashegdev.justasplash.config.JustASplashConfig; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +public class ConfigScreen extends Screen { + private final Screen parent; + private EditBox imageBox, soundBox, fadeBox, volBox; + private AbstractSliderButton fadeSlider, volSlider; + private Button imgNextBtn, sndNextBtn; + private double fade, volume; + private List imgFiles = List.of(); + private List sndFiles = List.of(); + + public ConfigScreen(Screen parent) { + super(Component.translatable("config.justasplash.title")); + this.parent = parent; + } + + @Override + protected void init() { + fade = JustASplashConfig.FADE.get(); + volume = JustASplashConfig.VOLUME.get(); + imgFiles = ImageLoader.listConfigImages(); + sndFiles = ImageLoader.listConfigSounds(); + int previewW = Math.min(280, width / 3); + int leftW = 300; + int gap = 20; + int leftX = width / 2 - leftW / 2 - (previewW + gap) / 2; + if (leftX < 10) leftX = 10; + int y = 35; + + imageBox = new EditBox(font, leftX, y, 200, 20, Component.translatable("config.justasplash.splashImage")); + imageBox.setMaxLength(512); + imageBox.setValue(JustASplashConfig.SPLASH_IMAGE.get()); + imageBox.setResponder(v -> { updatePreview(); }); + addRenderableWidget(imageBox); + imgNextBtn = Button.builder(Component.translatable("config.justasplash.nextImage"), b -> cycleImage()).bounds(leftX + 205, y, 95, 20).tooltip(Tooltip.create(Component.literal(pickLabel(imgFiles, imageBox.getValue(), "")))).build(); + addRenderableWidget(imgNextBtn); + y += 30; + soundBox = new EditBox(font, leftX, y, 200, 20, Component.translatable("config.justasplash.splashSound")); + soundBox.setMaxLength(512); + soundBox.setValue(JustASplashConfig.SPLASH_SOUND.get()); + addRenderableWidget(soundBox); + sndNextBtn = Button.builder(Component.translatable("config.justasplash.nextSound"), b -> cycleSound()).bounds(leftX + 205, y, 95, 20).tooltip(Tooltip.create(Component.literal(pickLabel(sndFiles, soundBox.getValue(), "")))).build(); + addRenderableWidget(sndNextBtn); + y += 30; + fadeBox = new EditBox(font, leftX, y, 145, 20, Component.translatable("config.justasplash.fade")); + fadeBox.setMaxLength(32); + fadeBox.setValue(String.valueOf(fade)); + fadeBox.setResponder(v -> { try { fade = Math.max(-1, Math.min(60, Double.parseDouble(v))); syncSlider(fadeSlider, (fade + 1) / 61.0); } catch (Exception ignored) {} }); + addRenderableWidget(fadeBox); + volBox = new EditBox(font, leftX + 155, y, 145, 20, Component.translatable("config.justasplash.volume")); + volBox.setMaxLength(32); + volBox.setValue(String.valueOf(volume)); + volBox.setResponder(v -> { try { volume = Math.max(0, Math.min(1, Double.parseDouble(v))); syncSlider(volSlider, volume); } catch (Exception ignored) {} }); + addRenderableWidget(volBox); + y += 25; + fadeSlider = new AbstractSliderButton(leftX, y, 145, 20, Component.literal("Fade"), (fade + 1) / 61.0) { + { updateMessage(); } + @Override protected void updateMessage() { setMessage(Component.literal("Fade: " + String.format("%.1f", fade) + "s")); } + @Override protected void applyValue() { fade = Math.round((value * 61.0 - 1) * 2) / 2.0; fadeBox.setValue(String.valueOf(fade)); updateMessage(); } + }; + volSlider = new AbstractSliderButton(leftX + 155, y, 145, 20, Component.literal("Vol"), volume) { + { updateMessage(); } + @Override protected void updateMessage() { setMessage(Component.literal("Vol: " + String.format("%.0f%%", volume * 100))); } + @Override protected void applyValue() { volume = Math.round(value * 100) / 100.0; volBox.setValue(String.valueOf(volume)); updateMessage(); } + }; + addRenderableWidget(fadeSlider); + addRenderableWidget(volSlider); + y += 30; + addRenderableWidget(Button.builder(Component.translatable("config.justasplash.test"), b -> { if (trySave(false)) SplashManager.trigger(); }).bounds(leftX, y, 145, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("config.justasplash.save"), b -> { if (trySave(true)) onClose(); }).bounds(leftX + 155, y, 145, 20).build()); + y += 25; + addRenderableWidget(Button.builder(Component.translatable("config.justasplash.cancel"), b -> onClose()).bounds(leftX + 75, y, 150, 20).build()); + } + + private void syncSlider(AbstractSliderButton s, double v) { + if (s == null) return; + try { + var f = AbstractSliderButton.class.getDeclaredField("value"); + f.setAccessible(true); + f.set(s, Math.max(0, Math.min(1, v))); + var m = AbstractSliderButton.class.getDeclaredMethod("updateMessage"); + m.setAccessible(true); + m.invoke(s); + } catch (Exception ignored) {} + } + + private String pickLabel(List files, String cur, String def) { + if (files.isEmpty()) return def; + for (String f : files) if (cur.contains(f)) return f; + return files.get(0); + } + + private void cycleImage() { + if (imgFiles.isEmpty()) return; + String cur = imageBox.getValue(); + int idx = -1; + for (int i = 0; i < imgFiles.size(); i++) if (cur.contains(imgFiles.get(i))) idx = i; + idx = (idx + 1) % imgFiles.size(); + String f = imgFiles.get(idx); + imageBox.setValue("justasplash:textures/gui/splashes/" + f); + if (imgNextBtn != null) imgNextBtn.setTooltip(Tooltip.create(Component.literal(f))); + updatePreview(); + } + + private void cycleSound() { + if (sndFiles.isEmpty()) return; + String cur = soundBox.getValue(); + int idx = -1; + for (int i = 0; i < sndFiles.size(); i++) if (cur.contains(sndFiles.get(i))) idx = i; + idx = (idx + 1) % sndFiles.size(); + String f = sndFiles.get(idx); + soundBox.setValue("justasplash:sounds/" + f); + if (sndNextBtn != null) sndNextBtn.setTooltip(Tooltip.create(Component.literal(f))); + } + + private void updatePreview() { + try { + String v = imageBox.getValue(); + ResourceLocation loc = ResourceLocation.tryParse(v); + if (loc != null) { ImageLoader.preload(loc); } + } catch (Exception ignored) {} + } + + private boolean trySave(boolean persist) { + try { fade = Double.parseDouble(fadeBox.getValue()); } catch (Exception e) {} + try { volume = Double.parseDouble(volBox.getValue()); } catch (Exception e) {} + fade = Math.max(-1, Math.min(60, fade)); + volume = Math.max(0, Math.min(1, volume)); + JustASplashConfig.SPLASH_IMAGE.set(imageBox.getValue()); + JustASplashConfig.SPLASH_SOUND.set(soundBox.getValue()); + JustASplashConfig.FADE.set(fade); + JustASplashConfig.VOLUME.set(volume); + if (persist) { + JustASplashConfig.SPEC.save(); + ImageLoader.clearCache(); + Minecraft.getInstance().execute(() -> { + try { Minecraft.getInstance().reloadResourcePacks(); } catch (Exception ignored) {} + }); + } + return true; + } + + @Override + public void render(GuiGraphics g, int mx, int my, float d) { + g.fill(0, 0, width, height, 0xCC0F0F1A); + super.render(g, mx, my, d); + int previewW = Math.min(280, width / 3); + int leftW = 300; + int gap = 20; + int leftX = width / 2 - leftW / 2 - (previewW + gap) / 2; + if (leftX < 10) leftX = 10; + int previewX = leftX + leftW + gap; + int previewY = 35; + int previewH = 180; + g.drawCenteredString(font, title, width / 2, 12, 0xFFE0E0FF); + g.drawString(font, Component.translatable("config.justasplash.splashImage"), leftX, 23, 0xFFAAAAFF); + g.drawString(font, Component.translatable("config.justasplash.splashSound"), leftX, 53, 0xFFAAAAFF); + g.drawString(font, Component.translatable("config.justasplash.fade"), leftX, 83, 0xFFAAAAFF); + g.drawString(font, Component.translatable("config.justasplash.volume"), leftX + 155, 83, 0xFFAAAAFF); + g.drawString(font, Component.translatable("config.justasplash.hint1"), leftX, 205, 0xFF8A8AC0, false); + g.drawString(font, Component.translatable("config.justasplash.hint2"), leftX, 215, 0xFF8A8AC0, false); + g.fill(previewX - 1, previewY - 1, previewX + previewW + 1, previewY + previewH + 1, 0xFF3D3D5A); + g.fill(previewX, previewY, previewX + previewW, previewY + previewH, 0xFF0F0F1A); + g.drawString(font, Component.translatable("config.justasplash.preview"), previewX, previewY - 10, 0xFFAAAAFF); + ResourceLocation tex = ImageLoader.getTexture(); + int iw = ImageLoader.getWidth(), ih = ImageLoader.getHeight(); + if (tex != null && iw > 0) { + float scale = Math.min((float) previewW / iw, (float) previewH / ih); + int dw = (int) (iw * scale), dh = (int) (ih * scale); + int dx = previewX + (previewW - dw) / 2, dy = previewY + (previewH - dh) / 2; + g.blit(tex, dx, dy, 0, 0, dw, dh, dw, dh); + g.drawString(font, iw + "x" + ih, previewX + 4, previewY + previewH - 10, 0xFFE0E0FF); + } else { + g.drawCenteredString(font, Component.translatable("config.justasplash.noPreview"), previewX + previewW / 2, previewY + previewH / 2, 0xFF8A8AC0); + } + } + + @Override + public void onClose() { Minecraft.getInstance().setScreen(parent); } +} diff --git a/src/main/java/me/sashegdev/justasplash/client/HotkeyTick.java b/src/main/java/me/sashegdev/justasplash/client/HotkeyTick.java index bbee1ff..1f6f336 100644 --- a/src/main/java/me/sashegdev/justasplash/client/HotkeyTick.java +++ b/src/main/java/me/sashegdev/justasplash/client/HotkeyTick.java @@ -7,9 +7,11 @@ import net.neoforged.neoforge.client.event.ClientTickEvent; @EventBusSubscriber(value = Dist.CLIENT, modid = "justasplash") public class HotkeyTick { + private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger("justasplash"); @SubscribeEvent public static void onTick(ClientTickEvent.Post e) { while (Hotkey.SPLASH.consumeClick()) { + LOG.info("[justasplash] hotkey Z pressed"); SplashManager.trigger(); } } diff --git a/src/main/java/me/sashegdev/justasplash/client/ImageLoader.java b/src/main/java/me/sashegdev/justasplash/client/ImageLoader.java index 34bf984..c52a44e 100644 --- a/src/main/java/me/sashegdev/justasplash/client/ImageLoader.java +++ b/src/main/java/me/sashegdev/justasplash/client/ImageLoader.java @@ -4,6 +4,8 @@ import com.mojang.blaze3d.platform.NativeImage; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.resources.ResourceLocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.imageio.ImageIO; import javax.imageio.ImageReader; @@ -13,7 +15,9 @@ import java.util.ArrayList; import java.util.List; public class ImageLoader { + private static final Logger LOG = LoggerFactory.getLogger("justasplash"); private static ResourceLocation texLoc; + private static ResourceLocation cachedLoc; private static int w, h; private static List frames = List.of(); private static int frameIdx = 0; @@ -21,22 +25,114 @@ public class ImageLoader { record Frame(ResourceLocation loc, int delayMs) {} + public static void preload(ResourceLocation loc) { + if (loc != null && loc.equals(cachedLoc) && texLoc != null && w > 0) { + LOG.info("[justasplash] preload skip cached {}", loc); + return; + } + load(loc); + } + public static void load(ResourceLocation loc) { + LOG.info("[justasplash] ImageLoader.load {}", loc); try { + byte[] data = null; + String p = loc.getPath().toLowerCase(); var rm = Minecraft.getInstance().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); + if (res != null) { + LOG.info("[justasplash] resource found: {}", loc); + try (InputStream in = res.open()) { data = in.readAllBytes(); } + } else { + java.nio.file.Path fp = resolveConfigFile(loc); + if (fp != null && java.nio.file.Files.exists(fp)) { + LOG.info("[justasplash] loading from config file {}", fp); + data = java.nio.file.Files.readAllBytes(fp); + p = fp.getFileName().toString().toLowerCase(); + } else { + LOG.warn("[justasplash] resource not found: {} (also checked config)", loc); + return; + } } + try (InputStream in2 = new java.io.ByteArrayInputStream(data)) { + if (p.endsWith(".gif")) loadGif(new java.io.ByteArrayInputStream(data)); + else if (p.endsWith(".png")) loadPng(new java.io.ByteArrayInputStream(data)); + else loadGeneric(new java.io.ByteArrayInputStream(data)); + } + cachedLoc = loc; + LOG.info("[justasplash] loaded tex={} w={} h={} frames={}", texLoc, w, h, frames.size()); } catch (Exception e) { + LOG.error("[justasplash] load failed {}", loc, e); e.printStackTrace(); } } + public static java.nio.file.Path resolveConfigFile(ResourceLocation loc) { + try { + String name = loc.getPath(); + if (name.contains("/")) name = name.substring(name.lastIndexOf('/') + 1); + java.nio.file.Path dir = java.nio.file.Paths.get("config/justasplash/assets"); + java.nio.file.Path direct = dir.resolve(name); + if (java.nio.file.Files.exists(direct)) return direct; + java.nio.file.Path byLoc = java.nio.file.Paths.get("config/justasplash/assets").resolve(loc.getPath()); + if (java.nio.file.Files.exists(byLoc)) return byLoc; + if (loc.getNamespace().equals("justasplash")) { + java.nio.file.Path alt = dir.resolve(loc.getPath().replace("textures/gui/splashes/", "").replace("sounds/", "")); + if (java.nio.file.Files.exists(alt)) return alt; + } + } catch (Exception ignored) {} + return null; + } + + public static java.util.List listConfigImages() { + try { + java.nio.file.Path dir = java.nio.file.Paths.get("config/justasplash/assets"); + if (!java.nio.file.Files.exists(dir)) return java.util.List.of(); + return java.nio.file.Files.list(dir).filter(p -> { + String n = p.getFileName().toString().toLowerCase(); + return n.endsWith(".png") || n.endsWith(".jpg") || n.endsWith(".jpeg") || n.endsWith(".gif"); + }).map(p -> p.getFileName().toString()).sorted().toList(); + } catch (Exception e) { return java.util.List.of(); } + } + + public static java.util.List listConfigSounds() { + try { + java.nio.file.Path dir = java.nio.file.Paths.get("config/justasplash/assets"); + if (!java.nio.file.Files.exists(dir)) return java.util.List.of(); + return java.nio.file.Files.list(dir).filter(p -> { + String n = p.getFileName().toString().toLowerCase(); + return n.endsWith(".ogg") || n.endsWith(".wav"); + }).map(p -> p.getFileName().toString()).sorted().toList(); + } catch (Exception e) { return java.util.List.of(); } + } + + private static void loadGeneric(InputStream in) throws Exception { + java.awt.image.BufferedImage bi = ImageIO.read(in); + if (bi == null) throw new java.io.IOException("ImageIO.read returned null for generic image"); + LOG.info("[justasplash] ImageIO read generic {}x{}", bi.getWidth(), bi.getHeight()); + NativeImage img = toNativeImage(bi); + w = img.getWidth(); + h = img.getHeight(); + register(img, 0); + frameIdx = 0; + lastFrameMs = 0; + } + private static void loadPng(InputStream in) throws Exception { - NativeImage img = NativeImage.read(in); + try { + NativeImage img = NativeImage.read(in); + w = img.getWidth(); + h = img.getHeight(); + register(img, 0); + frameIdx = 0; + lastFrameMs = 0; + return; + } catch (Exception e) { + LOG.warn("[justasplash] NativeImage.read failed, trying ImageIO fallback: {}", e.getMessage()); + } + java.awt.image.BufferedImage bi = ImageIO.read(in); + if (bi == null) throw new java.io.IOException("ImageIO.read returned null"); + NativeImage img = toNativeImage(bi); w = img.getWidth(); h = img.getHeight(); register(img, 0); @@ -79,6 +175,7 @@ public class ImageLoader { private static ResourceLocation registerImg(NativeImage img) { ResourceLocation loc = ResourceLocation.fromNamespaceAndPath("justasplash", "dynamic/splash_" + System.nanoTime()); Minecraft.getInstance().getTextureManager().register(loc, new DynamicTexture(img)); + LOG.info("[justasplash] registered dynamic texture {}", loc); return loc; } @@ -117,6 +214,16 @@ public class ImageLoader { return texLoc; } + public static void clearCache() { + texLoc = null; + cachedLoc = null; + w = 0; h = 0; + frames = List.of(); + frameIdx = 0; + lastFrameMs = 0; + LOG.info("[justasplash] cache cleared"); + } + public static int getWidth() { return w; } public static int getHeight() { return h; } } diff --git a/src/main/java/me/sashegdev/justasplash/client/ScreenSplashHandler.java b/src/main/java/me/sashegdev/justasplash/client/ScreenSplashHandler.java new file mode 100644 index 0000000..3117a92 --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/ScreenSplashHandler.java @@ -0,0 +1,16 @@ +package me.sashegdev.justasplash.client; + +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ScreenEvent; + +@EventBusSubscriber(value = Dist.CLIENT, modid = "justasplash") +public class ScreenSplashHandler { + @SubscribeEvent + public static void onScreenRender(ScreenEvent.Render.Post e) { + if (SplashManager.isShowing()) { + SplashRenderer.render(e.getGuiGraphics()); + } + } +} diff --git a/src/main/java/me/sashegdev/justasplash/client/SplashManager.java b/src/main/java/me/sashegdev/justasplash/client/SplashManager.java index 35283ca..92abeb1 100644 --- a/src/main/java/me/sashegdev/justasplash/client/SplashManager.java +++ b/src/main/java/me/sashegdev/justasplash/client/SplashManager.java @@ -5,9 +5,12 @@ import net.minecraft.client.Minecraft; import net.minecraft.resources.ResourceLocation; import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @OnlyIn(Dist.CLIENT) public class SplashManager { + private static final Logger LOG = LoggerFactory.getLogger("justasplash"); private static long startMs = 0; private static boolean showing = false; private static double fadeSec = 3.0; @@ -16,22 +19,32 @@ public class SplashManager { public static void init() { } - public static void trigger() { + public static boolean trigger() { var mc = Minecraft.getInstance(); - if (mc.level == null) return; + LOG.info("[justasplash] trigger called level={}", mc.level); + boolean allowInMenu = mc.level == null && mc.screen != null; + if (mc.level == null && !allowInMenu) { + LOG.warn("[justasplash] trigger ignored - no level and no screen"); + return false; + } String img = JustASplashConfig.SPLASH_IMAGE.get(); String snd = JustASplashConfig.SPLASH_SOUND.get(); fadeSec = JustASplashConfig.FADE.get(); double vol = JustASplashConfig.VOLUME.get(); + LOG.info("[justasplash] config img={} snd={} fade={} vol={}", img, snd, fadeSec, vol); ResourceLocation imgLoc = ResourceLocation.tryParse(img); ResourceLocation sndLoc = snd == null || snd.isEmpty() ? null : ResourceLocation.tryParse(snd); - if (imgLoc != null) ImageLoader.load(imgLoc); + if (imgLoc != null) ImageLoader.preload(imgLoc); + else LOG.warn("[justasplash] imgLoc parse failed: {}", img); if (sndLoc != null) { audioSec = AudioPlayer.play(sndLoc, (float) vol); + LOG.info("[justasplash] audioSec={}", audioSec); } else audioSec = 0; if (fadeSec < 0) fadeSec = audioSec > 0 ? audioSec : 3.0; startMs = System.currentTimeMillis(); showing = true; + LOG.info("[justasplash] showing=true startMs={} fadeSec={}", startMs, fadeSec); + return true; } public static boolean isShowing() { @@ -40,6 +53,7 @@ public class SplashManager { if (elapsed >= fadeSec && elapsed >= audioSec) { showing = false; if (audioSec > 0 && elapsed >= audioSec) AudioPlayer.stop(); + LOG.info("[justasplash] hiding after elapsed={}", elapsed); return false; } return true; diff --git a/src/main/java/me/sashegdev/justasplash/client/SplashRenderer.java b/src/main/java/me/sashegdev/justasplash/client/SplashRenderer.java index 3d78a79..cc401f1 100644 --- a/src/main/java/me/sashegdev/justasplash/client/SplashRenderer.java +++ b/src/main/java/me/sashegdev/justasplash/client/SplashRenderer.java @@ -1,33 +1,39 @@ 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.resources.ResourceLocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class SplashRenderer { + private static final Logger LOG = LoggerFactory.getLogger("justasplash"); + private static long lastLog = 0; public static void render(GuiGraphics g) { - if (!SplashManager.isShowing()) return; + boolean showing = SplashManager.isShowing(); + if (!showing) return; var mc = Minecraft.getInstance(); int w = mc.getWindow().getGuiScaledWidth(); int h = mc.getWindow().getGuiScaledHeight(); float a = SplashManager.getFadeAlpha(); if (a <= 0.01f) return; + long now = System.currentTimeMillis(); + if (now - lastLog > 500) { + lastLog = now; + ResourceLocation tex = ImageLoader.getTexture(); + LOG.info("[justasplash] render w={} h={} a={} tex={} iw={} ih={}", w, h, a, tex, ImageLoader.getWidth(), ImageLoader.getHeight()); + } RenderSystem.enableBlend(); - RenderSystem.setShaderColor(1f, 1f, 1f, a); + RenderSystem.defaultBlendFunc(); + g.setColor(1f, 1f, 1f, a); ResourceLocation tex = ImageLoader.getTexture(); if (tex != null && ImageLoader.getWidth() > 0) { - int iw = ImageLoader.getWidth(); - int ih = ImageLoader.getHeight(); - float scale = Math.max((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); + g.blit(tex, 0, 0, 0, 0, w, h, w, h); + } else { + LOG.warn("[justasplash] render skip tex null or w=0 tex={} w={}", tex, ImageLoader.getWidth()); } - RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + g.setColor(1f, 1f, 1f, 1f); RenderSystem.disableBlend(); } } diff --git a/src/main/java/me/sashegdev/justasplash/client/TitleScreenHandler.java b/src/main/java/me/sashegdev/justasplash/client/TitleScreenHandler.java new file mode 100644 index 0000000..9b250ed --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/TitleScreenHandler.java @@ -0,0 +1,21 @@ +package me.sashegdev.justasplash.client; + +import net.minecraft.client.gui.screens.TitleScreen; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ScreenEvent; + +@EventBusSubscriber(value = Dist.CLIENT, modid = "justasplash") +public class TitleScreenHandler { + private static boolean shown = false; + @SubscribeEvent + public static void onScreenOpening(ScreenEvent.Opening e) { + if (!shown && e.getNewScreen() instanceof TitleScreen ts) { + if (WelcomeScreen.shouldShow()) { + shown = true; + e.setNewScreen(new WelcomeScreen(ts)); + } + } + } +} diff --git a/src/main/java/me/sashegdev/justasplash/client/WelcomeScreen.java b/src/main/java/me/sashegdev/justasplash/client/WelcomeScreen.java new file mode 100644 index 0000000..55314e7 --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/client/WelcomeScreen.java @@ -0,0 +1,80 @@ +package me.sashegdev.justasplash.client; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.neoforged.fml.ModList; + +import java.io.FileReader; +import java.io.FileWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class WelcomeScreen extends Screen { + private final Screen parent; + private static final Gson GSON = new Gson(); + + public WelcomeScreen(Screen parent) { + super(Component.translatable("screen.justasplash.welcome.title")); + this.parent = parent; + } + + public static boolean shouldShow() { + try { + String cur = ModList.get().getModContainerById("justasplash").map(c -> c.getModInfo().getVersion().toString()).orElse("1.0.0"); + Path p = Paths.get("config/justasplash/first_launch.json"); + if (!Files.exists(p)) return true; + try (FileReader r = new FileReader(p.toFile())) { + JsonObject o = GSON.fromJson(r, JsonObject.class); + String seen = o.has("lastSeenVersion") ? o.get("lastSeenVersion").getAsString() : ""; + return !cur.equals(seen); + } + } catch (Exception e) { + return true; + } + } + + public static void markSeen() { + try { + String cur = ModList.get().getModContainerById("justasplash").map(c -> c.getModInfo().getVersion().toString()).orElse("1.0.0"); + Path p = Paths.get("config/justasplash/first_launch.json"); + Files.createDirectories(p.getParent()); + JsonObject o = new JsonObject(); + o.addProperty("lastSeenVersion", cur); + try (FileWriter w = new FileWriter(p.toFile())) { GSON.toJson(o, w); } + } catch (Exception ignored) {} + } + + @Override + protected void init() { + int cx = width / 2; + int y = height / 2 - 40; + addRenderableWidget(Button.builder(Component.translatable("screen.justasplash.welcome.test"), b -> { + SplashManager.trigger(); + }).bounds(cx - 105, y + 60, 100, 20).build()); + addRenderableWidget(Button.builder(Component.translatable("screen.justasplash.welcome.ok"), b -> { + markSeen(); + Minecraft.getInstance().setScreen(parent); + }).bounds(cx + 5, y + 60, 100, 20).build()); + } + + @Override + public void render(GuiGraphics g, int mx, int my, float delta) { + super.render(g, mx, my, delta); + int cx = width / 2; + int y = height / 2 - 40; + g.drawCenteredString(font, title, cx, y - 30, 0xFFFFFF); + g.drawCenteredString(font, Component.translatable("screen.justasplash.welcome.line1"), cx, y, 0xDDDDDD); + g.drawCenteredString(font, Component.translatable("screen.justasplash.welcome.line2"), cx, y + 12, 0xDDDDDD); + g.drawCenteredString(font, Component.translatable("screen.justasplash.welcome.line3"), cx, y + 24, 0xDDDDDD); + g.drawCenteredString(font, Component.translatable("screen.justasplash.welcome.line4"), cx, y + 36, 0xDDDDDD); + } + + @Override + public boolean shouldCloseOnEsc() { return false; } +} diff --git a/src/main/java/me/sashegdev/justasplash/config/JustASplashConfig.java b/src/main/java/me/sashegdev/justasplash/config/JustASplashConfig.java index 7def013..83ada8f 100644 --- a/src/main/java/me/sashegdev/justasplash/config/JustASplashConfig.java +++ b/src/main/java/me/sashegdev/justasplash/config/JustASplashConfig.java @@ -12,12 +12,12 @@ public class JustASplashConfig { static { ModConfigSpec.Builder b = new ModConfigSpec.Builder(); b.push("splash"); - SPLASH_IMAGE = b.comment("ResourceLocation png|gif in assets/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") + SPLASH_IMAGE = b.comment("ResourceLocation png|jpg|gif in assets/justasplash/ - e.g. justasplash:textures/gui/splashes/splash.png") + .define("splashImage", "justasplash:textures/gui/splashes/splash.jpg"); + SPLASH_SOUND = b.comment("ResourceLocation ogg/wav - 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); + FADE = b.comment("Fade duration seconds 100%->0%, -1 = use audio duration, default 2.5") + .defineInRange("fade", 2.5, -1.0, 60.0); VOLUME = b.comment("Sound volume 0.0-1.0") .defineInRange("volume", 1.0, 0.0, 1.0); b.pop(); diff --git a/src/main/java/me/sashegdev/justasplash/mixin/LibraryMixin.java b/src/main/java/me/sashegdev/justasplash/mixin/LibraryMixin.java new file mode 100644 index 0000000..083cdbf --- /dev/null +++ b/src/main/java/me/sashegdev/justasplash/mixin/LibraryMixin.java @@ -0,0 +1,30 @@ +package me.sashegdev.justasplash.mixin; + +import com.mojang.blaze3d.audio.Library; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +@Mixin(Library.class) +public class LibraryMixin { + @Inject(method = "init", at = @At("TAIL")) + private void justasplash$unlimitedChannels(String deviceName, boolean hrtf, CallbackInfo ci) { + try { + Class poolClass = Class.forName("com.mojang.blaze3d.audio.Library$CountingChannelPool"); + Constructor ctor = poolClass.getDeclaredConstructor(int.class); + ctor.setAccessible(true); + Object pool1 = ctor.newInstance(128); + Object pool2 = ctor.newInstance(128); + Field streaming = Library.class.getDeclaredField("streamingChannels"); + streaming.setAccessible(true); + streaming.set(this, pool1); + Field staticCh = Library.class.getDeclaredField("staticChannels"); + staticCh.setAccessible(true); + staticCh.set(this, pool2); + } catch (Exception ignored) {} + } +} diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index 54b81ad..412dc4b 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -23,3 +23,6 @@ description="Hotkey Z -> fullscreen png/gif + ogg/mp3 splash, 100% -> 0% fade, c versionRange="${minecraft_version_range}" ordering="NONE" side="CLIENT" + +[[mixins]] +config = "justasplash.mixins.json" 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..2c2ec7d --- /dev/null +++ b/src/main/resources/assets/justasplash/lang/en_us.json @@ -0,0 +1,32 @@ +{ + "key.justasplash.splash": "Trigger Splash", + "key.categories.justasplash": "Just A Splash", + "config.justasplash.title": "Just A Splash Config", + "config.justasplash.splashImage": "Splash Image", + "config.justasplash.splashImage.tooltip": "ResourceLocation like justasplash:textures/gui/splashes/splash.jpg or custom file in config/justasplash/assets/", + "config.justasplash.splashSound": "Splash Sound", + "config.justasplash.splashSound.tooltip": "Sound event like justasplash:splash or file path justasplash:sounds/splash.ogg, empty to disable", + "config.justasplash.fade": "Fade Duration", + "config.justasplash.fade.tooltip": "Seconds 100% -> 0%, -1 = audio duration", + "config.justasplash.volume": "Volume", + "config.justasplash.test": "Test Splash", + "config.justasplash.save": "Save", + "config.justasplash.cancel": "Cancel", + "screen.justasplash.welcome.title": "Welcome to Just A Splash!", + "screen.justasplash.welcome.line1": "Press Z for fullscreen splash", + "screen.justasplash.welcome.line2": "Fullscreen png/jpg/gif + ogg/mp3 overlay with fade", + "screen.justasplash.welcome.line3": "Does not block movement or mouse", + "screen.justasplash.welcome.line4": "Change via resource pack or config screen", + "screen.justasplash.welcome.test": "Test Splash", + "screen.justasplash.welcome.ok": "OK", + "toast.justasplash.ok": "Splash triggered!", + "toast.justasplash.error": "Failed to load splash image", + "command.justasplash.reload.success": "JustASplash reloaded", + "config.justasplash.hint1": "Drop png/jpg/gif into config/justasplash/assets/ and pick via \u25BE", + "config.justasplash.hint2": "or enter path like justasplash:textures/... (512 chars)", + "config.justasplash.preview": "Preview", + "config.justasplash.noPreview": "No preview", + "config.justasplash.fileDropdown": "File \u25BE", + "config.justasplash.nextImage": "Next image", + "config.justasplash.nextSound": "Next sound" +} 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..b914b28 --- /dev/null +++ b/src/main/resources/assets/justasplash/lang/ru_ru.json @@ -0,0 +1,32 @@ +{ + "key.justasplash.splash": "Показать сплеш", + "key.categories.justasplash": "Just A Splash", + "config.justasplash.title": "Настройки Just A Splash", + "config.justasplash.splashImage": "Картинка сплеша", + "config.justasplash.splashImage.tooltip": "ResourceLocation вроде justasplash:textures/gui/splashes/splash.jpg или файл в config/justasplash/assets/", + "config.justasplash.splashSound": "Звук сплеша", + "config.justasplash.splashSound.tooltip": "Событие justasplash:splash или путь justasplash:sounds/splash.ogg, пусто = без звука", + "config.justasplash.fade": "Длительность затухания", + "config.justasplash.fade.tooltip": "Секунды 100% -> 0%, -1 = длительность аудио", + "config.justasplash.volume": "Громкость", + "config.justasplash.test": "Протестить сплеш", + "config.justasplash.save": "Сохранить", + "config.justasplash.cancel": "Отмена", + "screen.justasplash.welcome.title": "Добро пожаловать в Just A Splash!", + "screen.justasplash.welcome.line1": "Нажми Z для фуллскрин сплеша", + "screen.justasplash.welcome.line2": "Фуллскрин png/jpg/gif + ogg/mp3 с затуханием", + "screen.justasplash.welcome.line3": "Не блокирует ходьбу и мышь", + "screen.justasplash.welcome.line4": "Меняй через ресурспак или экран настроек", + "screen.justasplash.welcome.test": "Протестить сплеш", + "screen.justasplash.welcome.ok": "Хорошо", + "toast.justasplash.ok": "Сплеш запущен!", + "toast.justasplash.error": "Не удалось загрузить картинку", + "command.justasplash.reload.success": "JustASplash перезагружен", + "config.justasplash.hint1": "Кидай png/jpg/gif в config/justasplash/assets/ и выбери \u25BE", + "config.justasplash.hint2": "или укажи путь вроде justasplash:textures/... (512 симв.)", + "config.justasplash.preview": "Превью", + "config.justasplash.noPreview": "Нет превью", + "config.justasplash.fileDropdown": "Файл \u25BE", + "config.justasplash.nextImage": "следующая картинка", + "config.justasplash.nextSound": "следующий звук" +} diff --git a/src/main/resources/assets/justasplash/sounds.json b/src/main/resources/assets/justasplash/sounds.json new file mode 100644 index 0000000..f122b5b --- /dev/null +++ b/src/main/resources/assets/justasplash/sounds.json @@ -0,0 +1,5 @@ +{ + "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 100755 index 0000000..849e5ab Binary files /dev/null and b/src/main/resources/assets/justasplash/sounds/splash.ogg differ diff --git a/src/main/resources/assets/justasplash/textures/gui/splashes/splash.jpg b/src/main/resources/assets/justasplash/textures/gui/splashes/splash.jpg new file mode 100755 index 0000000..b690632 Binary files /dev/null and b/src/main/resources/assets/justasplash/textures/gui/splashes/splash.jpg differ diff --git a/src/main/resources/justasplash.mixins.json b/src/main/resources/justasplash.mixins.json new file mode 100644 index 0000000..a189bab --- /dev/null +++ b/src/main/resources/justasplash.mixins.json @@ -0,0 +1,13 @@ +{ + "required": true, + "package": "me.sashegdev.justasplash.mixin", + "refmap": "justasplash.refmap.json", + "compatibilityLevel": "JAVA_21", + "client": [ + "LibraryMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "minVersion": "0.8" +}