Skip to content

Commit

Permalink
Rework screen event (#28)
Browse files Browse the repository at this point in the history
* Use Fabric events instead of mixins

* Better event line

* Better type + event firing handling

* Fix creative inventory firing event for normal one

* Rewrite screen name handling

* Initialize `TYPE_MAP` first

* Use a mixin to support `context.switched`

* `switched` -> `previous_screen_type`

* creative screen is opened from inventory screen

* Shadow `currentScreen`

* Remove TODO

* Add `previous_screen_type` to meta

* Add `from` switch

* Clearer comment

* Use access wideners for `DetailsScreen`

* Clarify screen name generation with comment

* Fix typo

* Move screen name mapping to a separate class

* Move special case handling to the mixin
  • Loading branch information
tal5 committed Sep 4, 2023
1 parent 41eb6d0 commit c5559bf
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 54 deletions.
1 change: 1 addition & 0 deletions build.gradle
Expand Up @@ -50,6 +50,7 @@ loom {
property("devauth.account", "alt") // DevAuth's name for the Microsoft account in the default config
}
}
accessWidenerPath = file("src/main/resources/clientizen.accesswidener")
}


Expand Down
@@ -1,65 +1,56 @@
package com.denizenscript.clientizen.events;

import com.denizenscript.clientizen.util.ScreenNameMapping;
import com.denizenscript.denizencore.events.ScriptEvent;
import com.denizenscript.denizencore.objects.ObjectTag;
import com.denizenscript.denizencore.objects.core.ElementTag;
import net.minecraft.client.gui.screen.GameMenuScreen;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.advancement.AdvancementsScreen;
import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen;
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.gui.screen.option.OptionsScreen;

import java.util.HashMap;
import java.util.Map;

