Repository: MCreator/MCreator
Pull request #5413: Adding Text Field Sync
Author: login: 151chit
Labels: name: community review
description: Used by maintainers to mark issues that should get community reviews, tests, and feedback
name: new feature
description: Used to mark PRs that add new feature(s)
Created: 2025-03-21T15:37:52Z | Merged: 2025-05-02T15:54:09Z

Hello, I would like to try adding the ability to get text from text fields on the server from the client GUI players based on sending text from text fields when it changes. The text from the fields should be sent to the textFieldsContent hash map located in the (Name)Menu class, so that later the procedural block on the entity instance could extract these values ​​and use them.


Diff:
diff --git a/plugins/generator-1.21.1/neoforge-1.21.1/gui.definition.yaml b/plugins/generator-1.21.1/neoforge-1.21.1/gui.definition.yaml
index c97b0357172..03311bd5a6f 100644
--- a/plugins/generator-1.21.1/neoforge-1.21.1/gui.definition.yaml
+++ b/plugins/generator-1.21.1/neoforge-1.21.1/gui.definition.yaml
@@ -15,6 +15,8 @@ global_templates:
     name: "@SRCROOT/@BASEPACKAGEPATH/init/@JavaModNameMenus.java"
   - template: elementinits/screens.java.ftl
     name: "@SRCROOT/@BASEPACKAGEPATH/init/@JavaModNameScreens.java"
+  - template: gui/gui_msg_menustate.java.ftl
+    name: "@SRCROOT/@BASEPACKAGEPATH/network/MenuStateUpdateMessage.java"
 
 localizationkeys:
   - fromlist: data.getFixedTextLabels()
