forked from Clones/Controlify
bind menu & button rendering
This commit is contained in:
@ -33,7 +33,7 @@ public enum Bind {
|
||||
Bind(BiFunction<ControllerState, Controller, Boolean> state, String identifier) {
|
||||
this.state = state;
|
||||
this.identifier = identifier;
|
||||
this.textureLocation = new ResourceLocation("controlify", "textures/gui/buttons/" + identifier + ".png");
|
||||
this.textureLocation = new ResourceLocation("controlify", "textures/gui/buttons/xbox/" + identifier + ".png");
|
||||
}
|
||||
|
||||
Bind(Function<ControllerState, Boolean> state, String identifier) {
|
||||
|
@ -17,7 +17,7 @@ public class ControllerBinding {
|
||||
this.controller = controller;
|
||||
this.bind = this.defaultBind = defaultBind;
|
||||
this.id = id;
|
||||
this.name = Component.translatable("controlify.binding." + id);
|
||||
this.name = Component.translatable("controlify.binding." + id.getNamespace() + "." + id.getPath());
|
||||
this.description = description;
|
||||
this.override = override;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.InputMode;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.event.ControlifyEvents;
|
||||
import dev.isxander.controlify.mixins.KeyMappingAccessor;
|
||||
import dev.isxander.controlify.mixins.feature.bind.KeyMappingAccessor;
|
||||
import net.minecraft.client.KeyMapping;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
@ -17,6 +17,7 @@ public class ControllerBindings {
|
||||
JUMP, SNEAK,
|
||||
ATTACK, USE,
|
||||
SPRINT,
|
||||
DROP,
|
||||
NEXT_SLOT, PREV_SLOT,
|
||||
PAUSE,
|
||||
INVENTORY,
|
||||
@ -24,7 +25,7 @@ public class ControllerBindings {
|
||||
OPEN_CHAT,
|
||||
GUI_PRESS, GUI_BACK;
|
||||
|
||||
private final Map<ResourceLocation, ControllerBinding> registry = new HashMap<>();
|
||||
private final Map<ResourceLocation, ControllerBinding> registry = new LinkedHashMap<>();
|
||||
|
||||
public ControllerBindings(Controller controller) {
|
||||
var options = Minecraft.getInstance().options;
|
||||
@ -34,6 +35,7 @@ public class ControllerBindings {
|
||||
register(ATTACK = new ControllerBinding(controller, Bind.RIGHT_TRIGGER, new ResourceLocation("controlify", "attack"), options.keyAttack));
|
||||
register(USE = new ControllerBinding(controller, Bind.LEFT_TRIGGER, new ResourceLocation("controlify", "use"), options.keyUse));
|
||||
register(SPRINT = new ControllerBinding(controller, Bind.LEFT_STICK, new ResourceLocation("controlify", "sprint"), options.keySprint));
|
||||
register(DROP = new ControllerBinding(controller, Bind.DPAD_DOWN, new ResourceLocation("controlify", "drop"), options.keyDrop));
|
||||
register(NEXT_SLOT = new ControllerBinding(controller, Bind.RIGHT_BUMPER, new ResourceLocation("controlify", "next_slot"), null));
|
||||
register(PREV_SLOT = new ControllerBinding(controller, Bind.LEFT_BUMPER, new ResourceLocation("controlify", "prev_slot"), null));
|
||||
register(PAUSE = new ControllerBinding(controller, Bind.START, new ResourceLocation("controlify", "pause"), null));
|
||||
@ -43,6 +45,7 @@ public class ControllerBindings {
|
||||
register(GUI_PRESS = new ControllerBinding(controller, Bind.A_BUTTON, new ResourceLocation("controlify", "gui_press"), null));
|
||||
register(GUI_BACK = new ControllerBinding(controller, Bind.B_BUTTON, new ResourceLocation("controlify", "gui_back"), null));
|
||||
|
||||
|
||||
ControlifyEvents.CONTROLLER_BIND_REGISTRY.invoker().onRegisterControllerBinds(this, controller);
|
||||
|
||||
ControlifyEvents.CONTROLLER_STATE_UPDATED.register(this::imitateVanillaClick);
|
||||
|
@ -3,17 +3,22 @@ package dev.isxander.controlify.compatibility.screen;
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.InputMode;
|
||||
import dev.isxander.controlify.compatibility.screen.component.ComponentProcessorProvider;
|
||||
import dev.isxander.controlify.compatibility.screen.component.CustomFocus;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.mixins.ScreenAccessor;
|
||||
import dev.isxander.controlify.mixins.compat.screen.vanilla.ScreenAccessor;
|
||||
import net.minecraft.client.gui.ComponentPath;
|
||||
import net.minecraft.client.gui.components.events.AbstractContainerEventHandler;
|
||||
import net.minecraft.client.gui.components.events.GuiEventListener;
|
||||
import net.minecraft.client.gui.navigation.FocusNavigationEvent;
|
||||
import net.minecraft.client.gui.navigation.ScreenDirection;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
|
||||
public class ScreenProcessor {
|
||||
private static final int REPEAT_DELAY = 5;
|
||||
private static final int INITIAL_REPEAT_DELAY = 20;
|
||||
private static final int REPEAT_DELAY = 3;
|
||||
|
||||
public final Screen screen;
|
||||
private int lastMoved = 0;
|
||||
@ -28,15 +33,16 @@ public class ScreenProcessor {
|
||||
}
|
||||
|
||||
protected void handleComponentNavigation(Controller controller) {
|
||||
if (screen.getFocused() != null) {
|
||||
var focused = screen.getFocused();
|
||||
var focusTree = getFocusTree();
|
||||
while (!focusTree.isEmpty()) {
|
||||
var focused = focusTree.poll();
|
||||
var processor = ComponentProcessorProvider.provide(focused);
|
||||
if (processor.overrideControllerNavigation(this, controller)) return;
|
||||
}
|
||||
|
||||
var accessor = (ScreenAccessor) screen;
|
||||
|
||||
boolean repeatEventAvailable = ++lastMoved > INITIAL_REPEAT_DELAY;
|
||||
boolean repeatEventAvailable = ++lastMoved >= REPEAT_DELAY;
|
||||
|
||||
var axes = controller.state().axes();
|
||||
var prevAxes = controller.prevState().axes();
|
||||
@ -67,14 +73,15 @@ public class ScreenProcessor {
|
||||
if (path != null) {
|
||||
accessor.invokeChangeFocus(path);
|
||||
ComponentProcessorProvider.provide(path.component()).onNavigateTo(this, controller);
|
||||
lastMoved = repeatEventAvailable ? INITIAL_REPEAT_DELAY - REPEAT_DELAY : 0;
|
||||
lastMoved = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleButtons(Controller controller) {
|
||||
if (screen.getFocused() != null) {
|
||||
var focused = screen.getFocused();
|
||||
var focusTree = getFocusTree();
|
||||
while (!focusTree.isEmpty()) {
|
||||
var focused = focusTree.poll();
|
||||
var processor = ComponentProcessorProvider.provide(focused);
|
||||
if (processor.overrideControllerButtons(this, controller)) return;
|
||||
}
|
||||
@ -94,4 +101,18 @@ public class ScreenProcessor {
|
||||
accessor.invokeChangeFocus(path);
|
||||
}
|
||||
}
|
||||
|
||||
protected Queue<GuiEventListener> getFocusTree() {
|
||||
if (screen.getFocused() == null) return new ArrayDeque<>();
|
||||
|
||||
var tree = new ArrayDeque<GuiEventListener>();
|
||||
var focused = screen.getFocused();
|
||||
tree.add(focused);
|
||||
while (focused instanceof CustomFocus customFocus) {
|
||||
focused = customFocus.getCustomFocus();
|
||||
tree.addFirst(focused);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
|
@ -7,23 +7,17 @@ import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controller.ControllerState;
|
||||
import net.minecraft.client.gui.components.events.GuiEventListener;
|
||||
|
||||
public class ComponentProcessor<T extends GuiEventListener> {
|
||||
static final ComponentProcessor<?> EMPTY = new ComponentProcessor<>(null);
|
||||
public interface ComponentProcessor {
|
||||
ComponentProcessor EMPTY = new ComponentProcessor(){};
|
||||
|
||||
protected final T component;
|
||||
|
||||
public ComponentProcessor(T component) {
|
||||
this.component = component;
|
||||
}
|
||||
|
||||
public boolean overrideControllerNavigation(ScreenProcessor screen, Controller controller) {
|
||||
default boolean overrideControllerNavigation(ScreenProcessor screen, Controller controller) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) {
|
||||
default boolean overrideControllerButtons(ScreenProcessor screen, Controller controller) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onNavigateTo(ScreenProcessor screen, Controller controller) {
|
||||
default void onNavigateTo(ScreenProcessor screen, Controller controller) {
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ package dev.isxander.controlify.compatibility.screen.component;
|
||||
import net.minecraft.client.gui.components.events.GuiEventListener;
|
||||
|
||||
public interface ComponentProcessorProvider {
|
||||
ComponentProcessor<?> componentProcessor();
|
||||
ComponentProcessor componentProcessor();
|
||||
|
||||
static ComponentProcessor<?> provide(GuiEventListener component) {
|
||||
static ComponentProcessor provide(GuiEventListener component) {
|
||||
if (component instanceof ComponentProcessorProvider provider)
|
||||
return provider.componentProcessor();
|
||||
return ComponentProcessor.EMPTY;
|
||||
|
@ -0,0 +1,7 @@
|
||||
package dev.isxander.controlify.compatibility.screen.component;
|
||||
|
||||
import net.minecraft.client.gui.components.events.GuiEventListener;
|
||||
|
||||
public interface CustomFocus {
|
||||
GuiEventListener getCustomFocus();
|
||||
}
|
@ -8,15 +8,17 @@ import org.lwjgl.glfw.GLFW;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class SliderComponentProcessor extends ComponentProcessor<AbstractSliderButton> {
|
||||
public class SliderComponentProcessor implements ComponentProcessor {
|
||||
private final Supplier<Boolean> canChangeValueGetter;
|
||||
private final Consumer<Boolean> canChangeValueSetter;
|
||||
|
||||
private final AbstractSliderButton component;
|
||||
|
||||
private static final int SLIDER_CHANGE_DELAY = 1;
|
||||
private int lastSliderChange = SLIDER_CHANGE_DELAY;
|
||||
|
||||
public SliderComponentProcessor(AbstractSliderButton component, Supplier<Boolean> canChangeValueGetter, Consumer<Boolean> canChangeValueSetter) {
|
||||
super(component);
|
||||
this.component = component;
|
||||
this.canChangeValueGetter = canChangeValueGetter;
|
||||
this.canChangeValueSetter = canChangeValueSetter;
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ public class ControlifyConfig {
|
||||
|
||||
for (var controller : Controller.CONTROLLERS.values()) {
|
||||
// `add` replaces if already existing
|
||||
// TODO: find a better way to identify controllers, GUID will report the same for multiple controllers of the same model
|
||||
configCopy.add(controller.guid(), generateControllerConfig(controller));
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,119 @@
|
||||
package dev.isxander.controlify.config.gui;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dev.isxander.controlify.bindings.Bind;
|
||||
import dev.isxander.controlify.compatibility.screen.ScreenProcessor;
|
||||
import dev.isxander.controlify.compatibility.screen.component.ComponentProcessor;
|
||||
import dev.isxander.controlify.compatibility.screen.component.ComponentProcessorProvider;
|
||||
import dev.isxander.controlify.event.ControlifyEvents;
|
||||
import dev.isxander.controlify.gui.ButtonRenderer;
|
||||
import dev.isxander.yacl.api.Controller;
|
||||
import dev.isxander.yacl.api.Option;
|
||||
import dev.isxander.yacl.api.utils.Dimension;
|
||||
import dev.isxander.yacl.gui.AbstractWidget;
|
||||
import dev.isxander.yacl.gui.YACLScreen;
|
||||
import dev.isxander.yacl.gui.controllers.ControllerWidget;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
public class BindButtonController implements Controller<Bind> {
|
||||
private final Option<Bind> option;
|
||||
|
||||
public BindButtonController(Option<Bind> option) {
|
||||
this.option = option;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Option<Bind> option() {
|
||||
return this.option;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component formatValue() {
|
||||
return Component.literal(option().pendingValue().identifier());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractWidget provideWidget(YACLScreen yaclScreen, Dimension<Integer> dimension) {
|
||||
return new BindButtonWidget(this, yaclScreen, dimension);
|
||||
}
|
||||
|
||||
public static class BindButtonWidget extends ControllerWidget<BindButtonController> implements ComponentProcessorProvider, ComponentProcessor {
|
||||
private boolean awaitingControllerInput = false;
|
||||
private final Component awaitingText = Component.translatable("controlify.gui.bind_input_awaiting").withStyle(ChatFormatting.ITALIC);
|
||||
|
||||
public BindButtonWidget(BindButtonController control, YACLScreen screen, Dimension<Integer> dim) {
|
||||
super(control, screen, dim);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawValueText(PoseStack matrices, int mouseX, int mouseY, float delta) {
|
||||
if (awaitingControllerInput) {
|
||||
textRenderer.drawShadow(matrices, awaitingText, getDimension().xLimit() - textRenderer.width(awaitingText) - getXPadding(), getDimension().centerY() - textRenderer.lineHeight / 2f, 0xFFFFFF);
|
||||
} else {
|
||||
ButtonRenderer.drawButton(control.option().pendingValue(), matrices, getDimension().xLimit() - ButtonRenderer.BUTTON_SIZE / 2, getDimension().centerY(), ButtonRenderer.BUTTON_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||
if (isFocused() && keyCode == GLFW.GLFW_KEY_ENTER && !awaitingControllerInput) {
|
||||
awaitingControllerInput = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (getDimension().isPointInside((int)mouseX, (int)mouseY)) {
|
||||
awaitingControllerInput = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComponentProcessor componentProcessor() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean overrideControllerButtons(ScreenProcessor screen, dev.isxander.controlify.controller.Controller controller) {
|
||||
if (!awaitingControllerInput || !isFocused()) return false;
|
||||
|
||||
for (var bind : Bind.values()) {
|
||||
boolean stateNow = bind.state(controller.state(), controller);
|
||||
boolean stateBefore = bind.state(controller.prevState(), controller);
|
||||
if (stateNow && !stateBefore) {
|
||||
control.option().requestSet(bind);
|
||||
awaitingControllerInput = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean overrideControllerNavigation(ScreenProcessor screen, dev.isxander.controlify.controller.Controller controller) {
|
||||
return awaitingControllerInput;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getHoveredControlWidth() {
|
||||
return getUnhoveredControlWidth();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getUnhoveredControlWidth() {
|
||||
if (awaitingControllerInput)
|
||||
return textRenderer.width(awaitingText);
|
||||
|
||||
return ButtonRenderer.BUTTON_SIZE;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package dev.isxander.controlify.config.gui;
|
||||
|
||||
import dev.isxander.controlify.bindings.Bind;
|
||||
import dev.isxander.controlify.config.ControlifyConfig;
|
||||
import dev.isxander.controlify.controller.Controller;
|
||||
import dev.isxander.controlify.controller.ControllerConfig;
|
||||
@ -69,6 +70,17 @@ public class YACLHelper {
|
||||
.build());
|
||||
category.group(configGroup.build());
|
||||
|
||||
var controlsGroup = OptionGroup.createBuilder()
|
||||
.name(Component.translatable("controlify.gui.group.controls"));
|
||||
for (var control : controller.bindings().registry().values()) {
|
||||
controlsGroup.option(Option.createBuilder(Bind.class)
|
||||
.name(control.name())
|
||||
.binding(control.defaultBind(), control::currentBind, control::setCurrentBind)
|
||||
.controller(BindButtonController::new)
|
||||
.build());
|
||||
}
|
||||
category.group(controlsGroup.build());
|
||||
|
||||
yacl.category(category.build());
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,17 @@
|
||||
package dev.isxander.controlify.gui;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import dev.isxander.controlify.bindings.Bind;
|
||||
import net.minecraft.client.gui.GuiComponent;
|
||||
|
||||
public class ButtonRenderer {
|
||||
public static final int BUTTON_SIZE = 22;
|
||||
|
||||
public static void drawButton(Bind button, PoseStack poseStack, int x, int y, int size) {
|
||||
RenderSystem.setShaderTexture(0, button.textureLocation());
|
||||
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
|
||||
|
||||
GuiComponent.blit(poseStack, x - size / 2, y - size / 2, 0, 0, BUTTON_SIZE, BUTTON_SIZE, BUTTON_SIZE, BUTTON_SIZE);
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package dev.isxander.controlify.mixins.compat.screen.vanilla;
|
||||
|
||||
import dev.isxander.controlify.compatibility.screen.component.CustomFocus;
|
||||
import net.minecraft.client.gui.components.events.AbstractContainerEventHandler;
|
||||
import net.minecraft.client.gui.components.events.GuiEventListener;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
@Mixin(AbstractContainerEventHandler.class)
|
||||
public abstract class AbstractContainerEventHandlerMixin implements CustomFocus {
|
||||
@Shadow public abstract @Nullable GuiEventListener getFocused();
|
||||
|
||||
@Override
|
||||
public GuiEventListener getCustomFocus() {
|
||||
return getFocused();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package dev.isxander.controlify.mixins;
|
||||
package dev.isxander.controlify.mixins.compat.screen.vanilla;
|
||||
|
||||
import dev.isxander.controlify.compatibility.screen.component.ComponentProcessor;
|
||||
import dev.isxander.controlify.compatibility.screen.component.ComponentProcessorProvider;
|
||||
@ -23,7 +23,7 @@ public class AbstractSliderButtonMixin implements ComponentProcessorProvider {
|
||||
);
|
||||
|
||||
@Override
|
||||
public ComponentProcessor<AbstractSliderButton> componentProcessor() {
|
||||
public ComponentProcessor componentProcessor() {
|
||||
return controlify$processor;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package dev.isxander.controlify.mixins.compat.screen.vanilla;
|
||||
|
||||
import dev.isxander.controlify.compatibility.screen.component.CustomFocus;
|
||||
import net.minecraft.client.gui.components.ContainerObjectSelectionList;
|
||||
import net.minecraft.client.gui.components.events.GuiEventListener;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
||||
@Mixin(ContainerObjectSelectionList.Entry.class)
|
||||
public abstract class ContainerObjectSelectionListEntryMixin implements CustomFocus {
|
||||
@Shadow public abstract @Nullable GuiEventListener getFocused();
|
||||
|
||||
@Override
|
||||
public GuiEventListener getCustomFocus() {
|
||||
return getFocused();
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package dev.isxander.controlify.mixins;
|
||||
package dev.isxander.controlify.mixins.compat.screen.vanilla;
|
||||
|
||||
import net.minecraft.client.gui.ComponentPath;
|
||||
import net.minecraft.client.gui.navigation.FocusNavigationEvent;
|
@ -1,4 +1,4 @@
|
||||
package dev.isxander.controlify.mixins;
|
||||
package dev.isxander.controlify.mixins.compat.screen.vanilla;
|
||||
|
||||
import dev.isxander.controlify.compatibility.screen.ScreenProcessorProvider;
|
||||
import dev.isxander.controlify.compatibility.screen.ScreenProcessor;
|
@ -1,4 +1,4 @@
|
||||
package dev.isxander.controlify.mixins;
|
||||
package dev.isxander.controlify.mixins.core;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
|
||||
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
|
@ -1,4 +1,4 @@
|
||||
package dev.isxander.controlify.mixins;
|
||||
package dev.isxander.controlify.mixins.core;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.InputMode;
|
@ -1,4 +1,4 @@
|
||||
package dev.isxander.controlify.mixins;
|
||||
package dev.isxander.controlify.mixins.core;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import net.minecraft.client.Minecraft;
|
@ -1,4 +1,4 @@
|
||||
package dev.isxander.controlify.mixins;
|
||||
package dev.isxander.controlify.mixins.core;
|
||||
|
||||
import dev.isxander.controlify.Controlify;
|
||||
import dev.isxander.controlify.InputMode;
|
@ -1,4 +1,4 @@
|
||||
package dev.isxander.controlify.mixins;
|
||||
package dev.isxander.controlify.mixins.feature.bind;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.minecraft.client.KeyMapping;
|
Reference in New Issue
Block a user