From 9598b1c961ca31497bcb6915d05c7df09feb0b73 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 16:11:28 +0100 Subject: [PATCH] Add @LinkedTimer A method of a linked class annotated with @LinkedTimer(delay, period) is run repeatedly by the linker, on Spigot and Velocity. The method must not have parameters, its return value is ignored. It is called through a MethodHandle bound once when linking instead of reflection; LambdaMetafactory cannot be used as the linker and the linked classes are in different class loaders. The unit defaults to ticks and can be set to milliseconds up to hours; the Unit enum converts it to ticks for Spigot and to a TimeUnit for Velocity. HullHiderCommand uses it for its tick task. --- .../features/hullhider/HullHiderCommand.java | 4 +- .../de/steamwar/linkage/AbstractLinker.java | 52 ++++++++++++ .../src/de/steamwar/linkage/LinkedTimer.java | 81 +++++++++++++++++++ .../src/de/steamwar/linkage/SpigotLinker.java | 5 ++ .../steamwar/velocitycore/VelocityCore.java | 6 ++ 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 CommonCore/Linkage/src/de/steamwar/linkage/LinkedTimer.java diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java index b5c2a3cf..2b2a99aa 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java @@ -32,6 +32,7 @@ import de.steamwar.hullhider.HullRegion; import de.steamwar.hullhider.Viewers; import de.steamwar.linkage.Linked; import de.steamwar.linkage.LinkedInstance; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.techhider.ObfuscationConfig; import net.md_5.bungee.api.ChatMessageType; import org.bukkit.Bukkit; @@ -55,8 +56,6 @@ public class HullHiderCommand extends SWCommand implements Listener { public HullHiderCommand() { super("hullhider"); - - Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), this::onTick, 1, 1); } @Linked @@ -140,6 +139,7 @@ public class HullHiderCommand extends SWCommand implements Listener { }); } + @LinkedTimer(delay = 1, period = 1) private void onTick() { if (hidden.isEmpty()) return; diff --git a/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java b/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java index 00d4b43a..9959ecef 100644 --- a/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java +++ b/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java @@ -22,11 +22,16 @@ package de.steamwar.linkage; import de.steamwar.linkage.api.Disable; import de.steamwar.linkage.api.Enable; import lombok.NonNull; +import lombok.SneakyThrows; import lombok.experimental.StandardException; import java.io.BufferedReader; import java.io.InputStreamReader; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; @@ -120,6 +125,45 @@ public abstract class AbstractLinker { Throwable cause = e.getCause(); throw new LinkException(cause.getMessage(), cause); } + + // After the injection, as timers may run immediately on another thread + for (Object any : instances.values()) { + scheduleTimers(any); + } + for (Object any : fieldInstances) { + scheduleTimers(any); + } + } + + private void scheduleTimers(Object any) throws LinkException { + for (Method method : any.getClass().getDeclaredMethods()) { + LinkedTimer timer = method.getAnnotation(LinkedTimer.class); + if (timer == null) continue; + + String name = any.getClass().getName() + "." + method.getName(); + if (method.getParameterCount() != 0) { + throw new LinkException("Linked timer " + name + " must not have parameters"); + } + + MethodHandle handle; + try { + handle = MethodHandles.privateLookupIn(method.getDeclaringClass(), MethodHandles.lookup()).unreflect(method); + } catch (IllegalAccessException e) { + throw new LinkException("Could not access linked timer " + name, e); + } + if (!Modifier.isStatic(method.getModifiers())) { + handle = handle.bindTo(any); + } + // Discards the return value + MethodHandle timerHandle = handle.asType(MethodType.methodType(void.class)); + + scheduleTimer(() -> runTimer(timerHandle), timer.delay(), timer.period(), timer.unit()); + } + } + + @SneakyThrows + private static void runTimer(MethodHandle timer) { + timer.invokeExact(); } private void linkFields(Object any) { @@ -186,6 +230,14 @@ public abstract class AbstractLinker { protected void linkObject(Object any) { } + /** + * Runs the runnable repeatedly for a {@link LinkedTimer}. + * + * @param delay time until the first run, in unit + * @param period time between two runs, in unit + */ + protected abstract void scheduleTimer(Runnable runnable, long delay, long period, LinkedTimer.Unit unit); + /** * There is no need in calling {@link Disable#disable()} ()} by this method. */ diff --git a/CommonCore/Linkage/src/de/steamwar/linkage/LinkedTimer.java b/CommonCore/Linkage/src/de/steamwar/linkage/LinkedTimer.java new file mode 100644 index 00000000..fdf96607 --- /dev/null +++ b/CommonCore/Linkage/src/de/steamwar/linkage/LinkedTimer.java @@ -0,0 +1,81 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.linkage; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * Runs the annotated method of a linked class (or of the value of a {@link Linked} field) repeatedly. + * The method must not have parameters, its return value is ignored. It may be static. + * The timer is not stopped on unlink, as the server stops anyway. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface LinkedTimer { + + /** + * Time until the first run, in {@link #unit()}. + */ + long delay(); + + /** + * Time between two runs, in {@link #unit()}. + */ + long period(); + + Unit unit() default Unit.TICKS; + + @AllArgsConstructor + enum Unit { + /** + * 1 tick = 50 ms + */ + TICKS(50, TimeUnit.MILLISECONDS), + MILLISECONDS(1, TimeUnit.MILLISECONDS), + SECONDS(1, TimeUnit.SECONDS), + MINUTES(1, TimeUnit.MINUTES), + HOURS(1, TimeUnit.HOURS); + + private final long factor; + @Getter + private final TimeUnit timeUnit; + + /** + * @return the value in ticks, rounded down + */ + public long toTicks(long value) { + return timeUnit.toMillis(value * factor) / 50; + } + + /** + * @return the value in {@link #getTimeUnit()} + */ + public long toTimeUnit(long value) { + return value * factor; + } + } +} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/linkage/SpigotLinker.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/linkage/SpigotLinker.java index 37ccaae3..36c0532f 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/linkage/SpigotLinker.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/linkage/SpigotLinker.java @@ -61,6 +61,11 @@ public class SpigotLinker extends AbstractLinker { } } + @Override + protected void scheduleTimer(Runnable runnable, long delay, long period, LinkedTimer.Unit unit) { + Bukkit.getScheduler().runTaskTimer(plugin, runnable, unit.toTicks(delay), unit.toTicks(period)); + } + @Override protected void unlinkObject(Object any) { if (any instanceof Listener) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java index 1701a10d..d94e8e3d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java +++ b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java @@ -32,6 +32,7 @@ import de.steamwar.command.*; import de.steamwar.linkage.AbstractLinker; import de.steamwar.linkage.EventMode; import de.steamwar.linkage.LinkedInstance; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.messages.Chatter; import de.steamwar.network.packets.PacketHandler; import de.steamwar.persistent.ReloadablePlugin; @@ -164,6 +165,11 @@ public class VelocityCore implements ReloadablePlugin { } } + @Override + protected void scheduleTimer(Runnable runnable, long delay, long period, LinkedTimer.Unit unit) { + schedule(runnable).delay(unit.toTimeUnit(delay), unit.getTimeUnit()).repeat(unit.toTimeUnit(period), unit.getTimeUnit()).schedule(); + } + @Override protected void unlinkObject(Object any) { if (any instanceof PacketHandler) {