diff --git a/plugins/generator-1.21.1/neoforge-1.21.1/templates/elementinits/menus.java.ftl b/plugins/generator-1.21.1/neoforge-1.21.1/templates/elementinits/menus.java.ftl
index 0b6a7d89f92..577e8d36f0e 100644
--- a/plugins/generator-1.21.1/neoforge-1.21.1/templates/elementinits/menus.java.ftl
+++ b/plugins/generator-1.21.1/neoforge-1.21.1/templates/elementinits/menus.java.ftl
@@ -52,8 +52,12 @@ public class ${JavaModName}Menus {
 
 		default void sendMenuStateUpdate(Player player, int elementType, String name, Object elementState, boolean needClientUpdate) {
 			getMenuState().put(elementType + ":" + name, elementState);
-			if (needClientUpdate && player.level().isClientSide && Minecraft.getInstance().screen instanceof ${JavaModName}Screens.ScreenAccessor accessor) {
-				accessor.updateMenuState(elementType, name, elementState);
+			if (player instanceof ServerPlayer serverPlayer) {
+				PacketDistributor.sendToPlayer(serverPlayer, new MenuStateUpdateMessage(elementType, name, elementState));
+			} else if (player.level().isClientSide) {
+				if (Minecraft.getInstance().screen instanceof ${JavaModName}Screens.ScreenAccessor accessor && needClientUpdate)
+					accessor.updateMenuState(elementType, name, elementState);
+				PacketDistributor.sendToServer(new MenuStateUpdateMessage(elementType, name, elementState));
 			}
 		}
 
diff --git a/plugins/generator-1.21.1/neoforge-1.21.1/templates/gui/gui_container.java.ftl b/plugins/generator-1.21.1/neoforge-1.21.1/templates/gui/gui_container.java.ftl
index 95bc996facd..b3a4109dcc4 100644
--- a/plugins/generator-1.21.1/neoforge-1.21.1/templates/gui/gui_container.java.ftl
+++ b/plugins/generator-1.21.1/neoforge-1.21.1/templates/gui/gui_container.java.ftl
@@ -44,7 +44,13 @@ import ${package}.${JavaModName};
 </#if>
 public class ${name}Menu extends AbstractContainerMenu implements ${JavaModName}Menus.MenuAccessor {
 
-	public final HashMap<String, Object> menuState = new HashMap<>();
+	public final Map<String, Object> menuState = new HashMap<>() {
+		@Override public Object put(String key, Object value) {
+			<#-- Prevent arbitrary data storage beyond the menu state -->
+			if (!this.containsKey(key) && this.size() >= ${data.components?size}) return null;
+			return super.put(key, value);
+		}
+	};
 
 	public final Level world;
 	public final Player entity;
diff --git a/plugins/generator-1.21.1/neoforge-1.21.1/templates/gui/gui_msg_menustate.java.ftl b/plugins/generator-1.21.1/neoforge-1.21.1/templates/gui/gui_msg_menustate.java.ftl
new file mode 100644
index 00000000000..21922ba96ce
--- /dev/null
+++ b/plugins/generator-1.21.1/neoforge-1.21.1/templates/gui/gui_msg_menustate.java.ftl
@@ -0,0 +1,91 @@
+<#--
+ # MCreator (https://mcreator.net/)
+ # Copyright (C) 2012-2020, Pylo
+ # Copyright (C) 2020-2025, Pylo, opensource contributors
+ #
+ # This program is free software: you can redistribute it and/or modify
+ # it under the terms of the GNU General Public License as published by
+ # the Free Software Foundation, either version 3 of the License, or
+ # (at your option) any later version.
+ #
+ # This program is distributed in the hope that it will be useful,
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ # GNU General Public License for more details.
+ #
+ # You should have received a copy of the GNU General Public License
+ # along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ #
+ # Additional permission for code generator templates (*.ftl files)
+ #
+ # As a special exception, you may create a larger work that contains part or
+ # all of the MCreator code generator templates (*.ftl files) and distribute
+ # that work under terms of your choice, so long as that work isn't itself a
+ # template for code generation. Alternatively, if you modify or redistribute
+ # the template itself, you may (at your option) remove this special exception,
+ # which will cause the template and the resulting code generator output files
+ # to be licensed under the GNU General Public License without this special
+ # exception.
+-->
+
+<#-- @formatter:off -->
+<#include "../procedures.java.ftl">
+
+package ${package}.network;
+
+@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD) public record MenuStateUpdateMessage(int elementType, String name, Object elementState) implements CustomPacketPayload {
+
+	public static final Type<MenuStateUpdateMessage> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(${JavaModName}.MODID, "guistate_update"));
+
+	public static final StreamCodec<RegistryFriendlyByteBuf, MenuStateUpdateMessage> STREAM_CODEC = StreamCodec.of(MenuStateUpdateMessage::write, MenuStateUpdateMessage::read);
+
+	public static void write(FriendlyByteBuf buffer, MenuStateUpdateMessage message) {
+		buffer.writeInt(message.elementType);
+		buffer.writeUtf(message.name);
+		if (message.elementType == 0) {
+			buffer.writeUtf((String) message.elementState);
+		} else if (message.elementType == 1) {
+			buffer.writeBoolean((boolean) message.elementState);
+		}
+	}
+
+	public static MenuStateUpdateMessage read(FriendlyByteBuf buffer) {
+		int elementType = buffer.readInt();
+		String name = buffer.readUtf();
+		Object elementState = null;
+		if (elementType == 0) {
+			elementState = buffer.readUtf();
+		} else if (elementType == 1) {
+			elementState = buffer.readBoolean();
+		}
+		return new MenuStateUpdateMessage(elementType, name, elementState);
+	}
+
+	@Override public Type<MenuStateUpdateMessage> type() {
+		return TYPE;
+	}
+
+	public static void handleMenuState(final MenuStateUpdateMessage message, final IPayloadContext context) {
+		<#-- Security measure to prevent accepting too big strings -->
+		if (message.name.length() > 256 || message.elementState instanceof String string && string.length() > 8192)
+			return;
+
+		context.enqueueWork(() -> {
+			if (context.player().containerMenu instanceof ${JavaModName}Menus.MenuAccessor menu) {
+				menu.getMenuState().put(message.elementType + ":" + message.name, message.elementState);
+				if (context.flow() == PacketFlow.CLIENTBOUND && Minecraft.getInstance().screen instanceof ${JavaModName}Screens.ScreenAccessor accessor) {
+					accessor.updateMenuState(message.elementType, message.name, message.elementState);
+				}
+			}
+		}).exceptionally(e -> {
+			context.connection().disconnect(Component.literal(e.getMessage()));
+			return null;
+		});
+	}
+
+	@SubscribeEvent public static void registerMessage(FMLCommonSetupEvent event) {
+		${JavaModName}.addNetworkMessage(MenuStateUpdateMessage.TYPE, MenuStateUpdateMessage.STREAM_CODEC, MenuStateUpdateMessage::handleMenuState);
+	}
+
+}
+<#-- @formatter:on -->
\ No newline at end of file
diff --git a/plugins/generator-1.21.4/neoforge-1.21.4/gui.definition.yaml b/plugins/generator-1.21.4/neoforge-1.21.4/gui.definition.yaml
index c97b0357172..03311bd5a6f 100644
--- a/plugins/generator-1.21.4/neoforge-1.21.4/gui.definition.yaml
+++ b/plugins/generator-1.21.4/neoforge-1.21.4/gui.definition.yaml
@@ -15,6 +15,8 @@ global_templates:
     name: "@SRCROOT/@BASEPACKAGEPATH/init/@JavaModNameMenus.java"
   - template: elementinits/screens.java.ftl
     name: "@SRCROOT/@BASEPACKAGEPATH/init/@JavaModNameScreens.java"
+  - template: gui/gui_msg_menustate.java.ftl
+    name: "@SRCROOT/@BASEPACKAGEPATH/network/MenuStateUpdateMessage.java"
 
 localizationkeys:
   - fromlist: data.getFixedTextLabels()
diff --git a/plugins/generator-1.21.4/neoforge-1.21.4/templates/elementinits/menus.java.ftl b/plugins/generator-1.21.4/neoforge-1.21.4/templates/elementinits/menus.java.ftl
index 0b6a7d89f92..577e8d36f0e 100644
--- a/plugins/generator-1.21.4/neoforge-1.21.4/templates/elementinits/menus.java.ftl
+++ b/plugins/generator-1.21.4/neoforge-1.21.4/templates/elementinits/menus.java.ftl
@@ -52,8 +52,12 @@ public class ${JavaModName}Menus {
 
 		default void sendMenuStateUpdate(Player player, int elementType, String name, Object elementState, boolean needClientUpdate) {
 			getMenuState().put(elementType + ":" + name, elementState);
-			if (needClientUpdate && player.level().isClientSide && Minecraft.getInstance().screen instanceof ${JavaModName}Screens.ScreenAccessor accessor) {
-				accessor.updateMenuState(elementType, name, elementState);
+			if (player instanceof ServerPlayer serverPlayer) {
+				PacketDistributor.sendToPlayer(serverPlayer, new MenuStateUpdateMessage(elementType, name, elementState));
+			} else if (player.level().isClientSide) {
+				if (Minecraft.getInstance().screen instanceof ${JavaModName}Screens.ScreenAccessor accessor && needClientUpdate)
+					accessor.updateMenuState(elementType, name, elementState);
+				PacketDistributor.sendToServer(new MenuStateUpdateMessage(elementType, name, elementState));
 			}
 		}
 
diff --git a/plugins/generator-1.21.4/neoforge-1.21.4/templates/gui/gui_container.java.ftl b/plugins/generator-1.21.4/neoforge-1.21.4/templates/gui/gui_container.java.ftl
index c878dc3e8e4..bfebb57a9ee 100644
--- a/plugins/generator-1.21.4/neoforge-1.21.4/templates/gui/gui_container.java.ftl
+++ b/plugins/generator-1.21.4/neoforge-1.21.4/templates/gui/gui_container.java.ftl
@@ -45,7 +45,13 @@ import ${package}.${JavaModName};
 
 public class ${name}Menu extends AbstractContainerMenu implements ${JavaModName}Menus.MenuAccessor {
 
-	public final HashMap<String, Object> menuState = new HashMap<>();
+	public final Map<String, Object> menuState = new HashMap<>() {
+		@Override public Object put(String key, Object value) {
+			<#-- Prevent arbitrary data storage beyond the menu state -->
+			if (!this.containsKey(key) && this.size() >= ${data.components?size}) return null;
+			return super.put(key, value);
+		}
+	};
 
 	public final Level world;
 	public final Player entity;
diff --git a/plugins/generator-1.21.4/neoforge-1.21.4/templates/gui/gui_msg_menustate.java.ftl b/plugins/generator-1.21.4/neoforge-1.21.4/templates/gui/gui_msg_menustate.java.ftl
new file mode 100644
index 00000000000..9305a9a6e35
--- /dev/null
+++ b/plugins/generator-1.21.4/neoforge-1.21.4/templates/gui/gui_msg_menustate.java.ftl
@@ -0,0 +1,91 @@
+<#--
+ # MCreator (https://mcreator.net/)
+ # Copyright (C) 2012-2020, Pylo
+ # Copyright (C) 2020-2025, Pylo, opensource contributors
+ #
+ # This program is free software: you can redistribute it and/or modify
+ # it under the terms of the GNU General Public License as published by
+ # the Free Software Foundation, either version 3 of the License, or
+ # (at your option) any later version.
+ #
+ # This program is distributed in the hope that it will be useful,
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ # GNU General Public License for more details.
+ #
+ # You should have received a copy of the GNU General Public License
+ # along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ #
+ # Additional permission for code generator templates (*.ftl files)
+ #
+ # As a special exception, you may create a larger work that contains part or
+ # all of the MCreator code generator templates (*.ftl files) and distribute
+ # that work under terms of your choice, so long as that work isn't itself a
+ # template for code generation. Alternatively, if you modify or redistribute
+ # the template itself, you may (at your option) remove this special exception,
+ # which will cause the template and the resulting code generator output files
+ # to be licensed under the GNU General Public License without this special
+ # exception.
+-->
+
+<#-- @formatter:off -->
+<#include "../procedures.java.ftl">
+
+package ${package}.network;
+
+@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD) public record MenuStateUpdateMessage(int elementType, String name, Object elementState) implements CustomPacketPayload {
+
+	public static final Type<MenuStateUpdateMessage> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(${JavaModName}.MODID, "menustate_update"));
+
+	public static final StreamCodec<RegistryFriendlyByteBuf, MenuStateUpdateMessage> STREAM_CODEC = StreamCodec.of(MenuStateUpdateMessage::write, MenuStateUpdateMessage::read);
+
+	public static void write(FriendlyByteBuf buffer, MenuStateUpdateMessage message) {
+		buffer.writeInt(message.elementType);
+		buffer.writeUtf(message.name);
+		if (message.elementType == 0) {
+			buffer.writeUtf((String) message.elementState);
+		} else if (message.elementType == 1) {
+			buffer.writeBoolean((boolean) message.elementState);
+		}
+	}
+
+	public static MenuStateUpdateMessage read(FriendlyByteBuf buffer) {
+		int elementType = buffer.readInt();
+		String name = buffer.readUtf();
+		Object elementState = null;
+		if (elementType == 0) {
+			elementState = buffer.readUtf();
+		} else if (elementType == 1) {
+			elementState = buffer.readBoolean();
+		}
+		return new MenuStateUpdateMessage(elementType, name, elementState);
+	}
+
+	@Override public Type<MenuStateUpdateMessage> type() {
+		return TYPE;
+	}
+
+	public static void handleMenuState(final MenuStateUpdateMessage message, final IPayloadContext context) {
+		<#-- Security measure to prevent accepting too big strings -->
+		if (message.name.length() > 256 || message.elementState instanceof String string && string.length() > 8192)
+			return;
+
+		context.enqueueWork(() -> {
+			if (context.player().containerMenu instanceof ${JavaModName}Menus.MenuAccessor menu) {
+				menu.getMenuState().put(message.elementType + ":" + message.name, message.elementState);
+				if (context.flow() == PacketFlow.CLIENTBOUND && Minecraft.getInstance().screen instanceof ${JavaModName}Screens.ScreenAccessor accessor) {
+					accessor.updateMenuState(message.elementType, message.name, message.elementState);
+				}
+			}
+		}).exceptionally(e -> {
+			context.connection().disconnect(Component.literal(e.getMessage()));
+			return null;
+		});
+	}
+
+	@SubscribeEvent public static void registerMessage(FMLCommonSetupEvent event) {
+		${JavaModName}.addNetworkMessage(MenuStateUpdateMessage.TYPE, MenuStateUpdateMessage.STREAM_CODEC, MenuStateUpdateMessage::handleMenuState);
+	}
+
+}
+<#-- @formatter:on -->
\ No newline at end of file
diff --git a/plugins/mcreator-core/procedures/gui_get_text_textfield.json b/plugins/mcreator-core/procedures/gui_get_text_textfield.json
index 5bf241d7536..55148aa225d 100644
--- a/plugins/mcreator-core/procedures/gui_get_text_textfield.json
+++ b/plugins/mcreator-core/procedures/gui_get_text_textfield.json
@@ -9,12 +9,6 @@
       "type": "input_value",
       "name": "entity",
       "check": "Entity"
-    },
-    {
-      "type": "field_image",
-      "src": "./res/client.png",
-      "width": 8,
-      "height": 24
     }
   ],
   "output": "String",
