Merge pull request 'Hider core' (#519) from HiderCore into main

Reviewed-on: SteamWar/SteamWar#519
Reviewed-by: FessmLP <72+fessmlp@noreply.localhost>
This commit is contained in:
2026-11-01 09:36:00 +01:00
69 changed files with 2385 additions and 1482 deletions
@@ -22,12 +22,18 @@ 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;
import java.util.List;
import java.util.Map;
@@ -38,6 +44,7 @@ public abstract class AbstractLinker<T> {
protected final T plugin;
private Map<Class<?>, Object> instances = new HashMap<>();
private final List<Object> fieldInstances = new ArrayList<>();
protected AbstractLinker(@NonNull T plugin) {
this.plugin = plugin;
@@ -90,6 +97,7 @@ public abstract class AbstractLinker<T> {
((Enable) any).enable();
}
linkObject(any);
linkFields(any);
});
} catch (SecurityException e) {
Throwable cause = e.getCause();
@@ -117,6 +125,70 @@ public abstract class AbstractLinker<T> {
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) {
for (Field field : any.getClass().getDeclaredFields()) {
if (field.getAnnotation(Linked.class) == null) continue;
Object value;
try {
field.setAccessible(true);
value = field.get(Modifier.isStatic(field.getModifiers()) ? null : any);
} catch (IllegalAccessException e) {
throw new SecurityException(e.getMessage(), e);
}
if (value == null) {
String message = "Linked field " + any.getClass().getName() + "." + field.getName() + " is not initialised";
throw new SecurityException(message, new IllegalStateException(message));
}
fieldInstances.add(value);
if (value instanceof Enable) {
((Enable) value).enable();
}
linkObject(value);
}
}
public final void unlink() {
@@ -126,6 +198,12 @@ public abstract class AbstractLinker<T> {
((Disable) any).disable();
}
});
fieldInstances.forEach(any -> {
unlinkObject(any);
if (any instanceof Disable) {
((Disable) any).disable();
}
});
}
public final void addLinkableInstance(@NonNull Object instance) {
@@ -152,6 +230,14 @@ public abstract class AbstractLinker<T> {
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.
*/
@@ -26,8 +26,12 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* On a class: the class is instantiated and linked.
* On a field of a linked class: the (already initialised) value of the field is linked as well, e.g. registered as listener.
*/
@IndexAnnotated
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Linked {
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}
}