package net.minecraft.client; import com.google.common.collect.ImmutableMap; import com.google.common.math.LongMath; import com.google.gson.JsonParser; import com.mojang.logging.LogUtils; import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.mojang.serialization.codecs.RecordCodecBuilder; import it.unimi.dsi.fastutil.objects.Object2BooleanFunction; import java.io.Reader; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.Util; import net.minecraft.client.gui.components.toasts.SystemToast; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.server.packs.resources.SimplePreparableReloadListener; import net.minecraft.util.profiling.ProfilerFiller; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class PeriodicNotificationManager extends SimplePreparableReloadListener>> implements AutoCloseable { private static final Codec>> CODEC = Codec.unboundedMap( Codec.STRING, RecordCodecBuilder.create( instance -> instance.group( Codec.LONG.optionalFieldOf("delay", 0L).forGetter(PeriodicNotificationManager.Notification::delay), Codec.LONG.fieldOf("period").forGetter(PeriodicNotificationManager.Notification::period), Codec.STRING.fieldOf("title").forGetter(PeriodicNotificationManager.Notification::title), Codec.STRING.fieldOf("message").forGetter(PeriodicNotificationManager.Notification::message) ) .apply(instance, PeriodicNotificationManager.Notification::new) ) .listOf() ); private static final Logger LOGGER = LogUtils.getLogger(); private final ResourceLocation notifications; private final Object2BooleanFunction selector; @Nullable private Timer timer; @Nullable private PeriodicNotificationManager.NotificationTask notificationTask; public PeriodicNotificationManager(ResourceLocation notifications, Object2BooleanFunction selector) { this.notifications = notifications; this.selector = selector; } protected Map> prepare(ResourceManager resourceManager, ProfilerFiller profilerFiller) { try { Reader reader = resourceManager.openAsReader(this.notifications); Map var4; try { var4 = (Map)CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).result().orElseThrow(); } catch (Throwable var7) { if (reader != null) { try { reader.close(); } catch (Throwable var6) { var7.addSuppressed(var6); } } throw var7; } if (reader != null) { reader.close(); } return var4; } catch (Exception var8) { LOGGER.warn("Failed to load {}", this.notifications, var8); return ImmutableMap.of(); } } protected void apply(Map> map, ResourceManager resourceManager, ProfilerFiller profilerFiller) { List list = (List)map.entrySet() .stream() .filter(entry -> this.selector.apply((String)entry.getKey())) .map(Entry::getValue) .flatMap(Collection::stream) .collect(Collectors.toList()); if (list.isEmpty()) { this.stopTimer(); } else if (list.stream().anyMatch(notification -> notification.period == 0L)) { Util.logAndPauseIfInIde("A periodic notification in " + this.notifications + " has a period of zero minutes"); this.stopTimer(); } else { long l = this.calculateInitialDelay(list); long m = this.calculateOptimalPeriod(list, l); if (this.timer == null) { this.timer = new Timer(); } if (this.notificationTask == null) { this.notificationTask = new PeriodicNotificationManager.NotificationTask(list, l, m); } else { this.notificationTask = this.notificationTask.reset(list, m); } this.timer.scheduleAtFixedRate(this.notificationTask, TimeUnit.MINUTES.toMillis(l), TimeUnit.MINUTES.toMillis(m)); } } public void close() { this.stopTimer(); } private void stopTimer() { if (this.timer != null) { this.timer.cancel(); } } private long calculateOptimalPeriod(List notifications, long delay) { return notifications.stream().mapToLong(notification -> { long m = notification.delay - delay; return LongMath.gcd(m, notification.period); }).reduce(LongMath::gcd).orElseThrow(() -> new IllegalStateException("Empty notifications from: " + this.notifications)); } private long calculateInitialDelay(List notifications) { return notifications.stream().mapToLong(notification -> notification.delay).min().orElse(0L); } @Environment(EnvType.CLIENT) public record Notification(long delay, long period, String title, String message) { public Notification(final long delay, final long period, final String title, final String message) { this.delay = delay != 0L ? delay : period; this.period = period; this.title = title; this.message = message; } } @Environment(EnvType.CLIENT) static class NotificationTask extends TimerTask { private final Minecraft minecraft = Minecraft.getInstance(); private final List notifications; private final long period; private final AtomicLong elapsed; public NotificationTask(List notifications, long elapsed, long period) { this.notifications = notifications; this.period = period; this.elapsed = new AtomicLong(elapsed); } public PeriodicNotificationManager.NotificationTask reset(List notifications, long period) { this.cancel(); return new PeriodicNotificationManager.NotificationTask(notifications, this.elapsed.get(), period); } public void run() { long l = this.elapsed.getAndAdd(this.period); long m = this.elapsed.get(); for (PeriodicNotificationManager.Notification notification : this.notifications) { if (l >= notification.delay) { long n = l / notification.period; long o = m / notification.period; if (n != o) { this.minecraft .execute( () -> SystemToast.add( Minecraft.getInstance().getToastManager(), SystemToast.SystemToastId.PERIODIC_NOTIFICATION, Component.translatable(notification.title, n), Component.translatable(notification.message, n) ) ); return; } } } } } }