diff --git a/plugins/mcreator-core/procedures/gui_get_value_checkbox.json b/plugins/mcreator-core/procedures/gui_get_value_checkbox.json
index 79492617b75..7f9484d4008 100644
--- a/plugins/mcreator-core/procedures/gui_get_value_checkbox.json
+++ b/plugins/mcreator-core/procedures/gui_get_value_checkbox.json
@@ -9,12 +9,6 @@
       "type": "input_value",
       "name": "entity",
       "check": "Entity"
-    },
-    {
-      "type": "field_image",
-      "src": "./res/client.png",
-      "width": 8,
-      "height": 24
     }
   ],
   "output": "Boolean",
diff --git a/plugins/mcreator-core/procedures/gui_set_text_textfield.json b/plugins/mcreator-core/procedures/gui_set_text_textfield.json
index 2f4e1443bf6..30d3974eeea 100644
--- a/plugins/mcreator-core/procedures/gui_set_text_textfield.json
+++ b/plugins/mcreator-core/procedures/gui_set_text_textfield.json
@@ -14,12 +14,6 @@
       "type": "input_value",
       "name": "text",
       "check": "String"
-    },
-    {
-      "type": "field_image",
-      "src": "./res/client.png",
-      "width": 8,
-      "height": 24
     }
   ],
   "inputsInline": true,
