From 3f6ddc43e5e04400d36ac48eecfbde3c73f2f2ef Mon Sep 17 00:00:00 2001 From: replaceitem <40722305+replaceitem@users.noreply.github.com> Date: Wed, 12 Jan 2022 22:52:38 +0100 Subject: [PATCH] Added screen handler system (#888) * Added screen handler value * Added screen handler constructor and function to show to player * Made inventory_* functions work with screen handlers * Event callbacks and some refactoring * Exception if screen has no inventory * Fixed NPE * Close screen when null * Added screen_property * UNKNOWN_SCREEN throwable * Fix error when not specifying click callback * Access to non-container screen handler slots via the inventory_* functions * Change lectern page * Added button event * Added 'close' event and refactoring callback * Beacon and Brewing screens * Cartography table screen * Crafting screen * Enchantment screen * Grindstone * Loom * Merchant * Shulkerbox * Smithing table * Stonecutter * Use strings as screen type instead of ScreenHandlerType * Remove support for multiple players * Combining create_screen and open_screen and refactoring * Allow modifying cursor stack * Cleanup * Docs * Added multiple screen examples * Rename to screen * Added smoker and blast furnace * Serialize screen content * Store host name instead of context * Add button value special cases to docs * Spelling and grammar fixes by @James103 * Optimize screen inventory access * Use -1 for slot click outside the screen * As boolean, ScreenValue equals isOpen * Revert "Use -1 for slot click outside the screen" This reverts commit 68c113e71bb653191cedf4cc356ab7705838dc25. * Rework of callback function * Improve screen functionality with drop_item * Editor keywords * Only check syncId * slot_update event callback * Screem typo * Mistake * toTag returns "null" NBT for closed screen * Use null for slot index of -999 Co-authored-by: replaceitem --- docs/scarpet/api/Inventories.md | 269 ++++++++++ docs/scarpet/resources/editors/idea/2.txt | 3 + .../scarpet/resources/editors/npp/scarpet.xml | 2 + .../carpet/fakes/ScreenHandlerInterface.java | 12 + ...tractRecipeScreenHandler_scarpetMixin.java | 19 + .../ScreenHandlerSubclasses_scarpetMixin.java | 29 ++ .../mixins/ScreenHandler_scarpetMixin.java | 79 +++ .../java/carpet/script/api/Inventories.java | 50 +- .../carpet/script/exception/Throwables.java | 1 + .../script/value/NBTSerializableValue.java | 7 +- .../java/carpet/script/value/ScreenValue.java | 461 ++++++++++++++++++ src/main/resources/carpet.mixins.json | 4 + 12 files changed, 931 insertions(+), 5 deletions(-) create mode 100644 src/main/java/carpet/fakes/ScreenHandlerInterface.java create mode 100644 src/main/java/carpet/mixins/AbstractRecipeScreenHandler_scarpetMixin.java create mode 100644 src/main/java/carpet/mixins/ScreenHandlerSubclasses_scarpetMixin.java create mode 100644 src/main/java/carpet/mixins/ScreenHandler_scarpetMixin.java create mode 100644 src/main/java/carpet/script/value/ScreenValue.java diff --git a/docs/scarpet/api/Inventories.md b/docs/scarpet/api/Inventories.md index b77c26e6..fc8a93ee 100644 --- a/docs/scarpet/api/Inventories.md +++ b/docs/scarpet/api/Inventories.md @@ -206,3 +206,272 @@ Returns size of the actual dropped items. inventory_drop(player(), 0, 1) => 1 // Q's one item on the ground inventory_drop(x,y,z, 0) => 64 // removed and spawned in the world a full stack of items + +## Screens + +A screen is a value type used to open screens for a player and interact with them. +For example, this includes the chest inventory gui, the crafting table gui and many more. + +### `create_screen(player, type, name, callback?)` + +Creates and opens a screen for a `player`. + +Available `type`s: + +* `anvil` +* `beacon` +* `blast_furnace` +* `brewing_stand` +* `cartography_table` +* `crafting` +* `enchantment` +* `furnace` +* `generic_3x3` +* `generic_9x1` +* `generic_9x2` +* `generic_9x3` +* `generic_9x4` +* `generic_9x5` +* `generic_9x6` +* `grindstone` +* `hopper` +* `lectern` +* `loom` +* `merchant` +* `shulker_box` +* `smithing` +* `smoker` +* `stonecutter` + +The `name` parameter can be a formatted text and will be displayed at the top of the screen. +Some screens like the lectern or beacon screen don't show it. + +Optionally, a `callback` function can be passed as the fourth argument. +This functions needs to have four parameters: +`_(screen, player, action, data) -> ...` + +The `screen` parameter is the screen value of the screen itself. +`player` is the player who interacted with the screen. +`action` is a string corresponding to the interaction type. +Can be any of the following: + +Slot interactions: + +* `pickup` +* `quick_move` +* `swap` +* `clone` +* `throw` +* `quick_craft` +* `pickup_all` + +The `data` for this interaction is a map, with a `slot` and `button` value. +`slot` is the slot index of the slot that was clicked. +When holding an item in the cursor stack and clicking inside the screen, +but not in a slot, this is -1. +If clicked outside the screen (where it would drop held items), this value is null. +The `button` is the mouse button used to click the slot. + +For the `swap` action, the `button` is the number key 0-8 for a certain hotbar slot. + +For the `quick_craft` action, the `data` also contains the `quick_craft_stage`, +which is either 0 (beginning of quick crafting), 1 (adding item to slot) or 2 (end of quick crafting). + +Other interactions: + +* `button` Pressing a button in certain screens that have button elements (enchantment table, lectern, loom and stonecutter) +The `data` provides a `button`, which is the index of the button that was pressed. +Note that for lecterns, this index can be certain a value above 100, for jumping to a certain page. +This can come from formatted text inside the book, with a `change_page` click event action. + +* `close` Triggers when the screen gets closed. No `data` provided. + +* `select_recipe` When clicking on a recipe in the recipe book. +`data` contains a `recipe`, which is the identifier of the clicked recipe, +as well as `craft_all`, which is a boolean specifying whether +shift was pressed when selecting the recipe. + +* `slot_update` Gets called **after** a slot has changed contents. `data` provides a `slot` and `stack`. + +By returning a string `'cancel'` in the callback function, +the screen interaction can be cancelled. +This doesn't work for the `close` action. + +The `create_screen` function returns a `screen` value, +which can be used in all inventory related functions to access the screens' slots. +The screen inventory covers all slots in the screen and the player inventory. +The last slot is the cursor stack of the screen, +meaning that using `-1` can be used to modify the stack the players' cursor is holding. + +### `close_screen(screen)` + +Closes the screen of the given screen value. +Returns `true` if the screen was closed. +If the screen is already closed, returns `false`. + +### `screen_property(screen, property)` + +### `screen_property(screen, property, value)` + +Queries or modifies a certain `property` of a `screen`. +The `property` is a string with the name of the property. +When called with `screen` and `property` parameter, returns the current value of the property. +When specifying a `value`, +the property will be assigned the new `value` and synced with the client. + +**Options for `property` string:** + +| `property` | Required screen type | Type | Description | +|---|---|---|---| +| `name` | **All** | text | The name of the screen, as specified in the `create_screen()` function. Can only be queried. | +| `open` | **All** | boolean | Returns `true` if the screen is open, `false` otherwise. Can only be queried. | +| `fuel_progress` | furnace/smoker/blast_furnace | number | Current value of the fuel indicator. | +| `max_fuel_progress` | furnace/smoker/blast_furnace | number | Maximum value for the full fuel indicator. | +| `cook_progress` | furnace/smoker/blast_furnace | number | Cooking progress indicator value. | +| `max_cook_progress` | furnace/smoker/blast_furnace | number | Maximum value for the cooking progress indicator. | +| `level_cost` | anvil | number | Displayed level cost for the anvil. | +| `page` | lectern | number | Opened page in the lectern screen. | +| `beacon_level` | beacon | number | The power level of the beacon screen. This affects how many effects under primary power are grayed out. Should be a value between 0-5. | +| `primary_effect` | beacon | number | The effect id of the primary effect. This changes the effect icon on the button on the secondary power side next to the regeneration effect. | +| `secondary_effect` | beacon | number | The effect id of the secondary effect. This seems to change nothing, but it exists. | +| `brew_time` | brewing_stand | number | The brewing time indicator value. This goes from 0 to 400. | +| `brewing_fuel` | brewing_stand | number | The fuel indicator progress. Values range between 0 to 20. | +| `enchantment_power_x` | enchantment | number | The level cost of the shown enchantment. Replace `x` with 1, 2 or 3 (e.g. `enchantment_power_2`) to target the first, second or third enchantment. | +| `enchantment_id_x` | enchantment | number | The id of the enchantment shown (replace `x` with the enchantment slot 1/2/3). | +| `enchantment_level_x` | enchantment | number | The enchantment level of the enchantment. | +| `enchantment_seed` | enchantment | number | The seed of the enchanting screen. This affects the text shown in the standard Galactic alphabet. | +| `banner_pattern` | loom | number | The selected banner pattern inside the loom. | +| `stonecutter_recipe` | stonecutter | number | The selected recipe in the stonecutter. | + +### Screen example scripts + +
+Chest click event + +```py +__command() -> ( + create_screen(player(),'generic_9x6',format('db Test'),_(screen, player, action, data) -> ( + print(player('all'),str('%s\n%s\n%s',player,action,data)); //for testing + if(action=='pickup', + inventory_set(screen,data:'slot',1,if(inventory_get(screen,data:'slot'),'air','red_stained_glass_pane')); + ); + 'cancel' + )); +); +``` +
+ +
+Anvil text prompt + +```py +// anvil text prompt gui +__command() -> ( + global_screen = create_screen(player(),'anvil',format('r Enter a text'),_(screen, player, action, data)->( + if(action == 'pickup' && data:'slot' == 2, + renamed_item = inventory_get(screen,2); + nbt = renamed_item:2; + name = parse_nbt(nbt:'display':'Name'):'text'; + if(!name, return('cancel')); //don't accept empty string + print(player,'Text: ' + name); + close_screen(screen); + ); + 'cancel' + )); + inventory_set(global_screen,0,1,'paper','{display:{Name:\'{"text":""}\'}}'); +); + +``` +
+ +
+Lectern flip book + +```py +// flip book lectern + +global_fac = 256/60; +curve(v) -> ( + v = v%360; + if(v<60,v*global_fac,v<180,255,v<240,255-(v-180)*global_fac,0); +); + +hex_from_hue(hue) -> str('#%02X%02X%02X',curve(hue+120),curve(hue),curve(hue+240)); + +make_char(hue) -> str('{"text":"▉","color":"%s"}',hex_from_hue(hue)); + +make_page(hue) -> ( + page = '['; + loop(15, //row + y = _; + loop(14, //col + x = _; + page += make_char(hue+x*4+y*4) + ','; + ); + ); + return(slice(page,0,-2)+']'); +); + + +__command() -> ( + screen = create_screen(player(),'lectern','Lectern example (this text is not visible)',_(screen, player, action, data)->( + if(action=='button', + print(player,'Button: ' + data:'button'); + ); + 'cancel' + )); + + page_count = 60; + pages = []; + + loop(page_count, + hue = _/page_count*360; + pages += make_page(hue); + ); + + nbt = encode_nbt({ + 'pages'-> pages, + 'author'->'-', + 'title'->'-', + 'resolved'->1 + }); + + inventory_set(screen,0,1,'written_book',nbt); + + task(_(outer(screen),outer(page_count))->( + while(screen != null && screen_property(screen,'open'),100000, + p = (p+1)%page_count; + screen_property(screen,'page',p); + sleep(50); + ); + )); +); + +``` +
+ +
+generic_3x3 cursor stack + +```py +__command() -> ( + screen = create_screen(player(),'generic_3x3','Title',_(screen, player, action, data) -> ( + if(action=='pickup', + // set slot to the cursor stack item + inventory_set(screen,data:'slot',1,inventory_get(screen,-1):0); + ); + 'cancel' + )); + + task(_(outer(screen))->( + // keep the cursor stack item blinking + while(screen_property(screen,'open'),100000, + inventory_set(screen,-1,1,'red_concrete'); + sleep(500); + inventory_set(screen,-1,1,'lime_concrete'); + sleep(500); + ); + )); +); +``` +
\ No newline at end of file diff --git a/docs/scarpet/resources/editors/idea/2.txt b/docs/scarpet/resources/editors/idea/2.txt index 99bb3b12..5e0a53fa 100644 --- a/docs/scarpet/resources/editors/idea/2.txt +++ b/docs/scarpet/resources/editors/idea/2.txt @@ -15,11 +15,13 @@ blocks_movement bossbar brightness chunk_tickets +close_screen convert_date crafting_remaining_item create_datapack create_explosion create_marker +create_screen current_dimension day_time delete_file @@ -100,6 +102,7 @@ scoreboard_add scoreboard_remove scoreboard_display scoreboard_property +screen_property tag_matches team_add team_leave diff --git a/docs/scarpet/resources/editors/npp/scarpet.xml b/docs/scarpet/resources/editors/npp/scarpet.xml index 1a1138c6..1d5b0f69 100644 --- a/docs/scarpet/resources/editors/npp/scarpet.xml +++ b/docs/scarpet/resources/editors/npp/scarpet.xml @@ -71,6 +71,8 @@ team_add team_list team_remove team_leave team_property + create_screen close_screen screen_property + bossbar player spawn entity_id entity_list entity_area entity_selector query modify entity_event entity_load_handler entity_types diff --git a/src/main/java/carpet/fakes/ScreenHandlerInterface.java b/src/main/java/carpet/fakes/ScreenHandlerInterface.java new file mode 100644 index 00000000..241da3b9 --- /dev/null +++ b/src/main/java/carpet/fakes/ScreenHandlerInterface.java @@ -0,0 +1,12 @@ +package carpet.fakes; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.recipe.Recipe; +import net.minecraft.screen.Property; +import net.minecraft.server.network.ServerPlayerEntity; + +public interface ScreenHandlerInterface { + Property getProperty(int index); + boolean callButtonClickListener(int button, PlayerEntity player); + boolean callSelectRecipeListener(ServerPlayerEntity player, Recipe recipe, boolean craftAll); +} diff --git a/src/main/java/carpet/mixins/AbstractRecipeScreenHandler_scarpetMixin.java b/src/main/java/carpet/mixins/AbstractRecipeScreenHandler_scarpetMixin.java new file mode 100644 index 00000000..151c272b --- /dev/null +++ b/src/main/java/carpet/mixins/AbstractRecipeScreenHandler_scarpetMixin.java @@ -0,0 +1,19 @@ +package carpet.mixins; + +import carpet.fakes.ScreenHandlerInterface; +import net.minecraft.recipe.Recipe; +import net.minecraft.screen.AbstractRecipeScreenHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(AbstractRecipeScreenHandler.class) +public class AbstractRecipeScreenHandler_scarpetMixin { + @Inject(method = "fillInputSlots",at = @At("HEAD"), cancellable = true) + private void selectRecipeCallback(boolean craftAll, Recipe recipe, ServerPlayerEntity player, CallbackInfo ci) { + if(((ScreenHandlerInterface) this).callSelectRecipeListener(player,recipe,craftAll)) + ci.cancel(); + } +} diff --git a/src/main/java/carpet/mixins/ScreenHandlerSubclasses_scarpetMixin.java b/src/main/java/carpet/mixins/ScreenHandlerSubclasses_scarpetMixin.java new file mode 100644 index 00000000..b1227cec --- /dev/null +++ b/src/main/java/carpet/mixins/ScreenHandlerSubclasses_scarpetMixin.java @@ -0,0 +1,29 @@ +package carpet.mixins; + +import carpet.fakes.ScreenHandlerInterface; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.screen.EnchantmentScreenHandler; +import net.minecraft.screen.LecternScreenHandler; +import net.minecraft.screen.LoomScreenHandler; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.screen.StonecutterScreenHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + + +//classes that override onButtonClick +@Mixin({EnchantmentScreenHandler.class, LecternScreenHandler.class, LoomScreenHandler.class, StonecutterScreenHandler.class}) +public abstract class ScreenHandlerSubclasses_scarpetMixin extends ScreenHandler { + protected ScreenHandlerSubclasses_scarpetMixin(ScreenHandlerType type, int syncId) { + super(type, syncId); + } + + @Inject(method = "onButtonClick", at = @At("HEAD"), cancellable = true) + private void buttonClickCallback(PlayerEntity player, int id, CallbackInfoReturnable cir) { + if(((ScreenHandlerInterface) this).callButtonClickListener(id,player)) + cir.cancel(); + } +} diff --git a/src/main/java/carpet/mixins/ScreenHandler_scarpetMixin.java b/src/main/java/carpet/mixins/ScreenHandler_scarpetMixin.java new file mode 100644 index 00000000..e56515a1 --- /dev/null +++ b/src/main/java/carpet/mixins/ScreenHandler_scarpetMixin.java @@ -0,0 +1,79 @@ +package carpet.mixins; + + +import carpet.fakes.ScreenHandlerInterface; +import carpet.script.value.ScreenValue; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.recipe.Recipe; +import net.minecraft.screen.Property; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerListener; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Final; +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; + +import java.util.List; + +@Mixin(ScreenHandler.class) +public abstract class ScreenHandler_scarpetMixin implements ScreenHandlerInterface +{ + @Shadow @Final private List listeners; + @Shadow public abstract void syncState(); + @Shadow @Final private List properties; + + @Inject(method = "internalOnSlotClick", at = @At("HEAD"), cancellable = true) + private void callSlotClickListener(int slotIndex, int button, SlotActionType actionType, PlayerEntity player, CallbackInfo ci) { + if(!(player instanceof ServerPlayerEntity serverPlayerEntity)) return; + for(ScreenHandlerListener screenHandlerListener : this.listeners) { + if(screenHandlerListener instanceof ScreenValue.ScarpetScreenHandlerListener scarpetScreenHandlerListener) { + if(scarpetScreenHandlerListener.onSlotClick(serverPlayerEntity, actionType, slotIndex, button)) { + ci.cancel(); + syncState(); + } + } + } + } + + @Inject(method = "close", at = @At("HEAD"), cancellable = true) + private void callCloseListener(PlayerEntity player, CallbackInfo ci) { + if(!(player instanceof ServerPlayerEntity serverPlayerEntity)) return; + for(ScreenHandlerListener screenHandlerListener : this.listeners) { + if(screenHandlerListener instanceof ScreenValue.ScarpetScreenHandlerListener scarpetScreenHandlerListener) { + scarpetScreenHandlerListener.onClose(serverPlayerEntity); + } + } + } + + @Override + public boolean callButtonClickListener(int button, PlayerEntity player) { + if(!(player instanceof ServerPlayerEntity serverPlayerEntity)) return false; + for(ScreenHandlerListener screenHandlerListener : listeners) { + if(screenHandlerListener instanceof ScreenValue.ScarpetScreenHandlerListener scarpetScreenHandlerListener) { + if(scarpetScreenHandlerListener.onButtonClick(serverPlayerEntity, button)) + return true; + } + } + return false; + } + + @Override + public boolean callSelectRecipeListener(ServerPlayerEntity player, Recipe recipe, boolean craftAll) { + for(ScreenHandlerListener screenHandlerListener : listeners) { + if(screenHandlerListener instanceof ScreenValue.ScarpetScreenHandlerListener scarpetScreenHandlerListener) { + if(scarpetScreenHandlerListener.onSelectRecipe(player, recipe, craftAll)) + return true; + } + } + return false; + } + + @Override + public Property getProperty(int index) { + return this.properties.get(index); + } +} \ No newline at end of file diff --git a/src/main/java/carpet/script/api/Inventories.java b/src/main/java/carpet/script/api/Inventories.java index e7ccb2a8..cfe700cd 100644 --- a/src/main/java/carpet/script/api/Inventories.java +++ b/src/main/java/carpet/script/api/Inventories.java @@ -4,15 +4,20 @@ import carpet.fakes.IngredientInterface; import carpet.fakes.RecipeManagerInterface; import carpet.script.CarpetContext; import carpet.script.Expression; +import carpet.script.argument.FunctionArgument; import carpet.script.exception.InternalExpressionException; import carpet.script.exception.ThrowStatement; import carpet.script.exception.Throwables; import carpet.script.utils.InputValidator; import carpet.script.value.BooleanValue; +import carpet.script.value.EntityValue; +import carpet.script.value.FormattedTextValue; +import carpet.script.value.FunctionValue; import carpet.script.value.ListValue; import carpet.script.value.NBTSerializableValue; import carpet.script.value.NullValue; import carpet.script.value.NumericValue; +import carpet.script.value.ScreenValue; import carpet.script.value.StringValue; import carpet.script.value.Value; import carpet.script.value.ValueConversions; @@ -25,7 +30,6 @@ import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.Item; import net.minecraft.item.ItemGroup; import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; import net.minecraft.nbt.NbtCompound; import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; import net.minecraft.recipe.AbstractCookingRecipe; @@ -36,10 +40,9 @@ import net.minecraft.recipe.ShapedRecipe; import net.minecraft.recipe.ShapelessRecipe; import net.minecraft.recipe.SpecialCraftingRecipe; import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.tag.Tag; import net.minecraft.tag.TagManager; +import net.minecraft.text.Text; import net.minecraft.util.Identifier; -import net.minecraft.util.InvalidIdentifierException; import net.minecraft.util.math.Vec3d; import net.minecraft.util.registry.Registry; @@ -381,11 +384,50 @@ public class Inventories { } return new NumericValue(item.getStack().getCount()); }); + + expression.addContextFunction("create_screen",-1, (c, t, lv) -> + { + if(lv.size() < 3) throw new InternalExpressionException("'create_screen' requires at least three arguments"); + Value playerValue = lv.get(0); + ServerPlayerEntity player = EntityValue.getPlayerByValue(((CarpetContext) c).s.getServer(), playerValue); + if(player == null) throw new InternalExpressionException("'create_screen' requires a valid online player as the first argument."); + String type = lv.get(1).getString(); + Text name = FormattedTextValue.getTextByValue(lv.get(2)); + FunctionValue function = null; + if(lv.size() > 3) + function = FunctionArgument.findIn(c, expression.module, lv, 3, true, false).function; + + return new ScreenValue(player,type,name,function,c); + }); + + expression.addContextFunction("close_screen",1, (c, t, lv) -> + { + Value value = lv.get(0); + if(!(value instanceof ScreenValue screenValue)) throw new InternalExpressionException("'close_screen' requires a screen value as the first argument."); + if(!screenValue.isOpen()) return Value.FALSE; + screenValue.close(); + return Value.TRUE; + }); + + expression.addContextFunction("screen_property",-1, (c, t, lv) -> + { + if(lv.size()<2) throw new InternalExpressionException("'screen_property' requires at least a screen and a property name"); + if(!(lv.get(0) instanceof ScreenValue screenValue)) throw new InternalExpressionException("'screen_property' requires a screen value as the first argument"); + String propertyName = lv.get(1).getString(); + if(lv.size()>=3) + { + return screenValue.modifyProperty(propertyName,lv.subList(2,lv.size())); + } + else + { + return screenValue.queryProperty(propertyName); + } + }); } private static void syncPlayerInventory(NBTSerializableValue.InventoryLocator inventory, int int_1) { - if (inventory.owner() instanceof ServerPlayerEntity player && !inventory.isEnder()) + if (inventory.owner() instanceof ServerPlayerEntity player && !inventory.isEnder() && !(inventory.inventory() instanceof ScreenValue.ScreenHandlerInventory)) { player.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket( -2, 0, // resolve mystery argument diff --git a/src/main/java/carpet/script/exception/Throwables.java b/src/main/java/carpet/script/exception/Throwables.java index fa751b2d..a0a85cb1 100644 --- a/src/main/java/carpet/script/exception/Throwables.java +++ b/src/main/java/carpet/script/exception/Throwables.java @@ -24,6 +24,7 @@ public class Throwables { public static final Throwables UNKNOWN_DIMENSION = register("unknown_dimension", VALUE_EXCEPTION); public static final Throwables UNKNOWN_STRUCTURE = register("unknown_structure", VALUE_EXCEPTION); public static final Throwables UNKNOWN_CRITERION = register("unknown_criterion", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_SCREEN = register("unknown_screen", VALUE_EXCEPTION); public static final Throwables IO_EXCEPTION = register("io_exception", THROWN_EXCEPTION_TYPE); public static final Throwables NBT_ERROR = register("nbt_error", IO_EXCEPTION); public static final Throwables JSON_ERROR = register("json_error", IO_EXCEPTION); diff --git a/src/main/java/carpet/script/value/NBTSerializableValue.java b/src/main/java/carpet/script/value/NBTSerializableValue.java index 650a8f50..15345223 100644 --- a/src/main/java/carpet/script/value/NBTSerializableValue.java +++ b/src/main/java/carpet/script/value/NBTSerializableValue.java @@ -266,6 +266,11 @@ public class NBTSerializableValue extends Value implements ContainerValueInterfa return null; return new InventoryLocator(pos, pos, inv, offset+1); } + if (v1 instanceof ScreenValue screenValue) + { + if(!screenValue.isOpen()) return null; + return new InventoryLocator(screenValue.getPlayer(), screenValue.getPlayer().getBlockPos(), screenValue.getInventory(), offset+1); + } BlockPos pos = new BlockPos( NumericValue.asNumber(v1).getDouble(), NumericValue.asNumber(params.get(1 + offset)).getDouble(), @@ -277,7 +282,7 @@ public class NBTSerializableValue extends Value implements ContainerValueInterfa } catch (IndexOutOfBoundsException e) { - throw new InternalExpressionException("Inventory should be defined either by three coordinates, a block value, or an entity"); + throw new InternalExpressionException("Inventory should be defined either by three coordinates, a block value, an entity, or a screen"); } } diff --git a/src/main/java/carpet/script/value/ScreenValue.java b/src/main/java/carpet/script/value/ScreenValue.java new file mode 100644 index 00000000..592b6609 --- /dev/null +++ b/src/main/java/carpet/script/value/ScreenValue.java @@ -0,0 +1,461 @@ +package carpet.script.value; + +import carpet.CarpetServer; +import carpet.CarpetSettings; +import carpet.fakes.ScreenHandlerInterface; + +import carpet.script.CarpetContext; +import carpet.script.CarpetScriptHost; +import carpet.script.CarpetScriptServer; +import carpet.script.Context; +import carpet.script.exception.IntegrityException; +import carpet.script.exception.InternalExpressionException; +import carpet.script.exception.InvalidCallbackException; +import carpet.script.exception.ThrowStatement; +import carpet.script.exception.Throwables; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.inventory.Inventory; +import net.minecraft.inventory.SimpleInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.nbt.NbtList; +import net.minecraft.recipe.Recipe; +import net.minecraft.screen.AbstractFurnaceScreenHandler; +import net.minecraft.screen.AnvilScreenHandler; +import net.minecraft.screen.ArrayPropertyDelegate; +import net.minecraft.screen.BeaconScreenHandler; +import net.minecraft.screen.BlastFurnaceScreenHandler; +import net.minecraft.screen.BrewingStandScreenHandler; +import net.minecraft.screen.CartographyTableScreenHandler; +import net.minecraft.screen.CraftingScreenHandler; +import net.minecraft.screen.EnchantmentScreenHandler; +import net.minecraft.screen.FurnaceScreenHandler; +import net.minecraft.screen.GenericContainerScreenHandler; +import net.minecraft.screen.GrindstoneScreenHandler; +import net.minecraft.screen.HopperScreenHandler; +import net.minecraft.screen.LecternScreenHandler; +import net.minecraft.screen.LoomScreenHandler; +import net.minecraft.screen.MerchantScreenHandler; +import net.minecraft.screen.NamedScreenHandlerFactory; +import net.minecraft.screen.Property; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.ScreenHandlerListener; +import net.minecraft.screen.ShulkerBoxScreenHandler; +import net.minecraft.screen.SimpleNamedScreenHandlerFactory; +import net.minecraft.screen.SmithingScreenHandler; +import net.minecraft.screen.SmokerScreenHandler; +import net.minecraft.screen.StonecutterScreenHandler; +import net.minecraft.screen.slot.Slot; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.Text; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +import static net.minecraft.screen.ScreenHandlerType.*; + +public class ScreenValue extends Value { + private ScreenHandler screenHandler; + private ScreenHandlerInventory inventory; + + private final Text name; + private final String typestring; + private final FunctionValue callback; + private final String hostname; + private final ServerPlayerEntity player; + + + public static Map screenHandlerFactories; + + static + { + screenHandlerFactories = new HashMap<>(); + + screenHandlerFactories.put("anvil",(syncId, playerInventory) -> new AnvilScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("beacon",(syncId, playerInventory) -> new BeaconScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("blast_furnace",(syncId, playerInventory) -> new BlastFurnaceScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("brewing_stand",(syncId, playerInventory) -> new BrewingStandScreenHandler(syncId,playerInventory,new SimpleInventory(5),new ArrayPropertyDelegate(2))); + screenHandlerFactories.put("cartography_table",(syncId, playerInventory) -> new CartographyTableScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("crafting",(syncId, playerInventory) -> new CraftingScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("enchantment",(syncId, playerInventory) -> new EnchantmentScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("furnace",(syncId, playerInventory) -> new FurnaceScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("generic_3x3",((syncId, playerInventory) -> new GenericContainerScreenHandler(GENERIC_3X3,syncId,playerInventory,new SimpleInventory(9),1))); + screenHandlerFactories.put("generic_9x1",((syncId, playerInventory) -> new GenericContainerScreenHandler(GENERIC_9X1,syncId,playerInventory,new SimpleInventory(9),1))); + screenHandlerFactories.put("generic_9x2",((syncId, playerInventory) -> new GenericContainerScreenHandler(GENERIC_9X2,syncId,playerInventory,new SimpleInventory(9*2),2))); + screenHandlerFactories.put("generic_9x3",((syncId, playerInventory) -> new GenericContainerScreenHandler(GENERIC_9X3,syncId,playerInventory,new SimpleInventory(9*3),3))); + screenHandlerFactories.put("generic_9x4",((syncId, playerInventory) -> new GenericContainerScreenHandler(GENERIC_9X4,syncId,playerInventory,new SimpleInventory(9*4),4))); + screenHandlerFactories.put("generic_9x5",((syncId, playerInventory) -> new GenericContainerScreenHandler(GENERIC_9X5,syncId,playerInventory,new SimpleInventory(9*5),5))); + screenHandlerFactories.put("generic_9x6",((syncId, playerInventory) -> new GenericContainerScreenHandler(GENERIC_9X6,syncId,playerInventory,new SimpleInventory(9*6),6))); + screenHandlerFactories.put("grindstone",(syncId, playerInventory) -> new GrindstoneScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("hopper",(syncId, playerInventory) -> new HopperScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("lectern",(syncId, playerInventory) -> new LecternScreenHandler(syncId,new SimpleInventory(1),new ArrayPropertyDelegate(1))); + screenHandlerFactories.put("loom",(syncId, playerInventory) -> new LoomScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("merchant",(syncId, playerInventory) -> new MerchantScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("shulker_box",(syncId, playerInventory) -> new ShulkerBoxScreenHandler(syncId,playerInventory,new SimpleInventory(9*3))); + screenHandlerFactories.put("smithing",(syncId, playerInventory) -> new SmithingScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("smoker",(syncId, playerInventory) -> new SmokerScreenHandler(syncId,playerInventory)); + screenHandlerFactories.put("stonecutter",(syncId, playerInventory) -> new StonecutterScreenHandler(syncId,playerInventory)); + } + + + protected interface ScarpetScreenHandlerFactory { + ScreenHandler create(int syncId, PlayerInventory playerInventory); + } + + + + + public ScreenValue(ServerPlayerEntity player, String type, Text name, FunctionValue callback, Context c) { + this.name = name; + this.typestring = type.toLowerCase(); + if(callback != null) callback.checkArgs(4); + this.callback = callback; + this.hostname = c.host.getName(); + this.player = player; + NamedScreenHandlerFactory factory = this.createScreenHandlerFactory(); + if(factory == null) throw new ThrowStatement(type, Throwables.UNKNOWN_SCREEN); + this.openScreen(factory); + this.inventory = new ScreenHandlerInventory(this.screenHandler); + } + + private NamedScreenHandlerFactory createScreenHandlerFactory() { + if(!screenHandlerFactories.containsKey(this.typestring)) { + return null; + } + + return new SimpleNamedScreenHandlerFactory((i, playerInventory, playerEntity) -> { + ScreenHandler screen = screenHandlerFactories.get(ScreenValue.this.typestring).create(i,playerInventory); + ScreenValue.this.addListenerCallback(screen); + ScreenValue.this.screenHandler = screen; + return screen; + }, this.name); + } + + private void openScreen(NamedScreenHandlerFactory factory) { + if(this.player == null) return; + OptionalInt optionalSyncId = this.player.openHandledScreen(factory); + if(optionalSyncId.isPresent() && this.player.currentScreenHandler.syncId == optionalSyncId.getAsInt()) { + this.screenHandler = this.player.currentScreenHandler; + } + } + + public void close() { + if(this.player.currentScreenHandler != this.player.playerScreenHandler) { + //prevent recursion when closing screen in closing screen callback by doing this before triggering event + this.inventory = null; + this.player.currentScreenHandler = this.player.playerScreenHandler; + this.player.closeHandledScreen(); + this.screenHandler = null; + } + } + + public boolean isOpen() { + if(this.screenHandler == null) { + return false; + } + if(this.player.currentScreenHandler.syncId == this.screenHandler.syncId) { + return true; + } + this.screenHandler = null; + return false; + } + + + private boolean callListener(ServerPlayerEntity player, String action, Map data) { + Value playerValue = EntityValue.of(player); + Value actionValue = StringValue.of(action); + Value dataValue = MapValue.wrap(data); + List args = Arrays.asList(this,playerValue,actionValue,dataValue); + CarpetScriptHost appHost = CarpetServer.scriptServer.getAppHostByName(this.hostname); + if(appHost == null) { + this.close(); + this.screenHandler = null; + return false; + } + ServerCommandSource source = player.getCommandSource().withLevel(CarpetSettings.runPermissionLevel); + CarpetScriptHost executingHost = appHost.retrieveForExecution(source,player); + try + { + Value cancelValue = executingHost.callUDF(player.getBlockPos(), source.withLevel(CarpetSettings.runPermissionLevel), callback, args); + return cancelValue.getString().equals("cancel"); + } + catch (NullPointerException | InvalidCallbackException | IntegrityException error) + { + CarpetScriptServer.LOG.error("Got exception when running screen event call ", error); + return false; + } + } + + private void addListenerCallback(ScreenHandler screenHandler) { + if(this.callback == null) return; + + screenHandler.addListener(new ScarpetScreenHandlerListener() { + @Override + public boolean onSlotClick(ServerPlayerEntity player, SlotActionType actionType, int slot, int button) { + Map data = new HashMap<>(); + data.put(StringValue.of("slot"),slot == ScreenHandler.EMPTY_SPACE_SLOT_INDEX ? Value.NULL : NumericValue.of(slot)); + if(actionType == SlotActionType.QUICK_CRAFT) { + data.put(StringValue.of("quick_craft_stage"),NumericValue.of(ScreenHandler.unpackQuickCraftStage(button))); + button = ScreenHandler.unpackQuickCraftButton(button); + } + data.put(StringValue.of("button"),NumericValue.of(button)); + return ScreenValue.this.callListener(player,actionTypeToString(actionType),data); + } + @Override + public boolean onButtonClick(ServerPlayerEntity player, int button) { + Map data = new HashMap<>(); + data.put(StringValue.of("button"),NumericValue.of(button)); + return ScreenValue.this.callListener(player,"button",data); + } + @Override + public void onClose(ServerPlayerEntity player) { + Map data = new HashMap<>(); + ScreenValue.this.callListener(player,"close",data); + } + @Override + public boolean onSelectRecipe(ServerPlayerEntity player, Recipe recipe, boolean craftAll) { + Map data = new HashMap<>(); + data.put(StringValue.of("recipe"),StringValue.of(recipe.getId().toString())); + data.put(StringValue.of("craft_all"),BooleanValue.of(craftAll)); + return ScreenValue.this.callListener(player,"select_recipe",data); + } + @Override + public void onSlotUpdate(ScreenHandler handler, int slotId, ItemStack stack) { + Map data = new HashMap<>(); + data.put(StringValue.of("slot"),NumericValue.of(slotId)); + data.put(StringValue.of("stack"),ValueConversions.of(stack)); + ScreenValue.this.callListener(ScreenValue.this.player,"slot_update",data); + } + @Override + public void onPropertyUpdate(ScreenHandler handler, int property, int value) {} + }); + } + + private Property getPropertyForType(Class screenHandlerClass, String requiredType, int propertyIndex, String propertyName) { + if(screenHandlerClass.isInstance(this.screenHandler)) { + return ((ScreenHandlerInterface) this.screenHandler).getProperty(propertyIndex); + } + if(!this.isOpen()) { + throw new InternalExpressionException("Screen property cannot be accessed, because the screen is already closed"); + } + throw new InternalExpressionException("Screen property " + propertyName + " expected a " + requiredType + " screen."); + } + + private Property getProperty(String propertyName) { + return switch (propertyName) { + case "fuel_progress" -> getPropertyForType(AbstractFurnaceScreenHandler.class, "furnace", 0, propertyName); + case "max_fuel_progress" -> getPropertyForType(AbstractFurnaceScreenHandler.class, "furnace", 1, propertyName); + case "cook_progress" -> getPropertyForType(AbstractFurnaceScreenHandler.class, "furnace", 2, propertyName); + case "max_cook_progress" -> getPropertyForType(AbstractFurnaceScreenHandler.class, "furnace", 3, propertyName); + + case "level_cost" -> getPropertyForType(AnvilScreenHandler.class, "anvil", 0, propertyName); + + case "page" -> getPropertyForType(LecternScreenHandler.class, "lectern", 0, propertyName); + + case "beacon_level" -> getPropertyForType(BeaconScreenHandler.class, "beacon", 0, propertyName); + case "primary_effect" -> getPropertyForType(BeaconScreenHandler.class, "beacon", 1, propertyName); + case "secondary_effect" -> getPropertyForType(BeaconScreenHandler.class, "beacon", 2, propertyName); + + case "brew_time" -> getPropertyForType(BrewingStandScreenHandler.class, "brewing_stand", 0, propertyName); + case "brewing_fuel" -> getPropertyForType(BrewingStandScreenHandler.class, "brewing_stand", 1, propertyName); + + case "enchantment_power_1" -> getPropertyForType(EnchantmentScreenHandler.class, "enchantment", 0, propertyName); + case "enchantment_power_2" -> getPropertyForType(EnchantmentScreenHandler.class, "enchantment", 1, propertyName); + case "enchantment_power_3" -> getPropertyForType(EnchantmentScreenHandler.class, "enchantment", 2, propertyName); + case "enchantment_seed" -> getPropertyForType(EnchantmentScreenHandler.class, "enchantment", 3, propertyName); + case "enchantment_id_1" -> getPropertyForType(EnchantmentScreenHandler.class, "enchantment", 4, propertyName); + case "enchantment_id_2" -> getPropertyForType(EnchantmentScreenHandler.class, "enchantment", 5, propertyName); + case "enchantment_id_3" -> getPropertyForType(EnchantmentScreenHandler.class, "enchantment", 6, propertyName); + case "enchantment_level_1" -> getPropertyForType(EnchantmentScreenHandler.class, "enchantment", 7, propertyName); + case "enchantment_level_2" -> getPropertyForType(EnchantmentScreenHandler.class, "enchantment", 8, propertyName); + case "enchantment_level_3" -> getPropertyForType(EnchantmentScreenHandler.class, "enchantment", 9, propertyName); + + case "banner_pattern" -> getPropertyForType(LoomScreenHandler.class, "loom", 0, propertyName); + + case "stonecutter_recipe" -> getPropertyForType(StonecutterScreenHandler.class, "stonecutter", 0, propertyName); + + default -> throw new InternalExpressionException("Invalid screen property: " + propertyName); + }; + + } + + public Value queryProperty(String propertyName) { + if(propertyName.equals("name")) return FormattedTextValue.of(this.name); + if(propertyName.equals("open")) return BooleanValue.of(this.isOpen()); + Property property = getProperty(propertyName); + return NumericValue.of(property.get()); + } + + public Value modifyProperty(String propertyName, List lv) { + Property property = getProperty(propertyName); + int intValue = NumericValue.asNumber(lv.get(0)).getInt(); + property.set(intValue); + this.screenHandler.syncState(); + return Value.TRUE; + } + + public ScreenHandler getScreenHandler() { + return this.screenHandler; + } + + public ServerPlayerEntity getPlayer() { + return this.player; + } + + public Inventory getInventory() { + return this.inventory; + } + + @Override + public String getString() { + return this.typestring + "_screen"; + } + + @Override + public boolean getBoolean() { + return this.isOpen(); + } + + @Override + public String getTypeString() + { + return "screen"; + } + + @Override + public NbtElement toTag(boolean force) { + if(this.screenHandler == null) { + return Value.NULL.toTag(true); + } + + NbtList nbtList = new NbtList(); + for(int i = 0; i < this.screenHandler.slots.size(); i++) { + ItemStack itemStack = this.screenHandler.getSlot(i).getStack(); + nbtList.add(itemStack.writeNbt(new NbtCompound())); + } + return nbtList; + } + + + public interface ScarpetScreenHandlerListener extends ScreenHandlerListener { + boolean onSlotClick(ServerPlayerEntity player, SlotActionType actionType, int slot, int button); + boolean onButtonClick(ServerPlayerEntity player, int button); + void onClose(ServerPlayerEntity player); + boolean onSelectRecipe(ServerPlayerEntity player, Recipe recipe, boolean craftAll); + } + + public static class ScreenHandlerInventory implements Inventory { + + protected ScreenHandler screenHandler; + + public ScreenHandlerInventory(ScreenHandler screenHandler) { + this.screenHandler = screenHandler; + } + + @Override + public int size() { + return this.screenHandler.slots.size() + 1; + } + + @Override + public boolean isEmpty() { + for(Slot slot : this.screenHandler.slots) { + if(slot.hasStack() && !slot.getStack().isEmpty()) return false; + } + return this.screenHandler.getCursorStack().isEmpty(); + } + + @Override + public ItemStack getStack(int slot) { + if(slot == this.size()-1) return this.screenHandler.getCursorStack(); + return slot >= -1 && slot < this.size() ? this.screenHandler.slots.get(slot).getStack() : ItemStack.EMPTY; + } + + @Override + public ItemStack removeStack(int slot, int amount) { + ItemStack itemStack; + if(slot == this.size()-1) + itemStack = this.screenHandler.getCursorStack().split(amount); + else + itemStack = ScreenHandlerInventory.splitStack(this.screenHandler.slots, slot, amount); + if (!itemStack.isEmpty()) { + this.markDirty(); + } + return itemStack; + } + + @Override + public ItemStack removeStack(int slot) { + ItemStack itemStack; + if(slot == this.size()-1) + itemStack = this.screenHandler.getCursorStack(); + else + itemStack = this.screenHandler.slots.get(slot).getStack(); + if (itemStack.isEmpty()) { + return ItemStack.EMPTY; + } else { + if(slot == this.size()-1) + this.screenHandler.setCursorStack(ItemStack.EMPTY); + else + this.screenHandler.slots.get(slot).setStack(ItemStack.EMPTY); + return itemStack; + } + } + + @Override + public void setStack(int slot, ItemStack stack) { + if(slot == this.size()-1) + this.screenHandler.setCursorStack(stack); + else + this.screenHandler.slots.get(slot).setStack(stack); + if (!stack.isEmpty() && stack.getCount() > this.getMaxCountPerStack()) { + stack.setCount(this.getMaxCountPerStack()); + } + + this.markDirty(); + } + + @Override + public void markDirty() { + + } + + @Override + public boolean canPlayerUse(PlayerEntity player) { + return true; + } + + @Override + public void clear() { + for(Slot slot : this.screenHandler.slots) { + slot.setStack(ItemStack.EMPTY); + } + this.screenHandler.setCursorStack(ItemStack.EMPTY); + this.markDirty(); + } + + + public static ItemStack splitStack(List slots, int slot, int amount) { + return slot >= 0 && slot < slots.size() && !slots.get(slot).getStack().isEmpty() && amount > 0 ? slots.get(slot).getStack().split(amount) : ItemStack.EMPTY; + } + } + + private static String actionTypeToString(SlotActionType actionType) { + return switch (actionType) { + case PICKUP -> "pickup"; + case QUICK_MOVE -> "quick_move"; + case SWAP -> "swap"; + case CLONE -> "clone"; + case THROW -> "throw"; + case QUICK_CRAFT -> "quick_craft"; + case PICKUP_ALL -> "pickup_all"; + }; + } +} diff --git a/src/main/resources/carpet.mixins.json b/src/main/resources/carpet.mixins.json index 65bf0545..9a571f7e 100644 --- a/src/main/resources/carpet.mixins.json +++ b/src/main/resources/carpet.mixins.json @@ -186,6 +186,10 @@ "Scoreboard_scarpetMixin", "Team_scarpetMixin", "TradeOutputSlot_scarpetEventMixin", + "ScreenHandler_scarpetMixin", + "ScreenHandlerSubclasses_scarpetMixin", + "AbstractRecipeScreenHandler_scarpetMixin", + "ChunkTicketManager_scarpetMixin", "PointOfInterest_scarpetMixin",