1
0
forked from Clones/Controlify

registry for screenop

This commit is contained in:
isXander
2023-03-27 19:11:11 +01:00
parent b426312a8e
commit 8f5d42e5aa
13 changed files with 370 additions and 106 deletions

View File

@ -124,7 +124,6 @@ public class Controlify implements ControlifyApi {
}
public void tick(Minecraft client) {
var minecraft = Minecraft.getInstance();
if (minecraft.getOverlay() == null) {
if (!calibrationQueue.isEmpty()) {
Screen screen = minecraft.screen;

View File

@ -12,7 +12,7 @@ import java.util.function.Function;
* Adds a guide to a button. This does not invoke the button press on binding trigger, only renders the guide.
* This should be called every time a button is initialised, like in {@link Screen#init()}
*/
public interface ButtonGuideApi {
public final class ButtonGuideApi {
/**
* Makes the button render the image of the binding specified.
* This does not invoke the button press on binding trigger, only renders the guide.
@ -23,7 +23,7 @@ public interface ButtonGuideApi {
* @param position where the guide should be rendered relative to the button
* @param renderPredicate whether the guide should be rendered
*/
static <T extends AbstractButton> void addGuideToButton(
public static <T extends AbstractButton> void addGuideToButton(
T button,
Function<ControllerBindings<?>, ControllerBinding<?>> binding,
ButtonRenderPosition position,

View File

@ -1,7 +1,9 @@
package dev.isxander.controlify.mixins.feature.screenop.vanilla;
package dev.isxander.controlify.mixins.feature.screenop;
import dev.isxander.controlify.screenop.ComponentProcessorProvider;
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
import dev.isxander.controlify.screenop.ScreenProcessor;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@ -21,6 +23,13 @@ public class ScreenMixin implements ScreenProcessorProvider {
@Inject(method = "rebuildWidgets", at = @At("RETURN"))
private void onScreenInit(CallbackInfo ci) {
screenProcessor().onWidgetRebuild();
// cannot use screenProcessor() because it may be overriden by registry
ScreenProcessorProvider.provide((Screen) (Object) this).onWidgetRebuild();
}
@Inject(method = "init(Lnet/minecraft/client/Minecraft;II)V", at = @At("HEAD"))
private void clearRegistryCaches(Minecraft client, int width, int height, CallbackInfo ci) {
ScreenProcessorProvider.REGISTRY.clearCache();
ComponentProcessorProvider.REGISTRY.clearCache();
}
}

View File

@ -8,6 +8,10 @@ public interface ComponentProcessorProvider {
static ComponentProcessor provide(GuiEventListener component) {
if (component instanceof ComponentProcessorProvider provider)
return provider.componentProcessor();
return ComponentProcessor.EMPTY;
return REGISTRY.get(component).orElse(ComponentProcessor.EMPTY);
}
Registry<GuiEventListener, ComponentProcessor> REGISTRY = new Registry<>();
}

View File

@ -0,0 +1,50 @@
package dev.isxander.controlify.screenop;
import org.jetbrains.annotations.ApiStatus;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
@SuppressWarnings("unchecked")
public class Registry<T, U> {
private final Map<Class<? extends T>, Function<T, U>> registry;
private final Map<T, U> cache;
public Registry() {
this.registry = new HashMap<>();
this.cache = new HashMap<>();
}
/**
* Registers a constructor for a class
*
* @param clazz the class to bind the constructor to
* @param constructor function to build the object from the class
* @param <V> type of class
*/
public <V extends T> void register(Class<V> clazz, Function<V, U> constructor) {
registry.put(clazz, (Function<T, U>) constructor);
}
Optional<U> get(T object) {
U cached = this.cache.get(object);
if (cached != null)
return Optional.of(cached);
Class<? extends T> clazz = (Class<? extends T>) object.getClass();
Function<T, U> constructor = registry.get(clazz);
if (constructor == null)
return Optional.empty();
U constructed = constructor.apply(object);
this.cache.put(object, constructed);
return Optional.of(constructed);
}
@ApiStatus.Internal
public void clearCache() {
this.cache.clear();
}
}

View File

@ -2,10 +2,20 @@ package dev.isxander.controlify.screenop;
import net.minecraft.client.gui.screens.Screen;
import java.util.Optional;
public interface ScreenProcessorProvider {
ScreenProcessor<?> screenProcessor();
static ScreenProcessor<?> provide(Screen screen) {
Optional<ScreenProcessor<?>> optional = REGISTRY.get(screen);
if (optional.isPresent()) return optional.get();
return ((ScreenProcessorProvider) screen).screenProcessor();
}
/**
* Register a screen processor for a screen from an entrypoint
*/
Registry<Screen, ScreenProcessor<?>> REGISTRY = new Registry<>();
}

View File

@ -30,6 +30,7 @@
"feature.guide.screen.AbstractButtonMixin",
"feature.guide.screen.AbstractWidgetMixin",
"feature.guide.screen.TabNavigationBarMixin",
"feature.screenop.ScreenMixin",
"feature.screenop.vanilla.AbstractButtonMixin",
"feature.screenop.vanilla.AbstractContainerEventHandlerMixin",
"feature.screenop.vanilla.AbstractContainerScreenMixin",
@ -44,7 +45,6 @@
"feature.screenop.vanilla.LanguageSelectionListEntryMixin",
"feature.screenop.vanilla.OptionsSubScreenAccessor",
"feature.screenop.vanilla.ScreenAccessor",
"feature.screenop.vanilla.ScreenMixin",
"feature.screenop.vanilla.SelectWorldScreenAccessor",
"feature.screenop.vanilla.SelectWorldScreenMixin",
"feature.screenop.vanilla.ServerSelectionListEntryMixin",

View File

@ -1,17 +1,13 @@
package dev.isxander.controlify.test;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerConfig;
import dev.isxander.controlify.controller.ControllerState;
import dev.isxander.controlify.controller.ControllerType;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Screenshot;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Predicate;
@ -52,86 +48,21 @@ public class ClientTestHelper {
return submit(function).join();
}
public static Controller<?, ?> createFakeController() {
return new Controller<>() {
private final ControllerBindings<ControllerState> bindings = new ControllerBindings<>(this);
private final ControllerConfig config = new ControllerConfig() {
@Override
public void setDeadzone(int axis, float deadzone) {
public static void takeScreenshot(String name) {
AtomicBoolean returned = new AtomicBoolean(false);
submitAndWait(mc -> {
Screenshot.grab(mc.gameDirectory, name+".png", mc.getMainRenderTarget(), text -> returned.set(true));
return true;
});
waitFor("Screenshot to be taken", mc -> returned.get(), Duration.ofSeconds(2));
}
@Override
public float getDeadzone(int axis) {
return 0;
}
};
@Override
public String uid() {
return "FAKE";
}
@Override
public int joystickId() {
return -1;
}
@Override
public ControllerBindings<ControllerState> bindings() {
return bindings;
}
@Override
public ControllerConfig config() {
return config;
}
@Override
public ControllerConfig defaultConfig() {
return config;
}
@Override
public void resetConfig() {
}
@Override
public void setConfig(Gson gson, JsonElement json) {
}
@Override
public ControllerType type() {
return ControllerType.UNKNOWN;
}
@Override
public String name() {
return "FAKE CONTROLLER";
}
@Override
public ControllerState state() {
return ControllerState.EMPTY;
}
@Override
public ControllerState prevState() {
return ControllerState.EMPTY;
}
@Override
public void updateState() {
}
@Override
public void clearState() {
}
};
public static FakeController createAndUseDummyController() {
var controller = new FakeController();
Controller.CONTROLLERS.put(controller.uid(), controller);
controller.use();
return controller;
}
}

View File

@ -10,7 +10,7 @@ import java.util.List;
import static dev.isxander.controlify.test.ClientTestHelper.*;
public class ControlifyAutoTestClient implements ClientModInitializer {
private static final Logger LOGGER = LoggerFactory.getLogger("Controlify Auto Test");
public static final Logger LOGGER = LoggerFactory.getLogger("Controlify Auto Test");
@Override
public void onInitializeClient() {
@ -36,8 +36,8 @@ public class ControlifyAutoTestClient implements ClientModInitializer {
for (var method : methods) {
method.setAccessible(true);
Test.PreLoad preLoad = method.getAnnotation(Test.PreLoad.class);
if (preLoad != null) {
Test.Entrypoint entrypoint = method.getAnnotation(Test.Entrypoint.class);
if (entrypoint != null) {
if (method.getParameterCount() > 0)
throw new RuntimeException("PreLoad test method " + method.getName() + " has parameters!");
@ -47,11 +47,11 @@ public class ControlifyAutoTestClient implements ClientModInitializer {
} catch (Exception e) {
throw new RuntimeException(e);
}
}, preLoad.value()));
}, entrypoint.value()));
}
Test.PostLoad postLoad = method.getAnnotation(Test.PostLoad.class);
if (postLoad != null) {
Test.TitleScreen titleScreen = method.getAnnotation(Test.TitleScreen.class);
if (titleScreen != null) {
if (method.getParameterCount() > 0)
throw new RuntimeException("PostLoad test method " + method.getName() + " has parameters!");
@ -61,7 +61,7 @@ public class ControlifyAutoTestClient implements ClientModInitializer {
} catch (Exception e) {
throw new RuntimeException(e);
}
}, postLoad.value()));
}, titleScreen.value()));
}
}
@ -88,11 +88,13 @@ public class ControlifyAutoTestClient implements ClientModInitializer {
private boolean wrapTestExecution(Test test) {
LOGGER.info("\u001b[36mRunning test " + test.name() + "...");
try {
test.method().run();
test.runTest();
LOGGER.info("\u001b[32mPassed test " + test.name() + "!");
takeScreenshot(test.name());
return true;
} catch (Throwable t) {
LOGGER.error("\u001b[31mFailed test " + test.name() + "!", t);
takeScreenshot(test.name());
return false;
}
}

View File

@ -6,6 +6,10 @@ import dev.isxander.controlify.api.bind.ControlifyBindingsApi;
import dev.isxander.controlify.api.event.ControlifyEvents;
import dev.isxander.controlify.bindings.BindingSupplier;
import dev.isxander.controlify.bindings.GamepadBinds;
import dev.isxander.controlify.screenop.ScreenProcessor;
import dev.isxander.controlify.screenop.ScreenProcessorProvider;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraft.resources.ResourceLocation;
import java.util.concurrent.atomic.AtomicBoolean;
@ -15,8 +19,9 @@ import static dev.isxander.controlify.test.ClientTestHelper.*;
public class ControlifyTests {
BindingSupplier binding = null;
boolean titleScreenProcessorPresent = false;
@Test.PreLoad("Binding registry test")
@Test.Entrypoint("Binding registry test")
void bindingRegistryTest() {
var registry = ControlifyBindingsApi.get();
assertNotNull(registry, "Binding registry is null");
@ -24,13 +29,14 @@ public class ControlifyTests {
assertNotNull(binding, "Bind registry failed - BindingSupplier is null");
}
@Test.PostLoad("BindingSupplier getter test")
@Test.TitleScreen("BindingSupplier getter test")
void bindingSupplierGetterTest() {
var controller = createFakeController();
var controller = createAndUseDummyController();
assertNotNull(binding.get(controller), "Bind registry failed - Bind for fake controller is null");
controller.finish();
}
@Test.PostLoad("Input mode changed event test")
@Test.TitleScreen("Input mode changed event test")
void checkInputModeChangedEvent() {
var api = ControlifyApi.get();
@ -42,4 +48,20 @@ public class ControlifyTests {
assertTrue(called.get(), "Input mode changed event was not called");
}
@Test.Entrypoint("Screen component registry setup test")
void setupScreenComponentRegistry() {
ScreenProcessorProvider.REGISTRY.register(TitleScreen.class, ts -> new ScreenProcessor<>(ts){
@Override
public void onWidgetRebuild() {
super.onWidgetRebuild();
titleScreenProcessorPresent = true;
}
});
}
@Test.TitleScreen("Screen component registry test")
void checkScreenComponentRegistry() {
assertTrue(titleScreenProcessorPresent, "Screen processor was not called");
}
}

View File

@ -0,0 +1,10 @@
package dev.isxander.controlify.test;
import java.util.List;
public record DiscoveredTests(List<Test> entrypointTests, List<Test> titleScreenTests) {
public boolean hasRanAllTests() {
return entrypointTests.stream().allMatch(Test::hasRan)
&& titleScreenTests.stream().allMatch(Test::hasRan);
}
}

View File

@ -0,0 +1,178 @@
package dev.isxander.controlify.test;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import dev.isxander.controlify.Controlify;
import dev.isxander.controlify.bindings.ControllerBindings;
import dev.isxander.controlify.controller.Controller;
import dev.isxander.controlify.controller.ControllerType;
import dev.isxander.controlify.controller.joystick.JoystickConfig;
import dev.isxander.controlify.controller.joystick.JoystickController;
import dev.isxander.controlify.controller.joystick.JoystickState;
import dev.isxander.controlify.controller.joystick.mapping.JoystickMapping;
import dev.isxander.controlify.controller.joystick.mapping.UnmappedJoystickMapping;
import java.util.List;
public class FakeController implements JoystickController<JoystickConfig> {
public static int JOYSTICK_COUNT = 0;
private final String uid;
private final int id;
private final ControllerBindings<JoystickState> bindings;
private final JoystickConfig config;
private JoystickState state = JoystickState.EMPTY, prevState = JoystickState.EMPTY;
private float axisState;
private boolean shouldClearAxisNextTick;
private boolean buttonState, shouldButtonPressNextTick;
private JoystickState.HatState hatState = JoystickState.HatState.CENTERED;
private boolean shouldCenterHatNextTick;
public FakeController() {
this.uid = "FAKE-" + JOYSTICK_COUNT++;
this.id = -JOYSTICK_COUNT;
this.bindings = new ControllerBindings<>(this);
this.config = new JoystickConfig(this);
this.config.calibrated = true;
}
@Override
public String uid() {
return uid;
}
@Override
public int joystickId() {
return id;
}
@Override
public ControllerBindings<JoystickState> bindings() {
return bindings;
}
@Override
public JoystickState state() {
return state;
}
@Override
public JoystickState prevState() {
return prevState;
}
@Override
public JoystickConfig config() {
return config;
}
@Override
public JoystickConfig defaultConfig() {
return config;
}
@Override
public void resetConfig() {
throw new UnsupportedOperationException();
}
@Override
public void setConfig(Gson gson, JsonElement json) {
throw new UnsupportedOperationException();
}
@Override
public ControllerType type() {
return ControllerType.UNKNOWN;
}
@Override
public String name() {
return "Fake Controller";
}
@Override
public void updateState() {
buttonState = shouldButtonPressNextTick;
shouldButtonPressNextTick = false;
state = new FakeControllerState(mapping(), axisState, buttonState, hatState);
if (shouldClearAxisNextTick) {
shouldClearAxisNextTick = false;
axisState = 0f;
}
if (shouldCenterHatNextTick) {
shouldCenterHatNextTick = false;
hatState = JoystickState.HatState.CENTERED;
}
}
@Override
public void clearState() {
state = JoystickState.EMPTY;
}
@Override
public JoystickMapping mapping() {
return UnmappedJoystickMapping.INSTANCE;
}
@Override
public int axisCount() {
return 1;
}
@Override
public int buttonCount() {
return 1;
}
@Override
public int hatCount() {
return 1;
}
public void setAxis(float axis, boolean clearNextTick) {
this.axisState = axis;
this.shouldClearAxisNextTick = clearNextTick;
}
public void clearAxisNextTick() {
this.shouldClearAxisNextTick = true;
}
public void pressButtonNextTick() {
this.shouldButtonPressNextTick = true;
}
public void setHat(JoystickState.HatState hatState, boolean clearNextTick) {
this.hatState = hatState;
this.shouldCenterHatNextTick = clearNextTick;
}
public void clearHatNextTick() {
this.shouldCenterHatNextTick = true;
}
public void use() {
Controlify.instance().setCurrentController(this);
}
public void finish() {
Controlify.instance().setCurrentController(null);
Controller.CONTROLLERS.remove(uid, this);
}
@Override
public boolean canBeUsed() {
return true;
}
public static class FakeControllerState extends JoystickState {
protected FakeControllerState(JoystickMapping mapping, float axis, boolean button, HatState hat) {
super(mapping, List.of(axis), List.of(axis), List.of(button), List.of(hat));
}
}
}

View File

@ -4,17 +4,66 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Objects;
public final class Test {
private final Runnable method;
private final String name;
private boolean hasRan;
public Test(Runnable method, String name) {
this.method = method;
this.name = name;
}
public void runTest() {
if (hasRan)
throw new IllegalStateException("Test `" + name + "` cannot run twice.");
method.run();
hasRan = true;
}
public String name() {
return name;
}
public boolean hasRan() {
return hasRan;
}
@Override
public boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null || obj.getClass() != this.getClass()) return false;
var that = (Test) obj;
return Objects.equals(this.method, that.method) &&
Objects.equals(this.name, that.name);
}
@Override
public int hashCode() {
return Objects.hash(method, name);
}
@Override
public String toString() {
return "Test[" +
"method=" + method + ", " +
"name=" + name + ']';
}
public record Test(Runnable method, String name) {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PreLoad {
public @interface Entrypoint {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PostLoad {
public @interface TitleScreen {
String value();
}
}