diff --git a/plugins/mcreator-localization/lang/texts.properties b/plugins/mcreator-localization/lang/texts.properties
index d32d2ca3789..f1c0aa0bfc2 100644
--- a/plugins/mcreator-localization/lang/texts.properties
+++ b/plugins/mcreator-localization/lang/texts.properties
@@ -1170,9 +1170,9 @@ blockly.block.gui_clear_slot=Clear slot %1 of the current GUI of %2
 blockly.block.gui_damage_item=Deal %1 damage to item in slot %2 of the current GUI of %3
 blockly.block.gui_get_amount_inslot=Get number of items from slot %1 of the current GUI of %2
 blockly.block.gui_get_item_inslot=Get item from slot %1 of the current GUI of %2
-blockly.block.gui_get_text_textfield=Get text inside textfield %1 of the current GUI of %2 %3
-blockly.block.gui_get_value_checkbox=Is checkbox %1 checked for the current GUI of %2 %3
-blockly.block.gui_set_text_textfield=Set text inside textfield %1 to %3 for the current GUI of %2 %4
+blockly.block.gui_get_text_textfield=Get text inside textfield %1 of the current GUI of %2
+blockly.block.gui_get_value_checkbox=Is checkbox %1 checked for the current GUI of %2
+blockly.block.gui_set_text_textfield=Set text inside textfield %1 to %3 for the current GUI of %2
 blockly.block.gui_remove_items=Remove %1 items from slot %2 of the current GUI of %3
 blockly.block.gui_set_items=Set %1 %3 in slot %2 of the current GUI of %4
 blockly.block.item_add_enhancement=Add enchantment %3 with level %1 to %2


Reviews:
user: login: KlemenDEV
body: Hi, while this is good idea, the implementation should be changed a bit. 

Currently, guistate dependency holds all client-side state of the GUI (textfield values and checkbox check states).

I think a better approach would be more generalized approach where a whole guistate is synced to the server-side when user does client-side actions. Additionally, I don't think a server-side UI should have contents set, rather just state itself should be used.

Situation where a dedicated server is used and multiple users sent different client-side states of the same GUI should also be considered, basically how this will work.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-29T12:58:31Z
user: login: KlemenDEV
body: Would it be possible to split this PR into two PRs?

1. Rework of guistate and converter for guistate dependency
2. Sync of guistate

This way we can keep changes more atomic and managable
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T12:18:13Z
user: login: KlemenDEV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T14:02:31Z
user: login: KlemenDEV
body: Also make sure to update the side indicators of those:

