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 extends ScreenHandler> 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 8f5d8084..1e60b2c9 100644
--- a/src/main/resources/carpet.mixins.json
+++ b/src/main/resources/carpet.mixins.json
@@ -187,6 +187,10 @@
"Scoreboard_scarpetMixin",
"Team_scarpetMixin",
"TradeOutputSlot_scarpetEventMixin",
+ "ScreenHandler_scarpetMixin",
+ "ScreenHandlerSubclasses_scarpetMixin",
+ "AbstractRecipeScreenHandler_scarpetMixin",
+
"ChunkTicketManager_scarpetMixin",
"PointOfInterest_scarpetMixin",