public class ScreenOpenCloseEvent extends ScriptEvent {

// <--[event]
// @Events
// <'screen_type'> screen opened|closed
// screen opened|closed
//
// @Switch type:<screen_type> to only process the event if the type of screen opened matches the specified matcher.
// @Switch from:<screen_type> to only process the event if the screen was opened from a different screen and that screen's type matches the specified matcher.
//
// @Triggers when a screen is opened or closed.
//
// @Context
// <context.screen_type> returns an ElementTag of the screen type that opened.
// <context.switched> returns an ElementTag(Boolean) of whether the screen was opened from another screen.
// <context.screen_type> returns an ElementTag of the screen type that opened/closed.
// <context.previous_screen_type> returns an ElementTag of the screen this screen was opened from, if any.
//
// -->

// TODO: This event needs a partial redo, mainly:
// - CreativeInventoryScreen no longer extends InventoryScreen
// - Add all relevant screen types
// - Potentially add support for handling screens that aren't directly defined via class name

public static ScreenOpenCloseEvent instance;

public static Map<String, Class<?>> TYPE_MAP = new HashMap<>();

static {
TYPE_MAP.put("inventory", InventoryScreen.class);
TYPE_MAP.put("creative", CreativeInventoryScreen.class);
TYPE_MAP.put("pause", GameMenuScreen.class);
TYPE_MAP.put("options", OptionsScreen.class);
TYPE_MAP.put("advancements", AdvancementsScreen.class);
ScreenEvents.AFTER_INIT.register((client, openedScreen, scaledWidth, scaledHeight) -> {
ScreenEvents.remove(openedScreen).register(closedScreen -> ScreenOpenCloseEvent.instance.handleScreenChange(closedScreen, null, false));
});
}

public String type;
public boolean opened;
public boolean switched;
public String previousType;

public ScreenOpenCloseEvent() {
registerCouldMatcher("<'screen_type'> screen opened|closed");
registerCouldMatcher("screen opened|closed");
registerSwitches("type", "from");
instance = this;
}

@Override
public boolean matches(ScriptPath path) {
String screenMatcher = path.eventArgLowerAt(0);
if (!runGenericCheck(screenMatcher, type)) {
if (!runGenericSwitchCheck(path, "type", type)) {
return false;
}
if (opened != path.eventArgLowerAt(2).equals("opened")) {
if (!runGenericSwitchCheck(path, "from", previousType)) {
return false;
}
if (opened != path.eventArgLowerAt(1).equals("opened")) {
return false;
}
return super.matches(path);
Expand All @@ -68,25 +59,31 @@ public boolean matches(ScriptPath path) {
@Override
public ObjectTag getContext(String name) {
return switch (name) {
case "screen_type" -> new ElementTag(type);
case "switched" -> new ElementTag(switched);
case "screen_type" -> new ElementTag(type, true);
case "previous_screen_type" -> previousType != null ? new ElementTag(previousType, true) : null;
default -> super.getContext(name);
};
}


public void handleScreenChange(Screen screen, Screen otherScreen, boolean open) {
for (Map.Entry<String, Class<?>> pair : TYPE_MAP.entrySet()) {
if (pair.getKey().equals("inventory") && screen instanceof CreativeInventoryScreen) {
continue;
}
if (pair.getValue().isInstance(screen)) {
type = pair.getKey();
opened = open;
switched = otherScreen != null;
fire();
return;
}
public void handleScreenChange(Screen screen, Screen previousScreen, boolean open) {
if (!enabled) {
return;
}
type = ScreenNameMapping.getScreenName(screen.getClass());
previousType = previousScreen != null ? ScreenNameMapping.getScreenName(previousScreen.getClass()) : null;
opened = open;
fire();
}

boolean enabled = false;

@Override
public void init() {
enabled = true;
}

@Override
public void destroy() {
enabled = false;
}
}
Expand Up @@ -3,31 +3,42 @@
import com.denizenscript.clientizen.events.ScreenOpenCloseEvent;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen;
import net.minecraft.client.gui.screen.ingame.InventoryScreen;
import net.minecraft.client.network.ClientPlayerInteractionManager;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

@Mixin(MinecraftClient.class)
public class MinecraftClientMixin {
public abstract class MinecraftClientMixin {

@Shadow
@Nullable
public Screen currentScreen;

@Inject(method = "setScreen", at = @At(value = "FIELD", target = "Lnet/minecraft/client/MinecraftClient;currentScreen:Lnet/minecraft/client/gui/screen/Screen;", ordinal = 2))
private void clientizen$screenOpened(Screen screen, CallbackInfo ci) {
if (screen != null) {
ScreenOpenCloseEvent.instance.handleScreenChange(screen, currentScreen, true);
}
}
@Shadow
@Nullable
public ClientPlayerInteractionManager interactionManager;

@Inject(method = "setScreen", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;removed()V"))
private void clientizen$screenClosed(Screen screen, CallbackInfo ci) {
if (screen != null) {
ScreenOpenCloseEvent.instance.handleScreenChange(currentScreen, screen, false);
@Inject(method = "setScreen", at = @At(value = "FIELD", target = "Lnet/minecraft/client/MinecraftClient;currentScreen:Lnet/minecraft/client/gui/screen/Screen;", opcode = Opcodes.PUTFIELD))
private void clientizen$onScreenOpened(Screen screen, CallbackInfo ci){
if (screen == null) {
return;
}
// An InventoryScreen is opened which then opens a creative screen if needed, so ignore an InventoryScreen if a creative one will be opened
if (screen instanceof InventoryScreen && interactionManager.hasCreativeInventory()) {
return;
}
Screen previousScreen = currentScreen;
// Since an inventory screen is opened internally but isn't actually visible, this doesn't count as having a previous screen
if (screen instanceof CreativeInventoryScreen && previousScreen instanceof InventoryScreen) {
previousScreen = null;
}
ScreenOpenCloseEvent.instance.handleScreenChange(screen, previousScreen, true);
}
}
141 changes: 141 additions & 0 deletions src/main/java/com/denizenscript/clientizen/util/ScreenNameMapping.java
@@ -0,0 +1,141 @@
package com.denizenscript.clientizen.util;

import net.minecraft.client.gui.screen.*;
import net.minecraft.client.gui.screen.advancement.AdvancementsScreen;
import net.minecraft.client.gui.screen.ingame.*;
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
import net.minecraft.client.gui.screen.multiplayer.MultiplayerWarningScreen;
import net.minecraft.client.gui.screen.multiplayer.SocialInteractionsScreen;
import net.minecraft.client.gui.screen.option.*;
import net.minecraft.client.gui.screen.pack.ExperimentalWarningScreen;
import net.minecraft.client.gui.screen.pack.PackScreen;
import net.minecraft.client.gui.screen.report.AbuseReportReasonScreen;
import net.minecraft.client.gui.screen.report.ChatReportScreen;
import net.minecraft.client.gui.screen.report.ChatSelectionScreen;
import net.minecraft.client.gui.screen.world.*;

import java.util.HashMap;
import java.util.Map;

public class ScreenNameMapping {

private static final Map<Class<? extends Screen>, String> TYPE_MAP = new HashMap<>();

static {
// Advancement screens
registerScreenName(AdvancementsScreen.class, "advancements");
// In-game screens
registerScreenName(AnvilScreen.class, "anvil");
registerScreenName(BeaconScreen.class, "beacon");
registerScreenName(BlastFurnaceScreen.class, "blast_furnace");
registerScreenName(BookEditScreen.class, "book_edit");
registerScreenName(BookScreen.class, "book");
registerScreenName(BrewingStandScreen.class, "brewing_stand");
registerScreenName(CartographyTableScreen.class, "cartography_table");
registerScreenName(CommandBlockScreen.class, "command_block");
registerScreenName(CraftingScreen.class, "crafting");
registerScreenName(CreativeInventoryScreen.class, "creative");
registerScreenName(EnchantmentScreen.class, "enchantment");
registerScreenName(FurnaceScreen.class, "furnace");
registerScreenName(Generic3x3ContainerScreen.class, "generic_3x3_container");
registerScreenName(GenericContainerScreen.class, "generic_container");
registerScreenName(GrindstoneScreen.class, "grindstone");
registerScreenName(HangingSignEditScreen.class, "hanging_sign_edit");
registerScreenName(HopperScreen.class, "hopper");
registerScreenName(HopperScreen.class, "horse");
registerScreenName(InventoryScreen.class, "inventory");
registerScreenName(JigsawBlockScreen.class, "jigsaw");
registerScreenName(LecternScreen.class, "lectern");
registerScreenName(LoomScreen.class, "loom");
registerScreenName(MerchantScreen.class, "merchant");
registerScreenName(MinecartCommandBlockScreen.class, "command_block_minecart");
registerScreenName(ShulkerBoxScreen.class, "shulker_box");
registerScreenName(SignEditScreen.class, "sign_edit");
registerScreenName(SmithingScreen.class, "smithing");
registerScreenName(SmokerScreen.class, "smoker");
registerScreenName(StonecutterScreen.class, "stonecutter");
registerScreenName(StructureBlockScreen.class, "structure_block");
// Multiplayer screens
registerScreenName(MultiplayerScreen.class, "multiplayer");
registerScreenName(MultiplayerWarningScreen.class, "multiplayer_warning");
registerScreenName(SocialInteractionsScreen.class, "social_interactions");
// Option screens
registerScreenName(AccessibilityOptionsScreen.class, "accessibility_options");
registerScreenName(ChatOptionsScreen.class, "chat_options");
registerScreenName(ControlsOptionsScreen.class, "controls_options");
registerScreenName(CreditsAndAttributionScreen.class, "credits_and_attribution");
registerScreenName(GameOptionsScreen.class, "game_options");
registerScreenName(KeybindsScreen.class, "keybinds");
registerScreenName(LanguageOptionsScreen.class, "language_options");
registerScreenName(MouseOptionsScreen.class, "mouse_options");
registerScreenName(OnlineOptionsScreen.class, "online_options");
registerScreenName(OptionsScreen.class, "options");
registerScreenName(SkinOptionsScreen.class, "skin_options");
registerScreenName(SoundOptionsScreen.class, "sound_options");
registerScreenName(TelemetryInfoScreen.class, "telemetry_info");
registerScreenName(VideoOptionsScreen.class, "video_options");
// Packs screens
registerScreenName(ExperimentalWarningScreen.class, "experimental_warning");
registerScreenName(ExperimentalWarningScreen.DetailsScreen.class, "experimental_warning_details");
registerScreenName(PackScreen.class, "pack");
// Report screens
registerScreenName(AbuseReportReasonScreen.class, "abuse_report_reason");
registerScreenName(ChatReportScreen.class, "chat_report");
registerScreenName(ChatSelectionScreen.class, "chat_selection");
// World screens
registerScreenName(CreateWorldScreen.class, "create_world");
registerScreenName(EditGameRulesScreen.class, "edit_game_rules");
registerScreenName(ExperimentsScreen.class, "experiments");
registerScreenName(OptimizeWorldScreen.class, "optimize_world");
registerScreenName(SelectWorldScreen.class, "select_world");
registerScreenName(SymlinkWarningScreen.class, "symlink_warning");
// Other screens
registerScreenName(AccessibilityOnboardingScreen.class, "accessibility_onboarding");
registerScreenName(AddServerScreen.class, "add_server");
registerScreenName(BackupPromptScreen.class, "backup_prompt");
registerScreenName(ChatScreen.class, "chat");
registerScreenName(ConfirmLinkScreen.class, "confirm_link");
registerScreenName(ConfirmScreen.class, "confirm");
registerScreenName(ConnectScreen.class, "connect");
registerScreenName(CreditsScreen.class, "credits");
registerScreenName(CustomizeBuffetLevelScreen.class, "customize_buffet_level");
registerScreenName(CustomizeFlatLevelScreen.class, "customize_flat_level");
registerScreenName(DatapackFailureScreen.class, "datapack_failure");
registerScreenName(DeathScreen.class, "death");
registerScreenName(DemoScreen.class, "demo");
registerScreenName(DialogScreen.class, "dialog");
registerScreenName(DirectConnectScreen.class, "direct_connect");
registerScreenName(DisconnectedScreen.class, "disconnected");
registerScreenName(DownloadingTerrainScreen.class, "downloading_terrain");
registerScreenName(FatalErrorScreen.class, "fatal_error");
registerScreenName(GameMenuScreen.class, "game_menu");
registerScreenName(GameModeSelectionScreen.class, "game_mode_selection");
registerScreenName(LevelLoadingScreen.class, "level_loading");
registerScreenName(MessageScreen.class, "message");
registerScreenName(NoticeScreen.class, "notice_screen");
registerScreenName(OpenToLanScreen.class, "open_to_lan");
registerScreenName(OutOfMemoryScreen.class, "out_of_memory");
registerScreenName(PresetsScreen.class, "presets");
registerScreenName(ProgressScreen.class, "progress");
registerScreenName(Realms32BitWarningScreen.class, "realms_32_bit_warning");
registerScreenName(SleepingChatScreen.class, "sleeping_chat");
registerScreenName(StatsScreen.class, "stats");
registerScreenName(TaskScreen.class, "task");
registerScreenName(TitleScreen.class, "title");
}

public static void registerScreenName(Class<? extends Screen> screenType, String name) {
TYPE_MAP.put(screenType, name);
}

public static String getScreenName(Class<? extends Screen> screenType) {
return TYPE_MAP.computeIfAbsent(screenType, clazz -> {
// Try dynamically generating the name - currently only for modded screens, as vanilla screens have remapped class names
String className = clazz.getSimpleName();
if (className.endsWith("Screen")) {
className = className.substring(0, className.length() - "Screen".length());
}
return Utilities.camelCaseToSnake(className);
});
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/denizenscript/clientizen/util/Utilities.java
Expand Up @@ -51,4 +51,20 @@ public static MapTag personsToMap(Iterable<Person> persons) {
}
return personsMap;
}

public static String camelCaseToSnake(String camelCase) {
StringBuilder snakeCaseBuilder = new StringBuilder(camelCase.length());
snakeCaseBuilder.append(Character.toLowerCase(camelCase.charAt(0)));
for (int i = 1; i < camelCase.length(); i++) {
char character = camelCase.charAt(i);
if (Character.isUpperCase(character)) {
snakeCaseBuilder.append('_');
snakeCaseBuilder.append(Character.toLowerCase(character));
}
else {
snakeCaseBuilder.append(character);
}
}
return snakeCaseBuilder.toString();
}
}
3 changes: 3 additions & 0 deletions src/main/resources/clientizen.accesswidener
@@ -0,0 +1,3 @@
accessWidener v2 named

accessible class net/minecraft/client/gui/screen/pack/ExperimentalWarningScreen$DetailsScreen
1 change: 1 addition & 0 deletions src/main/resources/fabric.mod.json
Expand Up @@ -24,6 +24,7 @@
"mixins": [
"clientizen.mixins.json"
],
"accessWidener": "clientizen.accesswidener",
"depends": {
"fabricloader": ">=0.14.9",
"fabric-api": "*",
Expand Down

0 comments on commit c5559bf

Please sign in to comment.