![image](https://github.com/user-attachments/assets/39d8d9e3-ffc5-4c18-b308-d86bbeb4518a)

Since they will now be synced on both sides, you should just remove them. Make sure to update texts.properties too
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T16:44:54Z
user: login: KlemenDEV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T12:39:38Z
user: login: KlemenDEV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T12:41:06Z
user: login: KlemenDEV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:00:02Z
user: login: KlemenDEV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:00:18Z
user: login: KlemenDEV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:01:41Z
user: login: KlemenDEV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-01T13:03:40Z
user: login: KlemenDEV
body: LGTM, thanks for working on this very important feature! :)
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:54:03Z

Review comments:
diff_hunk: @@ -50,10 +50,19 @@ public class ${JavaModName}Menus {
 
 		Map<Integer, Slot> getSlots();
 
-		default void sendMenuStateUpdate(Level world, int elementType, String name, Object elementState, boolean needClientUpdate) {
+		ServerPlayer getServerOwner();
+
+		default void sendMenuStateUpdate(int elementType, String name, Object elementState, boolean needClientUpdate) {
path: plugins/generator-1.21.4/neoforge-1.21.4/templates/elementinits/menus.java.ftl
user: login: KlemenDEV
body: Wouldn't it be cleaner to pass the entity as an argument?
created_at: 2025-04-30T14:02:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,93 @@
+<#--
+ # MCreator (https://mcreator.net/)
+ # Copyright (C) 2012-2020, Pylo
+ # Copyright (C) 2020-2025, Pylo, opensource contributors
+ #
+ # This program is free software: you can redistribute it and/or modify
+ # it under the terms of the GNU General Public License as published by
+ # the Free Software Foundation, either version 3 of the License, or
+ # (at your option) any later version.
+ #
+ # This program is distributed in the hope that it will be useful,
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ # GNU General Public License for more details.
+ #
+ # You should have received a copy of the GNU General Public License
+ # along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ #
+ # Additional permission for code generator templates (*.ftl files)
+ #
+ # As a special exception, you may create a larger work that contains part or
+ # all of the MCreator code generator templates (*.ftl files) and distribute
+ # that work under terms of your choice, so long as that work isn't itself a
+ # template for code generation. Alternatively, if you modify or redistribute
+ # the template itself, you may (at your option) remove this special exception,
+ # which will cause the template and the resulting code generator output files
+ # to be licensed under the GNU General Public License without this special
+ # exception.
+-->
+
+<#-- @formatter:off -->
+<#include "../procedures.java.ftl">
+
+package ${package}.network;
+
+@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD) public record ${JavaModName}MenustateUpdateMessage(int elementType, String name, Object elementState) implements CustomPacketPayload {
+
+	public static final Type<${JavaModName}MenustateUpdateMessage> TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath(${JavaModName}.MODID, "guistate_update"));
+
+	public static final StreamCodec<RegistryFriendlyByteBuf, ${JavaModName}MenustateUpdateMessage> STREAM_CODEC = StreamCodec.of(${JavaModName}MenustateUpdateMessage::write, ${JavaModName}MenustateUpdateMessage::read);
+
+	public static void write(FriendlyByteBuf buffer, ${JavaModName}MenustateUpdateMessage message) {
+		int eType = message.elementType;
path: plugins/generator-1.21.1/neoforge-1.21.1/templates/gui/gui_msg_menustate.java.ftl
user: login: KlemenDEV
body: I recommend using full name elementType for readability, it is static method so this does not collide with record's elementType. Apples to both versions
created_at: 2025-05-01T12:39:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,6 +15,8 @@ global_templates:
     name: "@SRCROOT/@BASEPACKAGEPATH/init/@JavaModNameMenus.java"
   - template: elementinits/screens.java.ftl
     name: "@SRCROOT/@BASEPACKAGEPATH/init/@JavaModNameScreens.java"
+  - template: gui/gui_msg_menustate.java.ftl
+    name: "@SRCROOT/@BASEPACKAGEPATH/network/@JavaModNameMenustateUpdateMessage.java"
path: plugins/generator-1.21.1/neoforge-1.21.1/gui.definition.yaml
user: login: KlemenDEV
body: Use MenuState instead of Menustate
created_at: 2025-05-01T12:41:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,7 +44,7 @@ public class ${name}Screen extends AbstractContainerScreen<${name}Menu> implemen
 	private final int x, y, z;
 	private final Player entity;
 
-	private boolean menuStateUpdateActive = false;
+  private boolean menuStateUpdateActive;
path: plugins/generator-1.21.4/neoforge-1.21.4/templates/gui/gui_window.java.ftl
user: login: KlemenDEV
body: I would avoid unrelated changes
created_at: 2025-05-01T13:00:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,7 +44,7 @@ public class ${name}Screen extends AbstractContainerScreen<${name}Menu> implemen
 	private final int x, y, z;
 	private final Player entity;
 
-	private boolean menuStateUpdateActive = false;
+	private boolean menuStateUpdateActive;
path: plugins/generator-1.21.1/neoforge-1.21.1/templates/gui/gui_window.java.ftl
user: login: KlemenDEV
body: Unrelated change
created_at: 2025-05-01T13:00:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,10 +83,8 @@ public class ${name}Screen extends AbstractContainerScreen<${name}Menu> implemen
 			</#list>
 		}
 		</#if>
-
-		<#-- updateMenuState is not implemented for checkboxes, as there is no procedure block to set checkbox state currently -->
-
 		menuStateUpdateActive = false;
+		<#-- updateMenuState is not implemented for checkboxes, as there is no procedure block to set checkbox state currently -->
path: plugins/generator-1.21.4/neoforge-1.21.4/templates/gui/gui_window.java.ftl
user: login: KlemenDEV
body: Unrelated changes
created_at: 2025-05-01T13:01:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -84,10 +83,8 @@ public class ${name}Screen extends AbstractContainerScreen<${name}Menu> implemen
 			</#list>
 		}
 		</#if>
-
-		<#-- updateMenuState is not implemented for checkboxes, as there is no procedure block to set checkbox state currently -->
-
 		menuStateUpdateActive = false;
+		<#-- updateMenuState is not implemented for checkboxes, as there is no procedure block to set checkbox state currently -->
path: plugins/generator-1.21.4/neoforge-1.21.4/templates/gui/gui_window.java.ftl
user: login: KlemenDEV
body: @151chit best to just checkout the file from master instead of manually tinkering changes until it works :)
created_at: 2025-05-01T13:03:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: CLAassistant
created_at: 2025-03-21T15:37:59Z
body: [![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/MCreator/MCreator?pullRequest=5413) <br/>All committers have signed the CLA.
author_association: NONE
user: login: KlemenDEV
created_at: 2025-03-23T11:43:56Z
body: Thank you for your contribution!
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-03-29T15:59:23Z
body: I don't quite understand this implementation, can you elaborate a bit? By client-side action do you mean changing the state of a text field or a checkbox? When acting on the client side, should the entire guistate hash map be sent to the server in the format:
PacketDistributor.sendToServer(new GuiStateMessage(guistate));
, even those elements that haven't changed their state, or only those that were directly interacted with?

I understand it this way:
the guistate hash map should be made non-static and received in the Screen class from the constructor.
Then the network packet on the server side should put values ​​in it in the format:
guistate.put("textfield:{text field name }", text );
and guistate.put("checkbox:{checkbox name }", boolean );
in the container menu class?

My initial idea was that the states of text fields and checkboxes should be synchronized in the server part of player.containerMenu, like slots do. Procedures on the Entity instance could get the value from the hash map on the server and apply them. Also, the menu class would have an interface by which it could be distinguished from other menus (for example, vanilla or from other mods) and get the hash map from it.

And should the procedure still use Guistate as a dependency and go to the arguments of the procedures, or is it possible to get it from containerMenu through the interface?

Also, I assume that creating instances of checkboxes or text fields on the server and putting them in the hash map is pointless, and there should only be values ​​on the server: string or boolean.

(Perhaps I misunderstood something from your message, because I use Google Translate and do not speak English)
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-03-29T18:15:40Z
body: > By client-side action do you mean changing the state of a text field or a checkbox

Yes. Those actions are not synced to server automatically so some custom sync for all those should be made at once to cover all those and potential future client GUI component to server sync scenarios

> My initial idea was that the states of text fields and checkboxes should be synchronized in the server part of player.containerMenu, like slots do. 

Utilizing vanilla systems for that would be actually the best approach possible :)

