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 <replaceitem.mc@gmail.com>
This commit is contained in:
replaceitemandreplaceitem authored and GitHub committed 2022-01-12 22:52:38 +01:00
1 parent 1a96533818
commit 3f6ddc43e5
12 files changed
+931 -5

No files matched your search

+269
View File
@@ -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
</pre>
## 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
<details>
<summary>Chest click event</summary>
```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'
));
);
```
</details>
<details>
<summary>Anvil text prompt</summary>
```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":""}\'}}');
);
```
</details>
<details>
<summary>Lectern flip book</summary>
```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);
);
));
);
```
</details>
<details>
<summary>generic_3x3 cursor stack</summary>
```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);
);
));
);
```
</details>
@@ -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
@@ -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
@@ -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);
}
@@ -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();
}
}
@@ -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<Boolean> cir) {
if(((ScreenHandlerInterface) this).callButtonClickListener(id,player))
cir.cancel();
}
}
@@ -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<ScreenHandlerListener> listeners;
@Shadow public abstract void syncState();
@Shadow @Final private List<Property> 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);
}
}
@@ -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
@@ -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);
@@ -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");
}
}
@@ -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<String,ScarpetScreenHandlerFactory> 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<Value,Value> data) {
Value playerValue = EntityValue.of(player);
Value actionValue = StringValue.of(action);
Value dataValue = MapValue.wrap(data);
List<Value> 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<Value,Value> 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<Value,Value> 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<Value,Value> data = new HashMap<>();
ScreenValue.this.callListener(player,"close",data);
}
@Override
public boolean onSelectRecipe(ServerPlayerEntity player, Recipe<?> recipe, boolean craftAll) {
Map<Value,Value> 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<Value,Value> 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<Value> 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<Slot> 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";
};
}
}
+4
View File
@@ -186,6 +186,10 @@
"Scoreboard_scarpetMixin",
"Team_scarpetMixin",
"TradeOutputSlot_scarpetEventMixin",
"ScreenHandler_scarpetMixin",
"ScreenHandlerSubclasses_scarpetMixin",
"AbstractRecipeScreenHandler_scarpetMixin",
"ChunkTicketManager_scarpetMixin",
"PointOfInterest_scarpetMixin",