> And should the procedure still use Guistate as a dependency and go to the arguments of the procedures, or is it possible to get it from containerMenu through the interface?

You can rework that if needed, but procedures need some variable that binds GUI data to the actual procedure blocks using it. You can do non-direct dependency such as you did with entity

> Also, I assume that creating instances of checkboxes or text fields on the server and putting them in the hash map is pointless, and there should only be values ​​on the server: string or boolean.

Sure, but ideally you would sync whole UI state to server to not have tons of different packets for every possible client action (currently just checkbox change or textfield change) that could appear in the future
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-03-29T19:26:14Z
body: > Sure, but ideally you would sync whole UI state to server to not have tons of different packets for every possible client action (currently just checkbox change or textfield change) that could appear in the future

I wouldn't want to do that, I don't think it's reliable enough, a mod developer can create a GUI that could have, for example, 20 text fields (for server settings, for example) and a checkbox next to each one, if a user of such a mod completely fills in all 20 fields to the max value and all of this is sent to the server at once, it can kick the player for exceeding the network packet weight limit. (At least that's what happened to me when I made a plugin for synchronizing text fields and checkboxes, which had a scheme for sending all the data in one packet. I was kicked from the server when testing whether to fill in all the text fields with text to the maximum.) It also seems to me that sending a network packet when a specific element changes will not affect the network much, given that the vanilla code can send several packets each tick, for example when the player moves.

I also sent a new version of the implementation, where guistate is used to store data on the server, but the storage format on the server and the client is different, because on the server I can't create client GUI elements for this hash map, and the client part should still be able to take GUI elements from it.
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-03-29T19:28:02Z
body: > I also sent a new version of the implementation, where guistate is used to store data on the server, but the storage format on the server and the client is different, because on the server I can't create client GUI elements for this hash map, and the client part should still be able to take GUI elements from it.

Did not check the PR yet, but indeed server-side should just store String/boolean values. You could technically rework the guistate as a whole to just contain state and not actual components on both client and server. It would also simplify the procedures
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-03-29T19:36:18Z
body: I just want the receiving procedures to be able to be called from any triggers, and not just the GUI itself as before, so guistate needs to be obtained somehow, for this you need an interface, and a custom one, not vanilla, like with slots, so the procedure will increase in any case, because you need to check the entity for the player and the menu belonging to the menu of the created mod, to reduce the volume of the procedural block, I moved the logic of receiving to the menu class, but it still turned out to be too big.

Also, I don’t know what may appear in the future, and I did not rework the principle of guistate itself, because suddenly you really need to obtain instances of GUI component classes and not just their values, if you need to make storage in guistate only values ​​and not the classes themselves on both sides, let me know
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-03-29T19:45:07Z
body: > if you need to make storage in guistate only values ​​and not the classes themselves on both sides, let me know

I think this may be a good idea to keep concept of guistate matching between server and client, so yes
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-03-30T15:35:18Z
body: In this version of the logic, I have reworked guistate, now it can store two-way synchronized values ​​of the player's gui elements states.

With the method:
public static <T> T getGuistateState(Entity entity, int elementType, String name, T defaultValue) in the Menus class, you can get synchronized values ​​of text fields and checkboxes in the format:
MyModMenus.getGuistateState(entity, 0 for text fields, 1 for checkboxes, "textfieldname", "" or false for checkboxes).

Next, with the method:
public static void sendGuistateUpdate(Player entity, int elementType, String name, Object elementState) you can update the hash map values, respectively, when called on the client, the changes will be reflected on the client and sent to the server, when called on the server, the changes will be reflected on the server and will go to the player's client.

To add synchronization to new elements, you can define new elementType.

When the guistate changes from the server, the client will automatically update the contents of the gui components using the public void onGuistateUpdate(int elementType, String name, Object elementState) method in the screen class.

The text setting block in the text field has also been changed.
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-03-31T14:13:00Z
body: Sorry, I'm new to GitHub. By splitting a PR into 2 does it mean creating 2 new ones and closing this one?
Also, should I remove guistate as a dependency in the procedures and make it accessible indirectly, through an instance of the entity, or leave it? -
![dependency](https://i.imgur.com/UMDOliM.png)
And how will 2 separate branches of two different PRs work if in the second PR "Rework of guistate and converter for guistate dependency" the code generated by the procedures will try to access the methods for working with synchronized data in the ${JavaModName}Menus class, which are not in the branch of the second PR but are only in the first?
It seems to me that if you split the PR into 2, the branch will either not pass the tests, or the mod generated by the branch will not compile?
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-03-31T15:47:24Z
body: > Sorry, I'm new to GitHub. By splitting a PR into 2 does it mean creating 2 new ones and closing this one?

You can keep this one, and make another one where only guistate rework is present. Then that one will be merged and at this point, you will merge master into this PR to apply changes here.

> Also, should I remove guistate as a dependency in the procedures and make it accessible indirectly, through an instance of the entity, or leave it? -

If now all access to guistate is provided via entity, just completely remove it, just make sure to do converter that replaces it with entity for existing workspaces.

> And how will 2 separate branches of two different PRs work if in the second PR "Rework of guistate and converter for guistate dependency" the code generated by the procedures will try to access the methods for working with synchronized data in the ${JavaModName}Menus class, which are not in the branch of the second PR but are only in the first?

The first PR should focus only on reworking the way guistate is handled and should not do any syncing yet.

> It seems to me that if you split the PR into 2, the branch will either not pass the tests, or the mod generated by the branch will not compile?

You can just draft this branch for now and open another one that focuses on

> Rework of guistate and converter for guistate dependency

without any syncing
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-03-31T19:08:12Z
body: I opened a PR https://github.com/MCreator/MCreator/pull/5430 which is focused on changing the logic of guistate, without adding synchronization yet
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-04-01T08:13:05Z
body: Thanks, will check it out!
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-04-02T12:16:04Z
body: On hold by https://github.com/MCreator/MCreator/pull/5430
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-04-29T11:43:10Z
body: @151chit, this PR is ready for merging and then for the final review :) Also, congrats on the first merged PR!
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-04-29T19:42:33Z
body: I would like to ask a few questions:
1) Should I use some flag (class field) in onMenuStateUpdate method that would be set to true when updating and then to false when finished, or maybe set responder to null during update and then restore it from final field? Because vanilla setResponder system will not be able to distinguish text field update from handle method of packet from user change, and therefore when receiving data from server client will send packet back to server again, which is not good, and it seems to me that any solutions here will be hacks

2) Should I make menustate setter procedure blocks with !level.isClientSide check, because without it I will get the following:
Running procedure from GUI containing menustate setter ->
     1. Run procedure locally on the client (send menustate update packet from client to server).
     2. Run procedure on the server (procedure will send client update from server again).
     
and this will be at the same time, is there a need for server side check then?
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-04-30T13:59:21Z
body: > Should I use some flag (class field) in onMenuStateUpdate method that would be set to true when updating and then to false when finished, or maybe set responder to null during update and then restore it from final field? Because vanilla setResponder system will not be able to distinguish text field update from handle method of packet from user change, and therefore when receiving data from server client will send packet back to server again, which is not good, and it seems to me that any solutions here will be hacks

I see. I have made a small PR to fix this, so this PR is not bloated. Please review it at: https://github.com/MCreator/MCreator/pull/5474

> Should I make menustate setter procedure blocks with !level.isClientSide check, because without it I will get the following:
> Running procedure from GUI containing menustate setter ->
> 
> Run procedure locally on the client (send menustate update packet from client to server).
> Run procedure on the server (procedure will send client update from server again).
> and this will be at the same time, is there a need for server side check then?

If you do side check inside the procedure block, this will make it so setting text-field value on client-side is not possible and we don't want that. In other words, **If you add this check and "call set text field" from client side, nothing will happen.** You need to figure out some different solution. Maybe some sort of argument passed to sendMenuStateUpdate to know it it was called from responder, procedure, or packet handler and then decide what to do in sendMenuStateUpdate?
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-04-30T16:36:44Z
body: I left a comment under #5474
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-04-30T16:45:08Z
body: > I left a comment under https://github.com/MCreator/MCreator/pull/5474

Will check asap
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-04-30T17:52:45Z
body: I wanted to ask about sending a packet twice:
As I understand it, vanilla logic also has such a system, for example, the block breaking system:
Player breaks a block ->
1) Local block deletion in the world.
2) Sending a packet to the server -> server breaking logic -> packet returns the state of the block to the local world to the player.

MenuState system:
Player changes a text field and presses, for example, a button that starts a procedure that has "Set text field text" ->
1) Local change of the text field.
2) Sending a packet to the server to start the procedure -> "Set text field text" on the server -> packet changes the local value of the field.

Is this acceptable?
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-04-30T20:53:33Z
body: > MenuState system:
> Player changes a text field and presses, for example, a button that starts a procedure that has "Set text field text" ->
> 
> Local change of the text field.
> Sending a packet to the server to start the procedure -> "Set text field text" on the server -> packet changes the local value of the field.
> Is this acceptable?

If procedure block is called from server and client side, it is acceptable client side update happens twice (once locally and once via the packet).

But if procedure block is called from server side only, I don't think it is ok client side code runs twice, but if I understood you correctly, this won't happen.
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-05-01T12:07:46Z
body: I made a slight mistake when I wrote "Local change of the text field." By this I meant that the button in the client GUI launches a procedure on the client, which has a "Change of the text field" block.

The button macro launches:
<#if hasProcedure(component.onClick)>
if (<@procedureOBJToConditionCode component.displayCondition/>) {
PacketDistributor.sendToServer(new ${name}ButtonMessage(${btid}, x, y, z)); //procedure on the server
${name}ButtonMessage.handleButtonAction(entity, ${btid}, x, y, z); //procedure on the client
}
</#if>
Then this procedure contains a procedure block with:
_menu.sendMenuStateUpdate(_entity, 0, "${field$textfield}", ${input$text}, true); //does not check the side, two-way.
1)sendMenuStateUpdate on the server -> Set the hash map and send the packet to the client
2)sendMenuStateUpdate on the client -> Set the hash map, change the contents of the GUI elements themselves, and again the packet.

The packet itself -> set the hash map and if the client, then update the GUI elements.

And from the procedure that puts the text in the text field and is launched from the button in the GUI there will be 3 packages:
1) Server procedure package
2) Menustate package (client -> server)
3) Menustate package (server -> client (due to package 1))

Is this normal and users should control the side themselves through the "Is client World" procedure block or not?
It will just be very difficult to fix and track
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-05-01T12:10:10Z
body: Also, should there be some check when generating the menustate part of menuaccessor and the menustate package if the user has not added a checkbox or text field in any GUI, or would that be a useless cluttering of the freemarker generator?
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-05-01T12:34:12Z
body: > By this I meant that the button in the client GUI launches a procedure on the client

GUI button click runs on both sides and is no different trigger than any other procedure trigger. When thinking of procedures, I recommend thinking universally and not for a specific use case. But I see your concern (explained below)

> Menustate package (server -> client (due to package 1))

If we think universally, many actions in the game send packets. There are other procedure triggers in MCreator that could happen locally and then be synced to server resulting in a similar problem.

So I think we can accept this and leave up to user to limit this if they want to (and understand why they need it, most users of MCreator will not understand this problem)

> should users control the side themselves through the "Is client World" procedure block or not?

This procedure definitely should work on both sides. Otherwise, this PR is kinda pointless and will break existing workspaces where this is manipulated client-side currently.
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-05-01T12:42:11Z
body: https://github.com/MCreator/MCreator/pull/5474 was merged so this PR should merge master into it
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-05-01T13:15:34Z
body: Ready for review/testing?
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-05-01T13:16:35Z
body: I think yes
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-05-02T06:58:15Z
body: This PR currently opens a door for a potential DoS attack from a hacked client that would result in heap exhaustion on the server (or on the client for a maliciously modified server). I will prepare a fix for this, but we always need to be careful when doing networking stuff to make it resistant to exploits.
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-05-02T08:50:14Z
body: Can DOS happen because the hash map will fill up until the RAM runs out?
author_association: CONTRIBUTOR
user: login: KlemenDEV
created_at: 2025-05-02T09:00:56Z
body: One can craft packets to put dummy stuff into the map from client to the server until RAM runs out, yes. I have working bandaid for this, just need to test it out
author_association: CONTRIBUTOR
user: login: 151chit
created_at: 2025-05-02T16:54:00Z
body: Thank you too
author_association: CONTRIBUTOR