diff --git a/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_stocking_input_bus.json b/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_stocking_input_bus.json new file mode 100644 index 0000000..225fcda --- /dev/null +++ b/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_stocking_input_bus.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=down": { + "model": "gtmutils:block/machine/me_enlarged_stocking_input_bus", + "x": 90 + }, + "facing=east": { + "model": "gtmutils:block/machine/me_enlarged_stocking_input_bus", + "y": 90 + }, + "facing=north": { + "model": "gtmutils:block/machine/me_enlarged_stocking_input_bus" + }, + "facing=south": { + "model": "gtmutils:block/machine/me_enlarged_stocking_input_bus", + "y": 180 + }, + "facing=up": { + "gtceu:z": 180, + "model": "gtmutils:block/machine/me_enlarged_stocking_input_bus", + "x": 270 + }, + "facing=west": { + "model": "gtmutils:block/machine/me_enlarged_stocking_input_bus", + "y": 270 + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_stocking_input_hatch.json b/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_stocking_input_hatch.json new file mode 100644 index 0000000..b5a8fb4 --- /dev/null +++ b/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_stocking_input_hatch.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=down": { + "model": "gtmutils:block/machine/me_enlarged_stocking_input_hatch", + "x": 90 + }, + "facing=east": { + "model": "gtmutils:block/machine/me_enlarged_stocking_input_hatch", + "y": 90 + }, + "facing=north": { + "model": "gtmutils:block/machine/me_enlarged_stocking_input_hatch" + }, + "facing=south": { + "model": "gtmutils:block/machine/me_enlarged_stocking_input_hatch", + "y": 180 + }, + "facing=up": { + "gtceu:z": 180, + "model": "gtmutils:block/machine/me_enlarged_stocking_input_hatch", + "x": 270 + }, + "facing=west": { + "model": "gtmutils:block/machine/me_enlarged_stocking_input_hatch", + "y": 270 + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_tag_stocking_input_bus.json b/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_tag_stocking_input_bus.json new file mode 100644 index 0000000..4e60522 --- /dev/null +++ b/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_tag_stocking_input_bus.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=down": { + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_bus", + "x": 90 + }, + "facing=east": { + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_bus", + "y": 90 + }, + "facing=north": { + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_bus" + }, + "facing=south": { + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_bus", + "y": 180 + }, + "facing=up": { + "gtceu:z": 180, + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_bus", + "x": 270 + }, + "facing=west": { + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_bus", + "y": 270 + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_tag_stocking_input_hatch.json b/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_tag_stocking_input_hatch.json new file mode 100644 index 0000000..203ebd5 --- /dev/null +++ b/src/generated/resources/assets/gtmutils/blockstates/me_enlarged_tag_stocking_input_hatch.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=down": { + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_hatch", + "x": 90 + }, + "facing=east": { + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_hatch", + "y": 90 + }, + "facing=north": { + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_hatch" + }, + "facing=south": { + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_hatch", + "y": 180 + }, + "facing=up": { + "gtceu:z": 180, + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_hatch", + "x": 270 + }, + "facing=west": { + "model": "gtmutils:block/machine/me_enlarged_tag_stocking_input_hatch", + "y": 270 + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/blockstates/me_tag_stocking_input_bus.json b/src/generated/resources/assets/gtmutils/blockstates/me_tag_stocking_input_bus.json new file mode 100644 index 0000000..84df94a --- /dev/null +++ b/src/generated/resources/assets/gtmutils/blockstates/me_tag_stocking_input_bus.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=down": { + "model": "gtmutils:block/machine/me_tag_stocking_input_bus", + "x": 90 + }, + "facing=east": { + "model": "gtmutils:block/machine/me_tag_stocking_input_bus", + "y": 90 + }, + "facing=north": { + "model": "gtmutils:block/machine/me_tag_stocking_input_bus" + }, + "facing=south": { + "model": "gtmutils:block/machine/me_tag_stocking_input_bus", + "y": 180 + }, + "facing=up": { + "gtceu:z": 180, + "model": "gtmutils:block/machine/me_tag_stocking_input_bus", + "x": 270 + }, + "facing=west": { + "model": "gtmutils:block/machine/me_tag_stocking_input_bus", + "y": 270 + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/blockstates/me_tag_stocking_input_hatch.json b/src/generated/resources/assets/gtmutils/blockstates/me_tag_stocking_input_hatch.json new file mode 100644 index 0000000..a08a742 --- /dev/null +++ b/src/generated/resources/assets/gtmutils/blockstates/me_tag_stocking_input_hatch.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=down": { + "model": "gtmutils:block/machine/me_tag_stocking_input_hatch", + "x": 90 + }, + "facing=east": { + "model": "gtmutils:block/machine/me_tag_stocking_input_hatch", + "y": 90 + }, + "facing=north": { + "model": "gtmutils:block/machine/me_tag_stocking_input_hatch" + }, + "facing=south": { + "model": "gtmutils:block/machine/me_tag_stocking_input_hatch", + "y": 180 + }, + "facing=up": { + "gtceu:z": 180, + "model": "gtmutils:block/machine/me_tag_stocking_input_hatch", + "x": 270 + }, + "facing=west": { + "model": "gtmutils:block/machine/me_tag_stocking_input_hatch", + "y": 270 + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/lang/en_ud.json b/src/generated/resources/assets/gtmutils/lang/en_ud.json index 7bcb3db..aba072e 100644 --- a/src/generated/resources/assets/gtmutils/lang/en_ud.json +++ b/src/generated/resources/assets/gtmutils/lang/en_ud.json @@ -1,4 +1,10 @@ { + "block.gtmutils.enlarged_stocking_bus.desc.0": "ʞɹoʍʇǝu ƎW ǝɥʇ ɯoɹɟ ʎןʇɔǝɹıp sɯǝʇı sǝʌǝıɹʇǝᴚ", + "block.gtmutils.enlarged_stocking_bus.desc.1": "ʇsıן buıʞɔoʇs ǝbɹɐן-ɐɹʇxƎ", + "block.gtmutils.enlarged_stocking_bus.desc.2": "sɯǝʇı ƎW ʇuɐpunqɐ ʇsoɯ ǝɥʇ ɥʇıʍ ʇsıן ǝɥʇ ןןıɟ uɐɔ ןןnԀ-oʇnⱯ", + "block.gtmutils.enlarged_stocking_hatch.desc.0": "ʞɹoʍʇǝu ƎW ǝɥʇ ɯoɹɟ ʎןʇɔǝɹıp spınןɟ sǝʌǝıɹʇǝᴚ", + "block.gtmutils.enlarged_stocking_hatch.desc.1": "ʇsıן buıʞɔoʇs ǝbɹɐן-ɐɹʇxƎ", + "block.gtmutils.enlarged_stocking_hatch.desc.2": "spınןɟ ƎW ʇuɐpunqɐ ʇsoɯ ǝɥʇ ɥʇıʍ ʇsıן ǝɥʇ ןןıɟ uɐɔ ןןnԀ-oʇnⱯ", "block.gtmutils.ev_64a_energy_converter": "ɹǝʇɹǝʌuoƆ ʎbɹǝuƎ ɹ§Ɐǝ§ㄣ9 ɹ§ΛƎϛ§", "block.gtmutils.ev_auto_charger_4x": "ɹǝbɹɐɥƆ oqɹn⟘ oʇnⱯ xㄣ ɹ§ǝbɐʇןoΛ ǝɯǝɹʇxƎϛ§", "block.gtmutils.expanded_me_pattern_buffer": "ɹǝɟɟnᗺ uɹǝʇʇɐԀ ƎW pǝpuɐdxƎ", @@ -12,6 +18,12 @@ "block.gtmutils.lv_64a_energy_converter": "ɹǝʇɹǝʌuoƆ ʎbɹǝuƎ ɹ§Ɐǝ§ㄣ9 ɹ§ΛꞀㄥ§", "block.gtmutils.lv_auto_charger_4x": "ɹǝbɹɐɥƆ oqɹn⟘ oʇnⱯ xㄣ ɹ§ǝbɐʇןoΛ ʍoꞀㄥ§", "block.gtmutils.max_64a_energy_converter": "ɹǝʇɹǝʌuoƆ ʎbɹǝuƎ ɹ§Ɐǝ§ㄣ9 ɹ§XⱯWן§ɔ§", + "block.gtmutils.me_enlarged_stocking_input_bus": "snᗺ ʇnduI buıʞɔoʇS pǝbɹɐןuƎ ƎW", + "block.gtmutils.me_enlarged_stocking_input_hatch": "ɥɔʇɐH ʇnduI buıʞɔoʇS pǝbɹɐןuƎ ƎW", + "block.gtmutils.me_enlarged_tag_stocking_input_bus": "snᗺ ʇnduI buıʞɔoʇS bɐ⟘ pǝbɹɐןuƎ ƎW", + "block.gtmutils.me_enlarged_tag_stocking_input_hatch": "ɥɔʇɐH ʇnduI buıʞɔoʇS bɐ⟘ pǝbɹɐןuƎ ƎW", + "block.gtmutils.me_tag_stocking_input_bus": "snᗺ ʇnduI buıʞɔoʇS bɐ⟘ ƎW", + "block.gtmutils.me_tag_stocking_input_hatch": "ɥɔʇɐH ʇnduI buıʞɔoʇS bɐ⟘ ƎW", "block.gtmutils.mv_64a_energy_converter": "ɹǝʇɹǝʌuoƆ ʎbɹǝuƎ ɹ§Ɐǝ§ㄣ9 ɹ§ΛWq§", "block.gtmutils.mv_auto_charger_4x": "ɹǝbɹɐɥƆ oqɹn⟘ oʇnⱯ xㄣ ɹ§ǝbɐʇןoΛ ɯnıpǝWq§", "block.gtmutils.opv_64a_energy_converter": "ɹǝʇɹǝʌuoƆ ʎbɹǝuƎ ɹ§Ɐǝ§ㄣ9 ɹ§ΛdOן§6§", @@ -21,6 +33,18 @@ "block.gtmutils.pattern_buffer_proxy.desc.0": "˙ɟ§ɹǝɟɟnᗺ uɹǝʇʇɐԀ ƎW pǝpuɐdxƎ9§ ɹɐןnbuıs ɐ oʇ sǝuıɥɔɐɯ ʎuɐɯ buıʞuıן sʍoןןⱯɟ§", "block.gtmutils.pterb_machine": "ɹǝɯɹoɟsuɐɹ⟘ ǝʌıʇɔⱯ ssǝןǝɹıM", "block.gtmutils.sterile_cleaning_maintenance_hatch": "ɥɔʇɐH ǝɔuɐuǝʇuıɐW buıuɐǝןƆ ǝןıɹǝʇS", + "block.gtmutils.tag_enlarged_stocking_bus.desc.0": "ʞɹoʍʇǝu ƎW ǝɥʇ ɯoɹɟ ʎןʇɔǝɹıp sɯǝʇı sǝʌǝıɹʇǝᴚ", + "block.gtmutils.tag_enlarged_stocking_bus.desc.1": "buıʞɔoʇs pǝsɐq-bɐʇ ǝbɹɐן-ɐɹʇxƎ", + "block.gtmutils.tag_enlarged_stocking_bus.desc.2": "ʇsıן ɯǝʇı pǝʞɔoʇs ǝɥʇ pןınq oʇ sbɐʇ ʇsıןʞɔɐןq/ʇsıןǝʇıɥʍ sǝs∩", + "block.gtmutils.tag_enlarged_stocking_hatch.desc.0": "ʞɹoʍʇǝu ƎW ǝɥʇ ɯoɹɟ ʎןʇɔǝɹıp spınןɟ sǝʌǝıɹʇǝᴚ", + "block.gtmutils.tag_enlarged_stocking_hatch.desc.1": "buıʞɔoʇs pǝsɐq-bɐʇ ǝbɹɐן-ɐɹʇxƎ", + "block.gtmutils.tag_enlarged_stocking_hatch.desc.2": "ʇsıן pınןɟ pǝʞɔoʇs ǝɥʇ pןınq oʇ sbɐʇ ʇsıןʞɔɐןq/ʇsıןǝʇıɥʍ sǝs∩", + "block.gtmutils.tag_stocking_bus.desc.0": "ʞɹoʍʇǝu ƎW ǝɥʇ ɯoɹɟ ʎןʇɔǝɹıp sɯǝʇı sǝʌǝıɹʇǝᴚ", + "block.gtmutils.tag_stocking_bus.desc.1": ")ʇsıןʞɔɐןq/ʇsıןǝʇıɥʍ( sɹǝʇןıɟ bɐʇ ɯoɹɟ ʇןınq sı ʇsıן ʞɔoʇS", + "block.gtmutils.tag_stocking_bus.desc.2": "ʎןןɐɔıʇɐɯoʇnɐ ʞɔoʇs uı sɯǝʇı buıɥɔʇɐɯ ǝɥʇ sdǝǝʞ", + "block.gtmutils.tag_stocking_hatch.desc.0": "ʞɹoʍʇǝu ƎW ǝɥʇ ɯoɹɟ ʎןʇɔǝɹıp spınןɟ sǝʌǝıɹʇǝᴚ", + "block.gtmutils.tag_stocking_hatch.desc.1": ")ʇsıןʞɔɐןq/ʇsıןǝʇıɥʍ( sɹǝʇןıɟ bɐʇ ɯoɹɟ ʇןınq sı ʇsıן ʞɔoʇS", + "block.gtmutils.tag_stocking_hatch.desc.2": "ʎןןɐɔıʇɐɯoʇnɐ ʞɔoʇs uı spınןɟ buıɥɔʇɐɯ ǝɥʇ sdǝǝʞ", "block.gtmutils.uev_64a_energy_converter": "ɹǝʇɹǝʌuoƆ ʎbɹǝuƎ ɹ§Ɐǝ§ㄣ9 ɹ§ΛƎ∩ɐ§", "block.gtmutils.uev_auto_charger_4x": "ɹǝbɹɐɥƆ oqɹn⟘ oʇnⱯ xㄣ ɹ§ǝbɐʇןoΛ ǝʌıssǝɔxƎ ɐɹʇן∩ɐ§", "block.gtmutils.uhv_64a_energy_converter": "ɹǝʇɹǝʌuoƆ ʎbɹǝuƎ ɹ§Ɐǝ§ㄣ9 ɹ§ΛH∩ㄣ§", @@ -44,6 +68,8 @@ "config.gtmutils.option.customLuVToolsEnabled": "pǝןqɐuƎsןoo⟘ΛnꞀɯoʇsnɔ", "config.gtmutils.option.customMVToolsEnabled": "pǝןqɐuƎsןoo⟘ΛWɯoʇsnɔ", "config.gtmutils.option.customZPMToolsEnabled": "pǝןqɐuƎsןoo⟘WԀZɯoʇsnɔ", + "config.gtmutils.option.enlargedStockingEnabled": "pǝןqɐuƎbuıʞɔoʇSpǝbɹɐןuǝ", + "config.gtmutils.option.enlargedStockingSizeRows": "sʍoᴚǝzıSbuıʞɔoʇSpǝbɹɐןuǝ", "config.gtmutils.option.expandedBuffersEnabled": "pǝןqɐuƎsɹǝɟɟnᗺpǝpuɐdxǝ", "config.gtmutils.option.features": "sǝɹnʇɐǝɟ", "config.gtmutils.option.omnibreakerEnabled": "pǝןqɐuƎɹǝʞɐǝɹqıuɯo", @@ -53,6 +79,9 @@ "config.gtmutils.option.pterbCoolantIOMultiplier": "ɹǝıןdıʇןnWOIʇuɐןooƆqɹǝʇd", "config.gtmutils.option.pterbEnabled": "pǝןqɐuƎqɹǝʇd", "config.gtmutils.option.sterileHatchEnabled": "pǝןqɐuƎɥɔʇɐHǝןıɹǝʇs", + "config.gtmutils.option.tagStockingEnabled": "pǝןqɐuƎbuıʞɔoʇSbɐʇ", + "config.jade.plugin_gtmutils.enlarged_me_stocking_bus_info": "oɟuI snᗺ buıʞɔoʇS ƎW pǝbɹɐןuƎ", + "config.jade.plugin_gtmutils.enlarged_me_stocking_bus_info.desc": "ǝpɐſ uı sʇuǝʇuoɔ snq buıʞɔoʇs ƎW pǝbɹɐןuǝ ɟo ʇoɥsdɐus ɐ sʍoɥS", "config.jade.plugin_gtmutils.pterb_info": "oɟuI ⟘ⱯM", "gtceu.placeholder_info.watfrequency.0": "˙ɹǝɯɹoɟsuɐɹ⟘ ǝʌıʇɔⱯ ssǝןǝɹıM ɐ ʎq pǝsn ʎɔuǝnbǝɹɟ ʇuǝɹɹnɔ ǝɥʇ suɹnʇǝᴚ", "gtceu.placeholder_info.watfrequency.1": ":ǝbɐs∩", diff --git a/src/generated/resources/assets/gtmutils/lang/en_us.json b/src/generated/resources/assets/gtmutils/lang/en_us.json index bd3556d..cb3697f 100644 --- a/src/generated/resources/assets/gtmutils/lang/en_us.json +++ b/src/generated/resources/assets/gtmutils/lang/en_us.json @@ -1,4 +1,10 @@ { + "block.gtmutils.enlarged_stocking_bus.desc.0": "Retrieves items directly from the ME network", + "block.gtmutils.enlarged_stocking_bus.desc.1": "Extra-large stocking list", + "block.gtmutils.enlarged_stocking_bus.desc.2": "Auto-Pull can fill the list with the most abundant ME items", + "block.gtmutils.enlarged_stocking_hatch.desc.0": "Retrieves fluids directly from the ME network", + "block.gtmutils.enlarged_stocking_hatch.desc.1": "Extra-large stocking list", + "block.gtmutils.enlarged_stocking_hatch.desc.2": "Auto-Pull can fill the list with the most abundant ME fluids", "block.gtmutils.ev_64a_energy_converter": "§5EV§r 64§eA§r Energy Converter", "block.gtmutils.ev_auto_charger_4x": "§5Extreme Voltage§r 4x Auto Turbo Charger", "block.gtmutils.expanded_me_pattern_buffer": "Expanded ME Pattern Buffer", @@ -12,6 +18,12 @@ "block.gtmutils.lv_64a_energy_converter": "§7LV§r 64§eA§r Energy Converter", "block.gtmutils.lv_auto_charger_4x": "§7Low Voltage§r 4x Auto Turbo Charger", "block.gtmutils.max_64a_energy_converter": "§c§lMAX§r 64§eA§r Energy Converter", + "block.gtmutils.me_enlarged_stocking_input_bus": "ME Enlarged Stocking Input Bus", + "block.gtmutils.me_enlarged_stocking_input_hatch": "ME Enlarged Stocking Input Hatch", + "block.gtmutils.me_enlarged_tag_stocking_input_bus": "ME Enlarged Tag Stocking Input Bus", + "block.gtmutils.me_enlarged_tag_stocking_input_hatch": "ME Enlarged Tag Stocking Input Hatch", + "block.gtmutils.me_tag_stocking_input_bus": "ME Tag Stocking Input Bus", + "block.gtmutils.me_tag_stocking_input_hatch": "ME Tag Stocking Input Hatch", "block.gtmutils.mv_64a_energy_converter": "§bMV§r 64§eA§r Energy Converter", "block.gtmutils.mv_auto_charger_4x": "§bMedium Voltage§r 4x Auto Turbo Charger", "block.gtmutils.opv_64a_energy_converter": "§9§lOpV§r 64§eA§r Energy Converter", @@ -21,6 +33,18 @@ "block.gtmutils.pattern_buffer_proxy.desc.0": "§fAllows linking many machines to a singular §6Expanded ME Pattern Buffer§f.", "block.gtmutils.pterb_machine": "Wireless Active Transformer", "block.gtmutils.sterile_cleaning_maintenance_hatch": "Sterile Cleaning Maintenance Hatch", + "block.gtmutils.tag_enlarged_stocking_bus.desc.0": "Retrieves items directly from the ME network", + "block.gtmutils.tag_enlarged_stocking_bus.desc.1": "Extra-large tag-based stocking", + "block.gtmutils.tag_enlarged_stocking_bus.desc.2": "Uses whitelist/blacklist tags to build the stocked item list", + "block.gtmutils.tag_enlarged_stocking_hatch.desc.0": "Retrieves fluids directly from the ME network", + "block.gtmutils.tag_enlarged_stocking_hatch.desc.1": "Extra-large tag-based stocking", + "block.gtmutils.tag_enlarged_stocking_hatch.desc.2": "Uses whitelist/blacklist tags to build the stocked fluid list", + "block.gtmutils.tag_stocking_bus.desc.0": "Retrieves items directly from the ME network", + "block.gtmutils.tag_stocking_bus.desc.1": "Stock list is built from tag filters (whitelist/blacklist)", + "block.gtmutils.tag_stocking_bus.desc.2": "Keeps the matching items in stock automatically", + "block.gtmutils.tag_stocking_hatch.desc.0": "Retrieves fluids directly from the ME network", + "block.gtmutils.tag_stocking_hatch.desc.1": "Stock list is built from tag filters (whitelist/blacklist)", + "block.gtmutils.tag_stocking_hatch.desc.2": "Keeps the matching fluids in stock automatically", "block.gtmutils.uev_64a_energy_converter": "§aUEV§r 64§eA§r Energy Converter", "block.gtmutils.uev_auto_charger_4x": "§aUltra Excessive Voltage§r 4x Auto Turbo Charger", "block.gtmutils.uhv_64a_energy_converter": "§4UHV§r 64§eA§r Energy Converter", @@ -44,6 +68,8 @@ "config.gtmutils.option.customLuVToolsEnabled": "customLuVToolsEnabled", "config.gtmutils.option.customMVToolsEnabled": "customMVToolsEnabled", "config.gtmutils.option.customZPMToolsEnabled": "customZPMToolsEnabled", + "config.gtmutils.option.enlargedStockingEnabled": "enlargedStockingEnabled", + "config.gtmutils.option.enlargedStockingSizeRows": "enlargedStockingSizeRows", "config.gtmutils.option.expandedBuffersEnabled": "expandedBuffersEnabled", "config.gtmutils.option.features": "features", "config.gtmutils.option.omnibreakerEnabled": "omnibreakerEnabled", @@ -53,6 +79,9 @@ "config.gtmutils.option.pterbCoolantIOMultiplier": "pterbCoolantIOMultiplier", "config.gtmutils.option.pterbEnabled": "pterbEnabled", "config.gtmutils.option.sterileHatchEnabled": "sterileHatchEnabled", + "config.gtmutils.option.tagStockingEnabled": "tagStockingEnabled", + "config.jade.plugin_gtmutils.enlarged_me_stocking_bus_info": "Enlarged ME Stocking Bus Info", + "config.jade.plugin_gtmutils.enlarged_me_stocking_bus_info.desc": "Shows a snapshot of enlarged ME stocking bus contents in Jade", "config.jade.plugin_gtmutils.pterb_info": "WAT Info", "gtceu.placeholder_info.watfrequency.0": "Returns the current frequency used by a Wireless Active Transformer.", "gtceu.placeholder_info.watfrequency.1": "Usage:", diff --git a/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_stocking_input_bus.json b/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_stocking_input_bus.json new file mode 100644 index 0000000..f301ebd --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_stocking_input_bus.json @@ -0,0 +1,56 @@ +{ + "parent": "minecraft:block/block", + "loader": "gtceu:machine", + "machine": "gtmutils:me_enlarged_stocking_input_bus", + "replaceable_textures": [ + "bottom", + "top", + "side" + ], + "variants": { + "is_formed=false,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_input_bus", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=false,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_input_bus", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=true,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_input_bus", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=true,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_input_bus", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_stocking_input_hatch.json b/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_stocking_input_hatch.json new file mode 100644 index 0000000..77ca94b --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_stocking_input_hatch.json @@ -0,0 +1,56 @@ +{ + "parent": "minecraft:block/block", + "loader": "gtceu:machine", + "machine": "gtmutils:me_enlarged_stocking_input_hatch", + "replaceable_textures": [ + "bottom", + "top", + "side" + ], + "variants": { + "is_formed=false,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_input_hatch", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=false,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_input_hatch", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=true,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_input_hatch", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=true,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_input_hatch", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_tag_stocking_input_bus.json b/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_tag_stocking_input_bus.json new file mode 100644 index 0000000..a3ca0b5 --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_tag_stocking_input_bus.json @@ -0,0 +1,56 @@ +{ + "parent": "minecraft:block/block", + "loader": "gtceu:machine", + "machine": "gtmutils:me_enlarged_tag_stocking_input_bus", + "replaceable_textures": [ + "bottom", + "top", + "side" + ], + "variants": { + "is_formed=false,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/zpm/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_bus", + "side": "gtceu:block/casings/voltage/zpm/side", + "top": "gtceu:block/casings/voltage/zpm/top" + } + } + }, + "is_formed=false,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/zpm/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_bus", + "side": "gtceu:block/casings/voltage/zpm/side", + "top": "gtceu:block/casings/voltage/zpm/top" + } + } + }, + "is_formed=true,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/zpm/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_bus", + "side": "gtceu:block/casings/voltage/zpm/side", + "top": "gtceu:block/casings/voltage/zpm/top" + } + } + }, + "is_formed=true,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/zpm/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_bus", + "side": "gtceu:block/casings/voltage/zpm/side", + "top": "gtceu:block/casings/voltage/zpm/top" + } + } + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_tag_stocking_input_hatch.json b/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_tag_stocking_input_hatch.json new file mode 100644 index 0000000..135e66a --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/block/machine/me_enlarged_tag_stocking_input_hatch.json @@ -0,0 +1,56 @@ +{ + "parent": "minecraft:block/block", + "loader": "gtceu:machine", + "machine": "gtmutils:me_enlarged_tag_stocking_input_hatch", + "replaceable_textures": [ + "bottom", + "top", + "side" + ], + "variants": { + "is_formed=false,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/zpm/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_hatch", + "side": "gtceu:block/casings/voltage/zpm/side", + "top": "gtceu:block/casings/voltage/zpm/top" + } + } + }, + "is_formed=false,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/zpm/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_hatch", + "side": "gtceu:block/casings/voltage/zpm/side", + "top": "gtceu:block/casings/voltage/zpm/top" + } + } + }, + "is_formed=true,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/zpm/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_hatch", + "side": "gtceu:block/casings/voltage/zpm/side", + "top": "gtceu:block/casings/voltage/zpm/top" + } + } + }, + "is_formed=true,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/zpm/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_hatch", + "side": "gtceu:block/casings/voltage/zpm/side", + "top": "gtceu:block/casings/voltage/zpm/top" + } + } + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/block/machine/me_tag_stocking_input_bus.json b/src/generated/resources/assets/gtmutils/models/block/machine/me_tag_stocking_input_bus.json new file mode 100644 index 0000000..af24d99 --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/block/machine/me_tag_stocking_input_bus.json @@ -0,0 +1,56 @@ +{ + "parent": "minecraft:block/block", + "loader": "gtceu:machine", + "machine": "gtmutils:me_tag_stocking_input_bus", + "replaceable_textures": [ + "bottom", + "top", + "side" + ], + "variants": { + "is_formed=false,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_bus", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=false,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_bus", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=true,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_bus", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=true,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_bus", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/block/machine/me_tag_stocking_input_hatch.json b/src/generated/resources/assets/gtmutils/models/block/machine/me_tag_stocking_input_hatch.json new file mode 100644 index 0000000..0733cd8 --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/block/machine/me_tag_stocking_input_hatch.json @@ -0,0 +1,56 @@ +{ + "parent": "minecraft:block/block", + "loader": "gtceu:machine", + "machine": "gtmutils:me_tag_stocking_input_hatch", + "replaceable_textures": [ + "bottom", + "top", + "side" + ], + "variants": { + "is_formed=false,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_hatch", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=false,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_hatch", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=true,is_painted=false": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_hatch", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + }, + "is_formed=true,is_painted=true": { + "model": { + "parent": "gtceu:block/machine/template/part/hatch_machine_color_ring", + "textures": { + "bottom": "gtceu:block/casings/voltage/luv/bottom", + "overlay": "gtceu:block/overlay/appeng/me_tag_hatch", + "side": "gtceu:block/casings/voltage/luv/side", + "top": "gtceu:block/casings/voltage/luv/top" + } + } + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/item/me_enlarged_stocking_input_bus.json b/src/generated/resources/assets/gtmutils/models/item/me_enlarged_stocking_input_bus.json new file mode 100644 index 0000000..06f35eb --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/item/me_enlarged_stocking_input_bus.json @@ -0,0 +1,3 @@ +{ + "parent": "gtmutils:block/machine/me_enlarged_stocking_input_bus" +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/item/me_enlarged_stocking_input_hatch.json b/src/generated/resources/assets/gtmutils/models/item/me_enlarged_stocking_input_hatch.json new file mode 100644 index 0000000..a1fbbcd --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/item/me_enlarged_stocking_input_hatch.json @@ -0,0 +1,3 @@ +{ + "parent": "gtmutils:block/machine/me_enlarged_stocking_input_hatch" +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/item/me_enlarged_tag_stocking_input_bus.json b/src/generated/resources/assets/gtmutils/models/item/me_enlarged_tag_stocking_input_bus.json new file mode 100644 index 0000000..f58c96f --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/item/me_enlarged_tag_stocking_input_bus.json @@ -0,0 +1,3 @@ +{ + "parent": "gtmutils:block/machine/me_enlarged_tag_stocking_input_bus" +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/item/me_enlarged_tag_stocking_input_hatch.json b/src/generated/resources/assets/gtmutils/models/item/me_enlarged_tag_stocking_input_hatch.json new file mode 100644 index 0000000..dd2a20e --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/item/me_enlarged_tag_stocking_input_hatch.json @@ -0,0 +1,3 @@ +{ + "parent": "gtmutils:block/machine/me_enlarged_tag_stocking_input_hatch" +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/item/me_tag_stocking_input_bus.json b/src/generated/resources/assets/gtmutils/models/item/me_tag_stocking_input_bus.json new file mode 100644 index 0000000..2635917 --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/item/me_tag_stocking_input_bus.json @@ -0,0 +1,3 @@ +{ + "parent": "gtmutils:block/machine/me_tag_stocking_input_bus" +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtmutils/models/item/me_tag_stocking_input_hatch.json b/src/generated/resources/assets/gtmutils/models/item/me_tag_stocking_input_hatch.json new file mode 100644 index 0000000..3cbb5e8 --- /dev/null +++ b/src/generated/resources/assets/gtmutils/models/item/me_tag_stocking_input_hatch.json @@ -0,0 +1,3 @@ +{ + "parent": "gtmutils:block/machine/me_tag_stocking_input_hatch" +} \ No newline at end of file diff --git a/src/main/java/net/neganote/gtutilities/common/gui/widgets/MultilineTextField.java b/src/main/java/net/neganote/gtutilities/common/gui/widgets/MultilineTextField.java new file mode 100644 index 0000000..9701092 --- /dev/null +++ b/src/main/java/net/neganote/gtutilities/common/gui/widgets/MultilineTextField.java @@ -0,0 +1,636 @@ +package net.neganote.gtutilities.common.gui.widgets; + +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; +import com.lowdragmc.lowdraglib.utils.Position; +import com.lowdragmc.lowdraglib.utils.Size; + +import net.minecraft.Util; +import net.minecraft.client.gui.components.Whence; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class MultilineTextField extends WidgetGroup { + + public static final int DEFAULT_MAX_LENGTH = Integer.MAX_VALUE; + private static final int ACTION_SET_TEXT = 1; + + private static final int KEY_ESCAPE = 256; + private static final int KEY_ENTER = 257; + private static final int KEY_KP_ENTER = 335; + private static final int KEY_TAB = 258; + + private static MultilineTextField focusedField = null; + + private final Supplier textSupplier; + private final Consumer textConsumer; + private final Component placeholder; + + private int maxLength = DEFAULT_MAX_LENGTH; + + @OnlyIn(Dist.CLIENT) + private net.minecraft.client.gui.Font font; + @OnlyIn(Dist.CLIENT) + private CachedTextField textField; + + private double scrollAmount = 0.0; + private boolean dragging = false; + private boolean hasFocus = false; + + private String lastSent = ""; + + public MultilineTextField( + int x, int y, int width, int height, + Supplier textSupplier, + Consumer textConsumer) { + this(x, y, width, height, textSupplier, textConsumer, Component.empty()); + } + + public MultilineTextField( + int x, int y, int width, int height, + Supplier textSupplier, + Consumer textConsumer, + Component placeholder) { + super(new Position(x, y), new Size(width, height)); + this.textSupplier = textSupplier; + this.textConsumer = textConsumer; + this.placeholder = placeholder == null ? Component.empty() : placeholder; + + String init = safe(textSupplier.get()); + this.lastSent = init; + + if (isRemote()) { + initClient(width); + setValueClient(init); + } + } + + public MultilineTextField setMaxLength(int maxLength) { + this.maxLength = Math.max(0, maxLength); + if (isRemote() && textField != null) { + textField.setCharacterLimit(this.maxLength); + // przytnij aktualną wartość jeśli trzeba + String v = safe(textField.value()); + if (v.length() > this.maxLength) { + setValueClient(v.substring(0, this.maxLength)); + sendToServer(true); + } + } + return this; + } + + public String getValue() { + if (isRemote() && textField != null) return safe(textField.value()); + return safe(textSupplier.get()); + } + + public void setValue(String v) { + v = clampToMax(safe(v)); + textConsumer.accept(v); + if (isRemote()) { + setValueClient(v); + } + } + + public void setDirectly(String newText) { + setValue(newText); + sendToServer(true); + } + + // ====== networking ====== + private void sendToServer() { + sendToServer(false); + } + + private void sendToServer(boolean force) { + String v = clampToMax(getValue()); + textConsumer.accept(v); + + if ((force || !v.equals(lastSent)) && isRemote()) { + writeClientAction(ACTION_SET_TEXT, buf -> buf.writeUtf(v, maxLength)); + lastSent = v; + } + } + + @Override + public void handleClientAction(int id, FriendlyByteBuf buffer) { + if (id == ACTION_SET_TEXT) { + String v = buffer.readUtf(maxLength); + textConsumer.accept(v); + return; + } + super.handleClientAction(id, buffer); + } + + @Override + public void updateScreen() { + super.updateScreen(); + + String fromMachine = clampToMax(safe(textSupplier.get())); + + if (isRemote() && textField != null) { + if (!hasFocus && !fromMachine.equals(textField.value())) { + setValueClient(fromMachine); + lastSent = fromMachine; + } + } + } + + // ====== client init ====== + @OnlyIn(Dist.CLIENT) + private void initClient(int width) { + this.font = net.minecraft.client.Minecraft.getInstance().font; + + this.textField = new CachedTextField(font, width - 4); + this.textField.setCharacterLimit(this.maxLength); + + this.textField.setCursorListener(() -> { + clampScroll(); + ensureCursorVisible(); + }); + + this.textField.setValueListener((str) -> { + clampScroll(); + ensureCursorVisible(); + }); + } + + @OnlyIn(Dist.CLIENT) + private void setValueClient(String v) { + if (textField == null) return; + textField.setValue(v); + clampScroll(); + ensureCursorVisible(); + } + + // ====== focus ====== + @OnlyIn(Dist.CLIENT) + private void takeFocus() { + if (focusedField != this) { + if (focusedField != null) focusedField.loseFocus(); + focusedField = this; + } + hasFocus = true; + } + + @OnlyIn(Dist.CLIENT) + private void loseFocus() { + hasFocus = false; + dragging = false; + if (focusedField == this) focusedField = null; + } + + // ====== scrolling helpers ====== + @OnlyIn(Dist.CLIENT) + private double getMaxScroll() { + if (textField == null || font == null) return 0.0; + int textH = textField.lineCount() * font.lineHeight; + int viewH = getSize().height - 4; + return (double) Math.max(textH - viewH, 0); + } + + @OnlyIn(Dist.CLIENT) + private void setScrollAmount(double a) { + scrollAmount = Mth.clamp(a, 0.0, getMaxScroll()); + } + + @OnlyIn(Dist.CLIENT) + private void clampScroll() { + setScrollAmount(scrollAmount); + } + + @OnlyIn(Dist.CLIENT) + private void ensureCursorVisible() { + if (textField == null || font == null) return; + + int viewH = getSize().height - 4; + int caretLine = textField.lineAtCursor(); + int caretY = caretLine * font.lineHeight; + + double top = scrollAmount; + double bottom = scrollAmount + viewH - font.lineHeight; + + if ((double) caretY < top) { + setScrollAmount((double) caretY); + } else if ((double) caretY > bottom) { + setScrollAmount((double) caretY - (double) (viewH - font.lineHeight)); + } + } + + @OnlyIn(Dist.CLIENT) + private void moveCursorToMouse(double mx, double my) { + if (textField == null) return; + double relX = mx - (getPosition().x + 2); + double relY = my - (getPosition().y + 2) + scrollAmount; + textField.seekCursorToPoint(relX, relY); + ensureCursorVisible(); + } + + @OnlyIn(Dist.CLIENT) + private boolean blink() { + return (Util.getMillis() / 500L) % 2L == 0L; + } + + // ====== clipboard + shortcuts ====== + @OnlyIn(Dist.CLIENT) + public static boolean isCtrlDown() { + return net.minecraft.client.gui.screens.Screen.hasControlDown(); + } + + @OnlyIn(Dist.CLIENT) + private void clipboardSet(String s) { + net.minecraft.client.Minecraft.getInstance().keyboardHandler.setClipboard(s); + } + + @OnlyIn(Dist.CLIENT) + private String clipboardGet() { + return net.minecraft.client.Minecraft.getInstance().keyboardHandler.getClipboard(); + } + + @OnlyIn(Dist.CLIENT) + private void selectAll() { + if (textField == null) return; + String v = safe(textField.value()); + int len = v.length(); + + textField.seekCursor(Whence.ABSOLUTE, 0); + textField.setSelecting(true); + textField.seekCursor(Whence.ABSOLUTE, len); + } + + @OnlyIn(Dist.CLIENT) + private void copySelection() { + if (textField == null) return; + if (!textField.hasSelection()) return; + + Selection sel = textField.selection(); + int a = Math.min(sel.begin, sel.end); + int b = Math.max(sel.begin, sel.end); + + String v = safe(textField.value()); + a = Mth.clamp(a, 0, v.length()); + b = Mth.clamp(b, 0, v.length()); + + if (a < b) clipboardSet(v.substring(a, b)); + } + + @OnlyIn(Dist.CLIENT) + private void cutSelection() { + if (textField == null) return; + if (!textField.hasSelection()) return; + + copySelection(); + + Selection sel = textField.selection(); + int a = Math.min(sel.begin, sel.end); + int b = Math.max(sel.begin, sel.end); + + String v = safe(textField.value()); + a = Mth.clamp(a, 0, v.length()); + b = Mth.clamp(b, 0, v.length()); + + if (a < b) { + String nv = v.substring(0, a) + v.substring(b); + nv = clampToMax(nv); + textField.setValue(nv); + textField.seekCursor(Whence.ABSOLUTE, a); + textField.setSelecting(false); + } + } + + @OnlyIn(Dist.CLIENT) + private void pasteClipboard() { + if (textField == null) return; + String clip = safe(clipboardGet()); + if (clip.isEmpty()) return; + + // kontrolujemy maxLength “po fakcie” + textField.insertText(clip); + String v = clampToMax(safe(textField.value())); + if (!v.equals(textField.value())) { + textField.setValue(v); + } + } + + // ====== input ====== + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isActive() || button != 0) return false; + + if (!isMouseOverElement(mouseX, mouseY)) { + if (hasFocus) loseFocus(); + return false; + } + + takeFocus(); + dragging = true; + + if (textField != null) { + if (!net.minecraft.client.gui.screens.Screen.hasShiftDown()) { + textField.setSelecting(false); + } + moveCursorToMouse(mouseX, mouseY); + } + return true; + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseReleased(double mouseX, double mouseY, int button) { + dragging = false; + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (dragging && hasFocus && textField != null) { + moveCursorToMouse(mouseX, mouseY); + return true; + } + return super.mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean mouseWheelMove(double mouseX, double mouseY, double wheelDelta) { + if (!isMouseOverElement(mouseX, mouseY) || font == null) return false; + setScrollAmount(scrollAmount - wheelDelta * font.lineHeight); + return true; + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!hasFocus || textField == null) return false; + + if (keyCode == KEY_ESCAPE) { + // zostawiamy ESC do wyjścia z GUI; nie kasujemy focusu na siłę + return false; + } + + // CTRL shortcuts + if (isCtrlDown()) { + if (net.minecraft.client.gui.screens.Screen.isSelectAll(keyCode)) { + selectAll(); + sendToServer(); + return true; + } + if (net.minecraft.client.gui.screens.Screen.isCopy(keyCode)) { + copySelection(); + return true; + } + if (net.minecraft.client.gui.screens.Screen.isCut(keyCode)) { + cutSelection(); + clampScroll(); + ensureCursorVisible(); + sendToServer(); + return true; + } + if (net.minecraft.client.gui.screens.Screen.isPaste(keyCode)) { + pasteClipboard(); + clampScroll(); + ensureCursorVisible(); + sendToServer(); + return true; + } + } + + // Enter / Tab + if (keyCode == KEY_ENTER || keyCode == KEY_KP_ENTER) { + textField.insertText("\n"); + clampScroll(); + ensureCursorVisible(); + sendToServer(); + return true; + } + if (keyCode == KEY_TAB) { + textField.insertText("\t"); + clampScroll(); + ensureCursorVisible(); + sendToServer(); + return true; + } + + // reszta (strzałki, backspace, delete, home/end, itp.) + boolean handled = textField.keyPressed(keyCode); + if (handled) { + clampScroll(); + ensureCursorVisible(); + sendToServer(); + } + + return true; + } + + @Override + @OnlyIn(Dist.CLIENT) + public boolean charTyped(char codePoint, int modifiers) { + if (!hasFocus || textField == null) return false; + + // nie wstawiaj znaków kontrolnych (poza \t i \n obsłużonych wyżej) + if (codePoint < 32) return true; + + textField.insertText(String.valueOf(codePoint)); + String v = clampToMax(safe(textField.value())); + if (!v.equals(textField.value())) { + textField.setValue(v); + } + + clampScroll(); + ensureCursorVisible(); + sendToServer(); + return true; + } + + // ====== render ====== + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(net.minecraft.client.gui.GuiGraphics graphics, int mouseX, int mouseY, + float partialTicks) { + if (textField == null || font == null) { + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + return; + } + + int x0 = getPosition().x; + int y0 = getPosition().y; + int w = getSize().width; + int h = getSize().height; + + int bg = 0xFF202020; + int border = hasFocus ? 0xFFFFFFFF : 0xFF808080; + + graphics.fill(x0 - 1, y0 - 1, x0 + w + 1, y0 + h + 1, 0xAA000000); + graphics.fill(x0, y0, x0 + w, y0 + h, bg); + graphics.fill(x0, y0, x0 + w, y0 + 1, border); + graphics.fill(x0, y0 + h - 1, x0 + w, y0 + h, border); + graphics.fill(x0, y0, x0 + 1, y0 + h, border); + graphics.fill(x0 + w - 1, y0, x0 + w, y0 + h, border); + + int clipL = x0 + 2; + int clipT = y0 + 2; + int clipR = x0 + w - 2; + int clipB = y0 + h - 2; + graphics.enableScissor(clipL, clipT, clipR, clipB); + + int firstLine = (int) (scrollAmount / font.lineHeight); + int y = clipT - (int) scrollAmount + firstLine * font.lineHeight; + + int selectionBegin = textField.hasSelection() ? textField.selection().begin : -1; + int selectionEnd = textField.hasSelection() ? textField.selection().end : -1; + int selectionColor = 0x80007FFF; + + for (int idx = firstLine; idx < textField.lineCount(); idx++) { + if (y > clipB) break; + + Line ln = textField.line(idx); + String full = safe(textField.value()); + String str = full.substring(ln.begin, ln.end); + + if (textField.hasSelection()) { + int lineStartChar = ln.begin; + int lineEndChar = ln.end; + + if (!(selectionEnd <= lineStartChar || selectionBegin >= lineEndChar)) { + int selStartInLine = Math.max(0, selectionBegin - lineStartChar); + int selEndInLine = Math.min(str.length(), selectionEnd - lineStartChar); + + if (selStartInLine < selEndInLine) { + String preSel = str.substring(0, selStartInLine); + String selectionText = str.substring(selStartInLine, selEndInLine); + + int selX = clipL + font.width(preSel); + int selW = font.width(selectionText); + graphics.fill(selX, y, selX + selW, y + font.lineHeight, selectionColor); + } + } + } + + graphics.drawString(font, str, clipL, y, 0xFFFFFFFF); + y += font.lineHeight; + } + + // caret + if (hasFocus && blink()) { + int curLine = textField.lineAtCursor(); + Line ln = textField.line(curLine); + + String full = safe(textField.value()); + int cursor = Mth.clamp(textField.cursor(), 0, full.length()); + + int cx = clipL + font.width(full.substring(ln.begin, cursor)); + int cy = clipT + curLine * font.lineHeight - (int) scrollAmount; + + if (cy >= clipT && cy < clipB) { + graphics.fill(cx, cy, cx + 1, cy + font.lineHeight, 0xFFFFFFFF); + } + } + + graphics.disableScissor(); + + // placeholder + if (safe(textField.value()).isEmpty() && !hasFocus && !placeholder.getString().isEmpty()) { + graphics.drawString(font, placeholder, clipL, clipT, 0xFF808080); + } + + super.drawInBackground(graphics, mouseX, mouseY, partialTicks); + } + + // ===== helpers ===== + private static String safe(String s) { + return s == null ? "" : s; + } + + private String clampToMax(String s) { + if (s.length() <= maxLength) return s; + return s.substring(0, maxLength); + } + + // ===== cached multiline field (client-only) ===== + private static final class Line { + + final int begin; + final int end; + + Line(int begin, int end) { + this.begin = begin; + this.end = end; + } + } + + private static final class Selection { + + final int begin; + final int end; + + Selection(int begin, int end) { + this.begin = begin; + this.end = end; + } + } + + @OnlyIn(Dist.CLIENT) + private static final class CachedTextField extends net.minecraft.client.gui.components.MultilineTextField { + + private List cache = null; + + CachedTextField(net.minecraft.client.gui.Font font, int w) { + super(font, w); + rebuild(); + } + + private List cacheList() { + if (cache == null) cache = new ArrayList<>(); + return cache; + } + + int lineCount() { + return cacheList().size(); + } + + Line line(int idx) { + List c = cacheList(); + if (c.isEmpty()) return new Line(0, 0); + int i = Mth.clamp(idx, 0, c.size() - 1); + return c.get(i); + } + + int lineAtCursor() { + return super.getLineAtCursor(); + } + + Selection selection() { + var sv = super.getSelected(); + return new Selection(sv.beginIndex(), sv.endIndex()); + } + + @Override + public void setValue(String v) { + super.setValue(v); + rebuild(); + } + + @Override + public void insertText(String t) { + super.insertText(t); + rebuild(); + } + + private void rebuild() { + List c = cacheList(); + c.clear(); + for (var sv : super.iterateLines()) { + c.add(new Line(sv.beginIndex(), sv.endIndex())); + } + } + } +} diff --git a/src/main/java/net/neganote/gtutilities/common/gui/widgets/SimpleScrollbarWidget.java b/src/main/java/net/neganote/gtutilities/common/gui/widgets/SimpleScrollbarWidget.java new file mode 100644 index 0000000..3fe9ae5 --- /dev/null +++ b/src/main/java/net/neganote/gtutilities/common/gui/widgets/SimpleScrollbarWidget.java @@ -0,0 +1,131 @@ +package net.neganote.gtutilities.common.gui.widgets; + +import com.lowdragmc.lowdraglib.gui.widget.Widget; +import com.lowdragmc.lowdraglib.utils.Position; +import com.lowdragmc.lowdraglib.utils.Size; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import lombok.Getter; + +import java.util.function.Consumer; + +public class SimpleScrollbarWidget extends Widget { + + private int minScroll = 0; + private int maxScroll = 0; + @Getter + private int currentScroll = 0; + private int pageSize = 1; + + private boolean dragging = false; + private int dragYOffset = 0; + + private final Consumer onScrollChanged; + + public SimpleScrollbarWidget(int x, int y, int height, Consumer onScrollChanged) { + super(new Position(x, y), new Size(12, height)); + this.onScrollChanged = onScrollChanged; + } + + public void setRange(int min, int max, int pageSize) { + this.minScroll = min; + this.maxScroll = Math.max(min, max); + this.pageSize = Math.max(1, pageSize); + setScroll(this.currentScroll); + } + + public void setScroll(int newScroll) { + int clamped = Mth.clamp(newScroll, minScroll, maxScroll); + if (this.currentScroll != clamped) { + this.currentScroll = clamped; + if (onScrollChanged != null) onScrollChanged.accept(this.currentScroll); + } + } + + public int getRange() { + return maxScroll - minScroll; + } + + @Override + @OnlyIn(Dist.CLIENT) + public void drawInBackground(net.minecraft.client.gui.GuiGraphics graphics, int mouseX, int mouseY, + float partialTicks) { + ResourceLocation scroll = new ResourceLocation("minecraft", + "textures/gui/container/creative_inventory/tabs.png"); + Position pos = getPosition(); + Size size = getSize(); + + graphics.fill(pos.x, pos.y, pos.x + size.width, pos.y + size.height, 0xFF202020); + graphics.fill(pos.x + 10, pos.y, pos.x + 11, pos.y + size.height, 0xFF353535); + graphics.fill(pos.x, pos.y, pos.x + 1, pos.y + size.height, 0xFF000000); + + int handleX = pos.x + (getSizeWidth() - 12) / 2; + int handleY = pos.y + ((this.currentScroll - this.minScroll) * (getSizeHeight() - 15) / getRange()); + + if (getRange() > 0) { + graphics.blit(scroll, handleX, handleY, 232, 0, getSizeWidth(), getSizeHeight()); + } else { + graphics.blit(scroll, handleX, pos.y, 244, 0, getSizeWidth(), getSizeHeight()); + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isMouseOverElement(mouseX, mouseY) || getRange() == 0) return false; + + Position pos = getPosition(); + int relY = (int) mouseY - pos.y; + + int handleHeight = 15; + int trackHeight = getSize().height - handleHeight; + + int currentYOffset = (this.currentScroll - this.minScroll) * trackHeight / getRange(); + + if (relY < currentYOffset) { + setScroll(this.currentScroll - pageSize); + } else if (relY > currentYOffset + handleHeight) { + setScroll(this.currentScroll + pageSize); + } else { + this.dragging = true; + this.dragYOffset = relY - currentYOffset; + } + return true; + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + this.dragging = false; + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (this.dragging && getRange() > 0) { + Position pos = getPosition(); + int handleHeight = 15; + int trackHeight = getSize().height - handleHeight; + + double handleUpperEdgeY = mouseY - pos.y - this.dragYOffset; + double position = Mth.clamp(handleUpperEdgeY / trackHeight, 0.0, 1.0); + + int calculatedScroll = this.minScroll + (int) Math.round(position * getRange()); + setScroll(calculatedScroll); + return true; + } + return false; + } + + @Override + public boolean mouseWheelMove(double mouseX, double mouseY, double wheelDelta) { + if (getRange() > 0 && this.getSizeHeight() + this.getPositionY() >= mouseY && this.getPositionY() <= mouseY) { + int delta = (int) (wheelDelta * pageSize); + setScroll(this.currentScroll - delta); + return true; + } + return false; + } +} diff --git a/src/main/java/net/neganote/gtutilities/common/machine/UtilAEMachines.java b/src/main/java/net/neganote/gtutilities/common/machine/UtilAEMachines.java index ceda9b6..71dd35a 100644 --- a/src/main/java/net/neganote/gtutilities/common/machine/UtilAEMachines.java +++ b/src/main/java/net/neganote/gtutilities/common/machine/UtilAEMachines.java @@ -7,9 +7,9 @@ import net.minecraft.network.chat.Component; import net.neganote.gtutilities.config.UtilConfig; -import net.neganote.gtutilities.integration.ae2.machine.ExpandedPatternBufferPartMachine; -import net.neganote.gtutilities.integration.ae2.machine.ExpandedPatternBufferProxyPartMachine; +import net.neganote.gtutilities.integration.ae2.machine.*; +import static com.gregtechceu.gtceu.api.GTValues.LuV; import static com.gregtechceu.gtceu.api.GTValues.ZPM; import static net.neganote.gtutilities.GregTechModernUtilities.REGISTRATE; @@ -17,37 +17,150 @@ public class UtilAEMachines { public static MachineDefinition EXPANDED_ME_PATTERN_BUFFER = null; public static MachineDefinition EXPANDED_ME_PATTERN_BUFFER_PROXY = null; + public static MachineDefinition ME_TAG_STOCKING_INPUT_BUS = null; + public static MachineDefinition ME_ENLARGED_STOCKING_INPUT_BUS = null; + public static MachineDefinition ME_ENLARGED_TAG_STOCKING_INPUT_BUS = null; + public static MachineDefinition ME_TAG_STOCKING_INPUT_HATCH = null; + public static MachineDefinition ME_ENLARGED_STOCKING_INPUT_HATCH = null; + public static MachineDefinition ME_ENLARGED_TAG_STOCKING_INPUT_HATCH = null; static { - if (UtilConfig.INSTANCE.features.expandedBuffersEnabled && GTCEu.Mods.isAE2Loaded() || GTCEu.isDataGen()) { - EXPANDED_ME_PATTERN_BUFFER = REGISTRATE - .machine("expanded_me_pattern_buffer", ExpandedPatternBufferPartMachine::new) - .tier(ZPM) - .rotationState(RotationState.ALL) - .abilities(PartAbility.IMPORT_ITEMS, PartAbility.IMPORT_FLUIDS, PartAbility.EXPORT_FLUIDS, - PartAbility.EXPORT_ITEMS) - .colorOverlayTieredHullModel(GTCEu.id("block/overlay/appeng/me_buffer_hatch")) - .langValue("Expanded ME Pattern Buffer") - .tooltips( - Component.translatable("block.gtmutils.pattern_buffer.desc.0"), - Component.translatable("block.gtceu.pattern_buffer.desc.1"), - Component.translatable("block.gtmutils.pattern_buffer.desc.2"), - Component.translatable("gtceu.part_sharing.enabled")) - .register(); - - EXPANDED_ME_PATTERN_BUFFER_PROXY = REGISTRATE - .machine("expanded_me_pattern_buffer_proxy", ExpandedPatternBufferProxyPartMachine::new) - .tier(ZPM) - .rotationState(RotationState.ALL) - .abilities(PartAbility.IMPORT_ITEMS, PartAbility.IMPORT_FLUIDS, PartAbility.EXPORT_FLUIDS, - PartAbility.EXPORT_ITEMS) - .colorOverlayTieredHullModel(GTCEu.id("block/overlay/appeng/me_buffer_hatch_proxy")) - .langValue("Expanded ME Pattern Buffer Proxy") - .tooltips( - Component.translatable("block.gtmutils.pattern_buffer_proxy.desc.0"), - Component.translatable("block.gtceu.pattern_buffer_proxy.desc.2"), - Component.translatable("gtceu.part_sharing.enabled")) - .register(); + if (GTCEu.Mods.isAE2Loaded()) { + if (UtilConfig.INSTANCE.features.expandedBuffersEnabled || GTCEu.isDataGen()) { + EXPANDED_ME_PATTERN_BUFFER = REGISTRATE + .machine("expanded_me_pattern_buffer", ExpandedPatternBufferPartMachine::new) + .tier(ZPM) + .rotationState(RotationState.ALL) + .abilities(PartAbility.IMPORT_ITEMS, PartAbility.IMPORT_FLUIDS, PartAbility.EXPORT_FLUIDS, + PartAbility.EXPORT_ITEMS) + .colorOverlayTieredHullModel(GTCEu.id("block/overlay/appeng/me_buffer_hatch")) + .langValue("Expanded ME Pattern Buffer") + .tooltips( + Component.translatable("block.gtmutils.pattern_buffer.desc.0"), + Component.translatable("block.gtceu.pattern_buffer.desc.1"), + Component.translatable("block.gtmutils.pattern_buffer.desc.2"), + Component.translatable("gtceu.part_sharing.enabled")) + .register(); + + EXPANDED_ME_PATTERN_BUFFER_PROXY = REGISTRATE + .machine("expanded_me_pattern_buffer_proxy", ExpandedPatternBufferProxyPartMachine::new) + .tier(ZPM) + .rotationState(RotationState.ALL) + .abilities(PartAbility.IMPORT_ITEMS, PartAbility.IMPORT_FLUIDS, PartAbility.EXPORT_FLUIDS, + PartAbility.EXPORT_ITEMS) + .colorOverlayTieredHullModel(GTCEu.id("block/overlay/appeng/me_buffer_hatch_proxy")) + .langValue("Expanded ME Pattern Buffer Proxy") + .tooltips( + Component.translatable("block.gtmutils.pattern_buffer_proxy.desc.0"), + Component.translatable("block.gtceu.pattern_buffer_proxy.desc.2"), + Component.translatable("gtceu.part_sharing.enabled")) + .register(); + } + + if (UtilConfig.INSTANCE.features.tagStockingEnabled || GTCEu.isDataGen()) { + ME_TAG_STOCKING_INPUT_BUS = REGISTRATE + .machine("me_tag_stocking_input_bus", METagStockingInputBusPartMachine::new) + .tier(LuV) + .rotationState(RotationState.ALL) + .abilities(PartAbility.IMPORT_ITEMS) + .colorOverlayTieredHullModel(GTCEu.id("block/overlay/appeng/me_tag_bus")) + .langValue("ME Tag Stocking Input Bus") + .tooltips( + Component.translatable("gtceu.machine.item_bus.import.tooltip"), + Component.translatable("block.gtmutils.tag_stocking_bus.desc.0"), + Component.translatable("block.gtmutils.tag_stocking_bus.desc.1"), + Component.translatable("block.gtmutils.tag_stocking_bus.desc.2"), + Component.translatable("gtceu.part_sharing.enabled")) + .register(); + } + + if (UtilConfig.INSTANCE.features.enlargedStockingEnabled || GTCEu.isDataGen()) { + ME_ENLARGED_STOCKING_INPUT_BUS = REGISTRATE + .machine("me_enlarged_stocking_input_bus", MEEnlargedStockingInputBusPartMachine::new) + .tier(LuV) + .rotationState(RotationState.ALL) + .abilities(PartAbility.IMPORT_ITEMS) + .colorOverlayTieredHullModel(GTCEu.id("block/overlay/appeng/me_input_bus")) + .langValue("ME Enlarged Stocking Input Bus") + .tooltips( + Component.translatable("gtceu.machine.item_bus.import.tooltip"), + Component.translatable("block.gtmutils.enlarged_stocking_bus.desc.0"), + Component.translatable("block.gtmutils.enlarged_stocking_bus.desc.1"), + Component.translatable("block.gtmutils.enlarged_stocking_bus.desc.2"), + Component.translatable("gtceu.part_sharing.enabled")) + .register(); + } + + if ((UtilConfig.INSTANCE.features.enlargedStockingEnabled && + UtilConfig.INSTANCE.features.tagStockingEnabled) || GTCEu.isDataGen()) { + ME_ENLARGED_TAG_STOCKING_INPUT_BUS = REGISTRATE + .machine("me_enlarged_tag_stocking_input_bus", MEEnlargedTagStockingInputBusPartMachine::new) + .tier(ZPM) + .rotationState(RotationState.ALL) + .abilities(PartAbility.IMPORT_ITEMS) + .colorOverlayTieredHullModel(GTCEu.id("block/overlay/appeng/me_tag_bus")) + .langValue("ME Enlarged Tag Stocking Input Bus") + .tooltips( + Component.translatable("gtceu.machine.item_bus.import.tooltip"), + Component.translatable("block.gtmutils.tag_enlarged_stocking_bus.desc.0"), + Component.translatable("block.gtmutils.tag_enlarged_stocking_bus.desc.1"), + Component.translatable("block.gtmutils.tag_enlarged_stocking_bus.desc.2"), + Component.translatable("gtceu.part_sharing.enabled")) + .register(); + } + + if (UtilConfig.INSTANCE.features.tagStockingEnabled || GTCEu.isDataGen()) { + ME_TAG_STOCKING_INPUT_HATCH = REGISTRATE + .machine("me_tag_stocking_input_hatch", METagStockingInputHatchPartMachine::new) + .tier(LuV) + .rotationState(RotationState.ALL) + .abilities(PartAbility.IMPORT_FLUIDS) + .colorOverlayTieredHullModel(GTCEu.id("block/overlay/appeng/me_tag_hatch")) + .langValue("ME Tag Stocking Input Hatch") + .tooltips( + Component.translatable("gtceu.machine.fluid_hatch.import.tooltip"), + Component.translatable("block.gtmutils.tag_stocking_hatch.desc.0"), + Component.translatable("block.gtmutils.tag_stocking_hatch.desc.1"), + Component.translatable("block.gtmutils.tag_stocking_hatch.desc.2"), + Component.translatable("gtceu.part_sharing.enabled")) + .register(); + } + + if (UtilConfig.INSTANCE.features.enlargedStockingEnabled || GTCEu.isDataGen()) { + ME_ENLARGED_STOCKING_INPUT_HATCH = REGISTRATE + .machine("me_enlarged_stocking_input_hatch", MEEnlargedStockingInputHatchPartMachine::new) + .tier(LuV) + .rotationState(RotationState.ALL) + .abilities(PartAbility.IMPORT_FLUIDS) + .colorOverlayTieredHullModel(GTCEu.id("block/overlay/appeng/me_input_hatch")) + .langValue("ME Enlarged Stocking Input Hatch") + .tooltips( + Component.translatable("gtceu.machine.fluid_hatch.import.tooltip"), + Component.translatable("block.gtmutils.enlarged_stocking_hatch.desc.0"), + Component.translatable("block.gtmutils.enlarged_stocking_hatch.desc.1"), + Component.translatable("block.gtmutils.enlarged_stocking_hatch.desc.2"), + Component.translatable("gtceu.part_sharing.enabled")) + .register(); + } + + if ((UtilConfig.INSTANCE.features.enlargedStockingEnabled && + UtilConfig.INSTANCE.features.tagStockingEnabled) || GTCEu.isDataGen()) { + ME_ENLARGED_TAG_STOCKING_INPUT_HATCH = REGISTRATE + .machine("me_enlarged_tag_stocking_input_hatch", + MEEnlargedTagStockingInputHatchPartMachine::new) + .tier(ZPM) + .rotationState(RotationState.ALL) + .abilities(PartAbility.IMPORT_FLUIDS) + .colorOverlayTieredHullModel(GTCEu.id("block/overlay/appeng/me_tag_hatch")) + .langValue("ME Enlarged Tag Stocking Input Hatch") + .tooltips( + Component.translatable("gtceu.machine.fluid_hatch.import.tooltip"), + Component.translatable("block.gtmutils.tag_enlarged_stocking_hatch.desc.0"), + Component.translatable("block.gtmutils.tag_enlarged_stocking_hatch.desc.1"), + Component.translatable("block.gtmutils.tag_enlarged_stocking_hatch.desc.2"), + Component.translatable("gtceu.part_sharing.enabled")) + .register(); + } } } diff --git a/src/main/java/net/neganote/gtutilities/config/UtilConfig.java b/src/main/java/net/neganote/gtutilities/config/UtilConfig.java index 6147bf7..325a9e2 100644 --- a/src/main/java/net/neganote/gtutilities/config/UtilConfig.java +++ b/src/main/java/net/neganote/gtutilities/config/UtilConfig.java @@ -93,8 +93,24 @@ public static class FeatureConfigs { @Configurable @Configurable.Comment({ - "Whether the Expanded Pattern Buffer and Expanded Pattern Buffer Proxy are enabled. If AE2 is not loaded, this config will not load the machines regardless. " }) + "Whether the Expanded Pattern Buffer and Expanded Pattern Buffer Proxy are enabled. If AE2 is not loaded, this config will not load the machines regardless." }) public boolean expandedBuffersEnabled = true; + + @Configurable + @Configurable.Comment({ + "Whether the Enlarged Stocking Bus/Hatch are enabled. If AE2 is not loaded, this config will not load the machines regardless." }) + public boolean enlargedStockingEnabled = false; + + @Configurable + @Configurable.DecimalRange(min = 1, max = 64) + @Configurable.Comment({ + "How many rows the Enlarged Stocking Input Bus/Hatch should have, max is 64 rows for 512 slots. (each row is 8 slots in size)" }) + public int enlargedStockingSizeRows = 8; + + @Configurable + @Configurable.Comment({ + "Whether the Tag Stocking Input Bus/Hatch are enabled. If AE2 is not loaded, this config will not load the machines regardless." }) + public boolean tagStockingEnabled = true; } public static boolean coolantEnabled() { diff --git a/src/main/java/net/neganote/gtutilities/datagen/lang/UtilLangHandler.java b/src/main/java/net/neganote/gtutilities/datagen/lang/UtilLangHandler.java index 53c6887..e826ab9 100644 --- a/src/main/java/net/neganote/gtutilities/datagen/lang/UtilLangHandler.java +++ b/src/main/java/net/neganote/gtutilities/datagen/lang/UtilLangHandler.java @@ -90,6 +90,37 @@ public static void init(RegistrateLangProvider provider) { provider.add("block.gtmutils.pattern_buffer_proxy.desc.0", "§fAllows linking many machines to a singular §6Expanded ME Pattern Buffer§f."); + multiLang(provider, "block.gtmutils.tag_stocking_bus.desc", + "Retrieves items directly from the ME network", + "Stock list is built from tag filters (whitelist/blacklist)", + "Keeps the matching items in stock automatically"); + multiLang(provider, "block.gtmutils.tag_stocking_hatch.desc", + "Retrieves fluids directly from the ME network", + "Stock list is built from tag filters (whitelist/blacklist)", + "Keeps the matching fluids in stock automatically"); + + multiLang(provider, "block.gtmutils.enlarged_stocking_bus.desc", + "Retrieves items directly from the ME network", + "Extra-large stocking list", + "Auto-Pull can fill the list with the most abundant ME items"); + multiLang(provider, "block.gtmutils.enlarged_stocking_hatch.desc", + "Retrieves fluids directly from the ME network", + "Extra-large stocking list", + "Auto-Pull can fill the list with the most abundant ME fluids"); + + multiLang(provider, "block.gtmutils.tag_enlarged_stocking_bus.desc", + "Retrieves items directly from the ME network", + "Extra-large tag-based stocking", + "Uses whitelist/blacklist tags to build the stocked item list"); + multiLang(provider, "block.gtmutils.tag_enlarged_stocking_hatch.desc", + "Retrieves fluids directly from the ME network", + "Extra-large tag-based stocking", + "Uses whitelist/blacklist tags to build the stocked fluid list"); + + provider.add("config.jade.plugin_gtmutils.enlarged_me_stocking_bus_info", "Enlarged ME Stocking Bus Info"); + provider.add("config.jade.plugin_gtmutils.enlarged_me_stocking_bus_info.desc", + "Shows a snapshot of enlarged ME stocking bus contents in Jade"); + dfs(provider, new HashSet<>(), UtilConfig.CONFIG_HOLDER.getValueMap()); } diff --git a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedStockingInputBusPartMachine.java b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedStockingInputBusPartMachine.java new file mode 100644 index 0000000..1feb739 --- /dev/null +++ b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedStockingInputBusPartMachine.java @@ -0,0 +1,404 @@ +package net.neganote.gtutilities.integration.ae2.machine; + +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.api.machine.trait.NotifiableItemStackHandler; +import com.gregtechceu.gtceu.common.item.IntCircuitBehaviour; +import com.gregtechceu.gtceu.config.ConfigHolder; +import com.gregtechceu.gtceu.integration.ae2.gui.widget.AEItemConfigWidget; +import com.gregtechceu.gtceu.integration.ae2.machine.MEStockingBusPartMachine; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEItemList; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEItemSlot; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAESlot; + +import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; +import com.lowdragmc.lowdraglib.gui.widget.Widget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.lowdragmc.lowdraglib.utils.Position; +import com.lowdragmc.lowdraglib.utils.Size; + +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.BlockHitResult; +import net.neganote.gtutilities.common.gui.widgets.SimpleScrollbarWidget; +import net.neganote.gtutilities.config.UtilConfig; + +import appeng.api.config.Actionable; +import appeng.api.networking.IGrid; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.storage.MEStorage; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.PriorityQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class MEEnlargedStockingInputBusPartMachine extends MEStockingBusPartMachine { + + protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + MEEnlargedStockingInputBusPartMachine.class, MEStockingBusPartMachine.MANAGED_FIELD_HOLDER); + + private static final int SLOTS_PER_ROW = 8; + private static final int TOTAL_ROWS = Math.min(64, UtilConfig.INSTANCE.features.enlargedStockingSizeRows); + private static final int TOTAL_SLOTS = SLOTS_PER_ROW * TOTAL_ROWS; + + @Persisted + @DescSynced + private boolean enlargedAutoPull = false; + + private Predicate enlargedAutoPullTest = $ -> false; + + public MEEnlargedStockingInputBusPartMachine(IMachineBlockEntity holder, Object... args) { + super(holder, args); + super.setAutoPull(false); + } + + @Override + public ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + @Override + protected NotifiableItemStackHandler createInventory(Object... args) { + this.aeItemHandler = new ExportOnlyAEStockingItemList(this, TOTAL_SLOTS); + return this.aeItemHandler; + } + + @Override + public boolean isAutoPull() { + return enlargedAutoPull; + } + + @Override + public void setAutoPull(boolean autoPull) { + this.enlargedAutoPull = autoPull; + + if (!isRemote()) { + if (!this.enlargedAutoPull) { + this.aeItemHandler.clearInventory(0); + } else if (updateMEStatus()) { + refreshListEnlarged(); + updateInventorySubscription(); + } + } + + super.setAutoPull(false); + } + + @Override + protected InteractionResult onScrewdriverClick(Player playerIn, InteractionHand hand, Direction gridSide, + BlockHitResult hitResult) { + if (!isRemote()) { + setAutoPull(!isAutoPull()); + if (isAutoPull()) { + playerIn.sendSystemMessage(Component.translatable("gtceu.machine.me.stocking_auto_pull_enabled")); + } else { + playerIn.sendSystemMessage(Component.translatable("gtceu.machine.me.stocking_auto_pull_disabled")); + } + } + return InteractionResult.sidedSuccess(isRemote()); + } + + @Override + public void autoIO() { + super.autoIO(); + + if (getTicksPerCycle() == 0) { + setTicksPerCycle(ConfigHolder.INSTANCE.compat.ae2.updateIntervals); + } + + if (getOffsetTimer() % (long) getTicksPerCycle() == 0L) { + if (!isRemote() && enlargedAutoPull) { + if (updateMEStatus()) { + refreshListEnlarged(); + super.syncME(); + updateInventorySubscription(); + } + } + } + } + + private void refreshListEnlarged() { + IGrid grid = this.getMainNode().getGrid(); + if (grid == null) { + aeItemHandler.clearInventory(0); + return; + } + + MEStorage networkStorage = grid.getStorageService().getInventory(); + var counter = networkStorage.getAvailableStacks(); + + final int size = this.aeItemHandler.getSlots(); + final int min = this.getMinStackSize(); + + PriorityQueue> topItems = new PriorityQueue<>( + Comparator.comparingLong(Object2LongMap.Entry::getLongValue)); + + for (Object2LongMap.Entry entry : counter) { + long amount = entry.getLongValue(); + AEKey what = entry.getKey(); + + if (amount <= 0) continue; + if (!(what instanceof AEItemKey itemKey)) continue; + + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, actionSource); + if (request == 0) continue; + + if (enlargedAutoPullTest != null && !enlargedAutoPullTest.test(new GenericStack(itemKey, amount))) continue; + + if (amount >= min) { + if (topItems.size() < size) { + topItems.offer(entry); + } else if (amount > topItems.peek().getLongValue()) { + topItems.poll(); + topItems.offer(entry); + } + } + } + + int index; + int itemAmount = topItems.size(); + for (index = 0; index < size; index++) { + if (topItems.isEmpty()) break; + + Object2LongMap.Entry entry = topItems.poll(); + AEKey what = entry.getKey(); + long amount = entry.getLongValue(); + + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, actionSource); + + ExportOnlyAEItemSlot slot = this.aeItemHandler.getInventory()[itemAmount - index - 1]; + slot.setConfig(new GenericStack(what, 1)); + slot.setStock(new GenericStack(what, request)); + } + + aeItemHandler.clearInventory(index); + } + + @Override + public void setAutoPullTest(Predicate autoPullTest) { + this.enlargedAutoPullTest = autoPullTest; + super.setAutoPullTest(autoPullTest); + } + + @Override + protected CompoundTag writeConfigToTag() { + if (!enlargedAutoPull) { + CompoundTag tag = super.writeConfigToTag(); + tag.putBoolean("AutoPull", false); + return tag; + } + CompoundTag tag = new CompoundTag(); + tag.putBoolean("AutoPull", true); + tag.putByte("GhostCircuit", + (byte) IntCircuitBehaviour.getCircuitConfiguration(circuitInventory.getStackInSlot(0))); + return tag; + } + + @Override + protected void readConfigFromTag(CompoundTag tag) { + if (tag.getBoolean("AutoPull")) { + setAutoPull(true); + circuitInventory.setStackInSlot(0, IntCircuitBehaviour.stack(tag.getByte("GhostCircuit"))); + return; + } + setAutoPull(false); + super.readConfigFromTag(tag); + } + + @Override + public Widget createUIWidget() { + int visibleRows = 4; + int rowHeight = 40; + int startX = 10; + int startY = 20; + int scrollbarX = 158; + + final List allRowWidgets = new ArrayList<>(); + + Consumer onScrollChanged = (currentScroll) -> { + for (int i = 0; i < allRowWidgets.size(); i++) { + AEItemConfigWidget row = allRowWidgets.get(i); + int relativeIndex = i - currentScroll; + if (relativeIndex >= 0 && relativeIndex < visibleRows) { + row.setVisible(true); + row.setSelfPosition(startX, startY + (relativeIndex * rowHeight)); + } else { + row.setVisible(false); + } + } + }; + + SimpleScrollbarWidget scrollbar = new SimpleScrollbarWidget( + scrollbarX, + startY, + visibleRows * rowHeight - 4, + onScrollChanged); + + scrollbar.setRange(0, TOTAL_ROWS - visibleRows, 1); + + WidgetGroup group = new WidgetGroup(new Position(0, 0), new Size(178, 200)) { + + @Override + public boolean mouseWheelMove(double mouseX, double mouseY, double wheelDelta) { + if (super.mouseWheelMove(mouseX, mouseY, wheelDelta)) { + return true; + } + if (isMouseOver(getPositionX(), getPositionY(), getSizeWidth(), getSizeHeight(), mouseX, mouseY)) { + return scrollbar.mouseWheelMove(mouseX, mouseY, wheelDelta); + } + return false; + } + }; + + group.addWidget(new LabelWidget(5, 5, () -> this.isOnline ? + "gtceu.gui.me_network.online" : + "gtceu.gui.me_network.offline")); + + for (int i = 0; i < TOTAL_ROWS; i++) { + final int currentRowIndex = i; + final int startSlotIndex = currentRowIndex * SLOTS_PER_ROW; + + AtomicInteger localSlotCounter = new AtomicInteger(0); + ExportOnlyAEItemList viewHandler = new ExportOnlyAEItemList( + this, + SLOTS_PER_ROW, + () -> { + int globalSlotIndex = startSlotIndex + localSlotCounter.getAndIncrement(); + if (globalSlotIndex < this.aeItemHandler.getSlots()) { + return this.aeItemHandler.getInventory()[globalSlotIndex]; + } + return null; + }) { + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean isAutoPull() { + return MEEnlargedStockingInputBusPartMachine.this.isAutoPull(); + } + }; + + var root = (ExportOnlyAEStockingItemList) MEEnlargedStockingInputBusPartMachine.this.aeItemHandler; + Runnable rootCb = root::fireContentsChanged; + + for (int s = 0; s < SLOTS_PER_ROW; s++) { + int global = startSlotIndex + s; + if (global >= 0 && global < MEEnlargedStockingInputBusPartMachine.this.aeItemHandler.getSlots()) { + MEEnlargedStockingInputBusPartMachine.this.aeItemHandler.getInventory()[global] + .setOnContentsChanged(rootCb); + } + } + + AEItemConfigWidget rowWidget = new AEItemConfigWidget(startX, startY, viewHandler) { + + @Override + public boolean hasStackInConfig(GenericStack stack) { + return MEEnlargedStockingInputBusPartMachine.this.aeItemHandler.hasStackInConfig(stack, true); + } + }; + + rowWidget.setVisible(false); + allRowWidgets.add(rowWidget); + group.addWidget(rowWidget); + } + + group.addWidget(scrollbar); + onScrollChanged.accept(0); + + return group; + } + + private class ExportOnlyAEStockingItemList extends ExportOnlyAEItemList { + + public ExportOnlyAEStockingItemList(MetaMachine holder, int slots) { + super(holder, slots, ExportOnlyAEStockingItemSlot::new); + } + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean isAutoPull() { + return MEEnlargedStockingInputBusPartMachine.this.isAutoPull(); + } + + public void fireContentsChanged() { + super.onContentsChanged(); + } + } + + private class ExportOnlyAEStockingItemSlot extends ExportOnlyAEItemSlot { + + public ExportOnlyAEStockingItemSlot() { + super(); + } + + public ExportOnlyAEStockingItemSlot(@Nullable GenericStack config, @Nullable GenericStack stock) { + super(config, stock); + } + + @Override + public ItemStack extractItem(int slot, int amount, boolean simulate) { + if (slot == 0 && this.stock != null) { + if (this.config != null) { + if (!isOnline()) return ItemStack.EMPTY; + MEStorage aeNetwork = getMainNode().getGrid().getStorageService().getInventory(); + + Actionable action = simulate ? Actionable.SIMULATE : Actionable.MODULATE; + var key = config.what(); + long extracted = aeNetwork.extract(key, amount, action, actionSource); + + if (extracted > 0) { + ItemStack resultStack = key instanceof AEItemKey itemKey ? itemKey.toStack((int) extracted) : + ItemStack.EMPTY; + + if (!simulate) { + this.stock = ExportOnlyAESlot.copy(stock, stock.amount() - extracted); + if (this.stock.amount() == 0) { + this.stock = null; + } + if (this.onContentsChanged != null) { + this.onContentsChanged.run(); + } + } + return resultStack; + } + } + } + return ItemStack.EMPTY; + } + + @Override + public ExportOnlyAEStockingItemSlot copy() { + return new ExportOnlyAEStockingItemSlot( + this.config == null ? null : copy(this.config), + this.stock == null ? null : copy(this.stock)); + } + } +} diff --git a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedStockingInputHatchPartMachine.java b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedStockingInputHatchPartMachine.java new file mode 100644 index 0000000..38781fa --- /dev/null +++ b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedStockingInputHatchPartMachine.java @@ -0,0 +1,420 @@ +package net.neganote.gtutilities.integration.ae2.machine; + +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.api.machine.trait.NotifiableFluidTank; +import com.gregtechceu.gtceu.common.item.IntCircuitBehaviour; +import com.gregtechceu.gtceu.config.ConfigHolder; +import com.gregtechceu.gtceu.integration.ae2.gui.widget.AEFluidConfigWidget; +import com.gregtechceu.gtceu.integration.ae2.machine.MEStockingHatchPartMachine; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEFluidList; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEFluidSlot; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAESlot; +import com.gregtechceu.gtceu.integration.ae2.utils.AEUtil; + +import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; +import com.lowdragmc.lowdraglib.gui.widget.Widget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.lowdragmc.lowdraglib.utils.Position; +import com.lowdragmc.lowdraglib.utils.Size; + +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.neganote.gtutilities.common.gui.widgets.SimpleScrollbarWidget; +import net.neganote.gtutilities.config.UtilConfig; + +import appeng.api.config.Actionable; +import appeng.api.networking.IGrid; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.storage.MEStorage; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.PriorityQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class MEEnlargedStockingInputHatchPartMachine extends MEStockingHatchPartMachine { + + protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + MEEnlargedStockingInputHatchPartMachine.class, MEStockingHatchPartMachine.MANAGED_FIELD_HOLDER); + + private static final int SLOTS_PER_ROW = 8; + private static final int TOTAL_ROWS = Math.min(64, UtilConfig.INSTANCE.features.enlargedStockingSizeRows); + private static final int TOTAL_SLOTS = SLOTS_PER_ROW * TOTAL_ROWS; + + @Persisted + @DescSynced + private boolean enlargedAutoPull = false; + private Predicate enlargedAutoPullTest = $ -> false; + + public MEEnlargedStockingInputHatchPartMachine(IMachineBlockEntity holder, Object... args) { + super(holder, args); + super.setAutoPull(false); + } + + @Override + public ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + @Override + protected NotifiableFluidTank createTank(int initialCapacity, int slots, Object... args) { + this.aeFluidHandler = new ExportOnlyAEStockingFluidList(this, TOTAL_SLOTS); + return this.aeFluidHandler; + } + + @Override + public boolean isAutoPull() { + return enlargedAutoPull; + } + + @Override + public void setAutoPull(boolean autoPull) { + this.enlargedAutoPull = autoPull; + + if (!isRemote()) { + if (!this.enlargedAutoPull) { + this.aeFluidHandler.clearInventory(0); + } else if (updateMEStatus()) { + refreshListEnlarged(); + updateTankSubscription(); + } + } + + super.setAutoPull(false); + } + + @Override + public void setAutoPullTest(Predicate autoPullTest) { + this.enlargedAutoPullTest = autoPullTest; + super.setAutoPullTest(autoPullTest); + } + + @Override + public void autoIO() { + super.autoIO(); + + if (getTicksPerCycle() == 0) setTicksPerCycle(ConfigHolder.INSTANCE.compat.ae2.updateIntervals); + + if (getOffsetTimer() % (long) getTicksPerCycle() == 0L) { + if (!isRemote() && enlargedAutoPull) { + if (updateMEStatus()) { + refreshListEnlarged(); + super.syncME(); + updateTankSubscription(); + } + } + } + } + + private void refreshListEnlarged() { + IGrid grid = this.getMainNode().getGrid(); + if (grid == null) { + aeFluidHandler.clearInventory(0); + return; + } + + MEStorage networkStorage = grid.getStorageService().getInventory(); + var counter = networkStorage.getAvailableStacks(); + + final int size = this.aeFluidHandler.getTanks(); + final int min = this.getMinStackSize(); + + PriorityQueue> topFluids = new PriorityQueue<>( + Comparator.comparingLong(Object2LongMap.Entry::getLongValue)); + + for (Object2LongMap.Entry entry : counter) { + long amount = entry.getLongValue(); + AEKey what = entry.getKey(); + + if (amount <= 0) continue; + if (!(what instanceof AEFluidKey fluidKey)) continue; + + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, actionSource); + if (request == 0) continue; + + if (enlargedAutoPullTest != null && !enlargedAutoPullTest.test(new GenericStack(fluidKey, amount))) + continue; + + if (amount >= min) { + if (topFluids.size() < size) { + topFluids.offer(entry); + } else if (amount > topFluids.peek().getLongValue()) { + topFluids.poll(); + topFluids.offer(entry); + } + } + } + + int index; + int fluidAmount = topFluids.size(); + for (index = 0; index < size; index++) { + if (topFluids.isEmpty()) break; + + Object2LongMap.Entry entry = topFluids.poll(); + AEKey what = entry.getKey(); + long amount = entry.getLongValue(); + + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, actionSource); + + var slot = this.aeFluidHandler.getInventory()[fluidAmount - index - 1]; + slot.setConfig(new GenericStack(what, 1)); + slot.setStock(new GenericStack(what, request)); + } + + aeFluidHandler.clearInventory(index); + } + + @Override + protected CompoundTag writeConfigToTag() { + if (!enlargedAutoPull) { + CompoundTag tag = super.writeConfigToTag(); + tag.putBoolean("AutoPull", false); + return tag; + } + CompoundTag tag = new CompoundTag(); + tag.putBoolean("AutoPull", true); + tag.putByte("GhostCircuit", + (byte) IntCircuitBehaviour.getCircuitConfiguration(circuitInventory.getStackInSlot(0))); + return tag; + } + + @Override + protected void readConfigFromTag(CompoundTag tag) { + if (tag.getBoolean("AutoPull")) { + setAutoPull(true); + circuitInventory.setStackInSlot(0, IntCircuitBehaviour.stack(tag.getByte("GhostCircuit"))); + return; + } + setAutoPull(false); + super.readConfigFromTag(tag); + } + + @Override + protected InteractionResult onScrewdriverClick(Player playerIn, InteractionHand hand, Direction gridSide, + BlockHitResult hitResult) { + if (!isRemote()) { + setAutoPull(!isAutoPull()); + if (isAutoPull()) { + playerIn.sendSystemMessage(Component.translatable("gtceu.machine.me.stocking_auto_pull_enabled")); + } else { + playerIn.sendSystemMessage(Component.translatable("gtceu.machine.me.stocking_auto_pull_disabled")); + } + } + return InteractionResult.sidedSuccess(isRemote()); + } + + @Override + public Widget createUIWidget() { + int visibleRows = 4; + int rowHeight = 40; + int startX = 10; + int startY = 20; + int scrollbarX = 158; + + final List allRowWidgets = new ArrayList<>(); + + Consumer onScrollChanged = (currentScroll) -> { + for (int i = 0; i < allRowWidgets.size(); i++) { + AEFluidConfigWidget row = allRowWidgets.get(i); + int relativeIndex = i - currentScroll; + if (relativeIndex >= 0 && relativeIndex < visibleRows) { + row.setVisible(true); + row.setSelfPosition(startX, startY + (relativeIndex * rowHeight)); + } else { + row.setVisible(false); + } + } + }; + + SimpleScrollbarWidget scrollbar = new SimpleScrollbarWidget( + scrollbarX, + startY, + visibleRows * rowHeight - 4, + onScrollChanged); + + scrollbar.setRange(0, TOTAL_ROWS - visibleRows, 1); + + WidgetGroup group = new WidgetGroup(new Position(0, 0), new Size(178, 200)) { + + @Override + public boolean mouseWheelMove(double mouseX, double mouseY, double wheelDelta) { + if (super.mouseWheelMove(mouseX, mouseY, wheelDelta)) { + return true; + } + if (isMouseOver(getPositionX(), getPositionY(), getSizeWidth(), getSizeHeight(), mouseX, mouseY)) { + return scrollbar.mouseWheelMove(mouseX, mouseY, wheelDelta); + } + return false; + } + }; + + group.addWidget(new LabelWidget(5, 5, () -> this.isOnline ? + "gtceu.gui.me_network.online" : + "gtceu.gui.me_network.offline")); + + for (int i = 0; i < TOTAL_ROWS; i++) { + final int currentRowIndex = i; + final int startSlotIndex = currentRowIndex * SLOTS_PER_ROW; + + AtomicInteger localSlotCounter = new AtomicInteger(0); + ExportOnlyAEFluidList viewHandler = new ExportOnlyAEFluidList( + this, + SLOTS_PER_ROW, + () -> { + int globalSlotIndex = startSlotIndex + localSlotCounter.getAndIncrement(); + if (globalSlotIndex < this.aeFluidHandler.getTanks()) { + return this.aeFluidHandler.getInventory()[globalSlotIndex]; + } + return null; + }) { + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean isAutoPull() { + return MEEnlargedStockingInputHatchPartMachine.this.isAutoPull(); + } + }; + + var root = (ExportOnlyAEStockingFluidList) MEEnlargedStockingInputHatchPartMachine.this.aeFluidHandler; + Runnable rootCb = root::fireContentsChanged; + + for (int s = 0; s < SLOTS_PER_ROW; s++) { + int global = startSlotIndex + s; + if (global >= 0 && global < MEEnlargedStockingInputHatchPartMachine.this.aeFluidHandler.getTanks()) { + MEEnlargedStockingInputHatchPartMachine.this.aeFluidHandler.getInventory()[global] + .setOnContentsChanged(rootCb); + } + } + + AEFluidConfigWidget rowWidget = new AEFluidConfigWidget(startX, startY, viewHandler) { + + @Override + public boolean hasStackInConfig(GenericStack stack) { + return MEEnlargedStockingInputHatchPartMachine.this.aeFluidHandler.hasStackInConfig(stack, true); + } + }; + + rowWidget.setVisible(false); + allRowWidgets.add(rowWidget); + group.addWidget(rowWidget); + } + + group.addWidget(scrollbar); + onScrollChanged.accept(0); + + return group; + } + + private class ExportOnlyAEStockingFluidList extends ExportOnlyAEFluidList { + + public ExportOnlyAEStockingFluidList(MetaMachine holder, int slots) { + super(holder, slots, ExportOnlyAEStockingFluidSlot::new); + } + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean isAutoPull() { + return MEEnlargedStockingInputHatchPartMachine.this.isAutoPull(); + } + + @Override + public boolean hasStackInConfig(GenericStack stack, boolean checkExternal) { + boolean inThisBus = hasStackInternal(stack); + if (inThisBus) return true; + if (checkExternal) { + return testConfiguredInOtherPart(stack); + } + return false; + } + + private boolean hasStackInternal(GenericStack stack) { + if (stack == null || stack.amount() <= 0) return false; + for (int i = 0; i < MEEnlargedStockingInputHatchPartMachine.this.aeFluidHandler.getTanks(); i++) { + var slot = MEEnlargedStockingInputHatchPartMachine.this.aeFluidHandler.getConfigurableSlot(i); + GenericStack config = slot.getConfig(); + if (config != null && config.what().equals(stack.what())) { + return true; + } + } + return false; + } + + public void fireContentsChanged() { + super.onContentsChanged(); + } + } + + private class ExportOnlyAEStockingFluidSlot extends ExportOnlyAEFluidSlot { + + public ExportOnlyAEStockingFluidSlot() { + super(); + } + + public ExportOnlyAEStockingFluidSlot(@Nullable GenericStack config, @Nullable GenericStack stock) { + super(config, stock); + } + + @Override + public ExportOnlyAEFluidSlot copy() { + return new ExportOnlyAEStockingFluidSlot( + this.config == null ? null : copy(this.config), + this.stock == null ? null : copy(this.stock)); + } + + @Override + public FluidStack drain(int maxDrain, IFluidHandler.FluidAction action) { + if (this.stock != null && this.config != null) { + if (!isOnline()) return FluidStack.EMPTY; + MEStorage aeNetwork = getMainNode().getGrid().getStorageService().getInventory(); + + Actionable actionable = action.simulate() ? Actionable.SIMULATE : Actionable.MODULATE; + var key = config.what(); + long extracted = aeNetwork.extract(key, maxDrain, actionable, actionSource); + + if (extracted > 0) { + FluidStack resultStack = key instanceof AEFluidKey fluidKey ? + AEUtil.toFluidStack(fluidKey, extracted) : FluidStack.EMPTY; + + if (action.execute()) { + this.stock = ExportOnlyAESlot.copy(stock, stock.amount() - extracted); + if (this.stock.amount() == 0) this.stock = null; + if (this.onContentsChanged != null) this.onContentsChanged.run(); + } + return resultStack; + } + } + return FluidStack.EMPTY; + } + } +} diff --git a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputBusPartMachine.java b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputBusPartMachine.java new file mode 100644 index 0000000..59485a0 --- /dev/null +++ b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputBusPartMachine.java @@ -0,0 +1,563 @@ +package net.neganote.gtutilities.integration.ae2.machine; + +import com.gregtechceu.gtceu.api.gui.GuiTextures; +import com.gregtechceu.gtceu.api.gui.fancy.ConfiguratorPanel; +import com.gregtechceu.gtceu.api.gui.fancy.IFancyConfiguratorButton; +import com.gregtechceu.gtceu.api.gui.widget.ToggleButtonWidget; +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.api.machine.fancyconfigurator.AutoStockingFancyConfigurator; +import com.gregtechceu.gtceu.api.machine.fancyconfigurator.CircuitFancyConfigurator; +import com.gregtechceu.gtceu.api.machine.trait.NotifiableItemStackHandler; +import com.gregtechceu.gtceu.config.ConfigHolder; +import com.gregtechceu.gtceu.integration.ae2.gui.widget.AEItemConfigWidget; +import com.gregtechceu.gtceu.integration.ae2.machine.MEStockingBusPartMachine; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEItemList; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEItemSlot; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAESlot; + +import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; +import com.lowdragmc.lowdraglib.gui.texture.TextTexture; +import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; +import com.lowdragmc.lowdraglib.gui.widget.Widget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.lowdragmc.lowdraglib.utils.Position; +import com.lowdragmc.lowdraglib.utils.Size; + +import net.minecraft.ChatFormatting; +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.BlockHitResult; +import net.neganote.gtutilities.common.gui.widgets.MultilineTextField; +import net.neganote.gtutilities.common.gui.widgets.SimpleScrollbarWidget; +import net.neganote.gtutilities.config.UtilConfig; +import net.neganote.gtutilities.utils.TagMatcher; + +import appeng.api.config.Actionable; +import appeng.api.networking.IGrid; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.storage.MEStorage; +import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class MEEnlargedTagStockingInputBusPartMachine extends MEStockingBusPartMachine { + + protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + MEEnlargedTagStockingInputBusPartMachine.class, MEStockingBusPartMachine.MANAGED_FIELD_HOLDER); + + @Persisted + @DescSynced + protected String whitelistExpr = ""; + + @Persisted + @DescSynced + protected String blacklistExpr = ""; + + @DescSynced + protected String Wltmp = ""; + + @DescSynced + protected String Bltmp = ""; + + private Predicate tagAutoPullTest = ($) -> true; + + private transient String wlLast = null; + private transient String blLast = null; + private transient TagMatcher.Compiled wlCompiled = TagMatcher.compile(""); + private transient TagMatcher.Compiled blCompiled = TagMatcher.compile(""); + + private final transient Object2ByteOpenHashMap decisionCache = new Object2ByteOpenHashMap<>(); + private static final int DECISION_CACHE_LIMIT = 16384; + + private static final int SLOTS_PER_ROW = 8; + private static final int TOTAL_ROWS = Math.min(64, UtilConfig.INSTANCE.features.enlargedStockingSizeRows); + private static final int TOTAL_SLOTS = SLOTS_PER_ROW * TOTAL_ROWS; + + public MEEnlargedTagStockingInputBusPartMachine(IMachineBlockEntity holder, Object... args) { + super(holder, args); + decisionCache.defaultReturnValue((byte) -1); + } + + @Override + public ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + @Override + protected NotifiableItemStackHandler createInventory(Object... args) { + this.aeItemHandler = new ExportOnlyAEStockingItemList(this, TOTAL_SLOTS); + return this.aeItemHandler; + } + + @Override + public void onLoad() { + super.onLoad(); + if (!isRemote()) { + if (super.isAutoPull()) { + super.setAutoPull(false); + } + invalidateFilterCaches(); + + if (updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateInventorySubscription(); + } + } + } + + @Override + public void autoIO() { + super.autoIO(); + + if (!isRemote() && super.isAutoPull()) { + super.setAutoPull(false); + } + + if (this.getTicksPerCycle() == 0) { + this.setTicksPerCycle(ConfigHolder.INSTANCE.compat.ae2.updateIntervals); + } + + if (this.getOffsetTimer() % (long) this.getTicksPerCycle() == 0L) { + if (!isRemote() && updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateInventorySubscription(); + } + } + } + + private static String norm(@Nullable String s) { + if (s == null) return ""; + s = s.trim(); + return s; + } + + private void invalidateFilterCaches() { + wlLast = null; + blLast = null; + decisionCache.clear(); + } + + private void ensureCompiledUpToDate() { + String wl = norm(whitelistExpr); + String bl = norm(blacklistExpr); + + if (!Objects.equals(wl, wlLast)) { + wlLast = wl; + wlCompiled = TagMatcher.compile(wl); + decisionCache.clear(); + } + if (!Objects.equals(bl, blLast)) { + blLast = bl; + blCompiled = TagMatcher.compile(bl); + decisionCache.clear(); + } + + if (decisionCache.size() > DECISION_CACHE_LIMIT) { + decisionCache.clear(); + } + } + + protected boolean isAllowed(AEItemKey key) { + ensureCompiledUpToDate(); + + if ((wlLast == null || wlLast.isEmpty()) && (blLast == null || blLast.isEmpty())) return false; + + byte cached = decisionCache.getByte(key); + if (cached != -1) { + return cached == 1; + } + + boolean allowed; + if (blLast != null && !blLast.isEmpty() && blCompiled != null && blCompiled.isValid()) { + if (TagMatcher.doesItemMatch(key, blCompiled)) { + decisionCache.put(key, (byte) 0); + return false; + } + } + + if (wlLast != null && !wlLast.isEmpty() && wlCompiled != null && wlCompiled.isValid()) { + allowed = TagMatcher.doesItemMatch(key, wlCompiled); + } else { + allowed = false; + } + + decisionCache.put(key, allowed ? (byte) 1 : (byte) 0); + return allowed; + } + + protected void refreshListFromTags() { + IGrid grid = this.getMainNode().getGrid(); + if (grid == null) { + this.aeItemHandler.clearInventory(0); + return; + } + + MEStorage networkStorage = grid.getStorageService().getInventory(); + var counter = networkStorage.getAvailableStacks(); + + PriorityQueue> top = new PriorityQueue<>( + Comparator.comparingLong(Object2LongMap.Entry::getLongValue)); + + int maxSlots = this.aeItemHandler.getSlots(); + + for (Object2LongMap.Entry entry : counter) { + long amount = entry.getLongValue(); + AEKey what = entry.getKey(); + + if (amount <= 0) continue; + if (!(what instanceof AEItemKey itemKey)) continue; + + if (!isAllowed(itemKey)) continue; + + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, this.actionSource); + if (request == 0L) continue; + + if (tagAutoPullTest != null && !tagAutoPullTest.test(new GenericStack(itemKey, amount))) continue; + + if (top.size() < maxSlots) { + top.offer(entry); + } else if (amount > Objects.requireNonNull(top.peek()).getLongValue()) { + top.poll(); + top.offer(entry); + } + } + + int itemAmount = top.size(); + + int index; + for (index = 0; index < maxSlots && !top.isEmpty(); index++) { + Object2LongMap.Entry entry = top.poll(); + AEKey what = entry.getKey(); + long amount = entry.getLongValue(); + + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, this.actionSource); + + ExportOnlyAEItemSlot slot = this.aeItemHandler.getInventory()[itemAmount - index - 1]; + slot.setConfig(new GenericStack(what, 1L)); + slot.setStock(new GenericStack(what, request)); + } + + this.aeItemHandler.clearInventory(index); + markDirty(); + self().getHolder().self().setChanged(); + } + + @Override + public void setAutoPullTest(Predicate autoPullTest) { + this.tagAutoPullTest = autoPullTest; + super.setAutoPullTest(autoPullTest); + } + + @Override + protected InteractionResult onScrewdriverClick(Player playerIn, InteractionHand hand, Direction gridSide, + BlockHitResult hitResult) { + if (!this.isRemote()) { + playerIn.sendSystemMessage(Component.literal("This bus is always Tag-Mode (no manual/autoPull toggle).")); + } + return InteractionResult.sidedSuccess(this.isRemote()); + } + + @Override + protected CompoundTag writeConfigToTag() { + CompoundTag tag = super.writeConfigToTag(); + tag.putString("WhitelistExpr", whitelistExpr == null ? "" : whitelistExpr); + tag.putString("BlacklistExpr", blacklistExpr == null ? "" : blacklistExpr); + tag.putBoolean("TagMode", true); + return tag; + } + + @Override + protected void readConfigFromTag(CompoundTag tag) { + super.readConfigFromTag(tag); + + if (!isRemote() && super.isAutoPull()) { + super.setAutoPull(false); + } + + if (tag.contains("WhitelistExpr")) whitelistExpr = tag.getString("WhitelistExpr"); + if (tag.contains("BlacklistExpr")) blacklistExpr = tag.getString("BlacklistExpr"); + + invalidateFilterCaches(); + + if (!isRemote() && updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateInventorySubscription(); + } + } + + @Override + public Widget createUIWidget() { + int visibleRows = 3; + int rowHeight = 40; + int listStartY = 85; + int listStartX = 10; + int scrollbarX = 158; + + final List allRowWidgets = new ArrayList<>(); + + Consumer onScrollChanged = (currentScroll) -> { + for (int i = 0; i < allRowWidgets.size(); i++) { + AEItemConfigWidget row = allRowWidgets.get(i); + int relativeIndex = i - currentScroll; + if (relativeIndex >= 0 && relativeIndex < visibleRows) { + row.setVisible(true); + row.setSelfPosition(listStartX, listStartY + (relativeIndex * rowHeight)); + } else { + row.setVisible(false); + } + } + }; + + SimpleScrollbarWidget scrollbar = new SimpleScrollbarWidget( + scrollbarX, + listStartY, + visibleRows * rowHeight - 4, + onScrollChanged); + + scrollbar.setRange(0, TOTAL_ROWS - visibleRows, 1); + + WidgetGroup group = new WidgetGroup(new Position(0, 0), new Size(178, 200)) { + + @Override + public boolean mouseWheelMove(double mouseX, double mouseY, double wheelDelta) { + if (super.mouseWheelMove(mouseX, mouseY, wheelDelta)) { + return true; + } + if (isMouseOver(getPositionX(), getPositionY(), getSizeWidth(), getSizeHeight(), mouseX, mouseY)) { + return scrollbar.mouseWheelMove(mouseX, mouseY, wheelDelta); + } + return false; + } + }; + + int y = 6; + group.addWidget(new LabelWidget(3, y, () -> this.isOnline ? + "gtceu.gui.me_network.online" : + "gtceu.gui.me_network.offline")); + + group.addWidget(new ToggleButtonWidget(176 - 55, 2, 50, 16, () -> false, pressed -> { + whitelistExpr = pressed ? Wltmp : whitelistExpr; + blacklistExpr = pressed ? Bltmp : blacklistExpr; + + invalidateFilterCaches(); + + if (updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateInventorySubscription(); + } + }).setTexture( + new GuiTextureGroup(GuiTextures.VANILLA_BUTTON, new TextTexture("Confirm")), + new GuiTextureGroup(GuiTextures.VANILLA_BUTTON, new TextTexture("Confirm")))); + + y += 14; + var WLField = new MultilineTextField( + 7, y, 160, 25, + () -> Wltmp, + v -> { Wltmp = v; }, + Component.literal("Whitelist tags...")); + group.addWidget(WLField); + + y += 29; + var BLField = new MultilineTextField( + 7, y, 160, 25, + () -> Bltmp, + v -> { Bltmp = v; }, + Component.literal("Blacklist tags...")); + group.addWidget(BLField); + + WLField.setDirectly(whitelistExpr); + BLField.setDirectly(blacklistExpr); + + for (int i = 0; i < TOTAL_ROWS; i++) { + final int currentRowIndex = i; + final int startSlotIndex = currentRowIndex * SLOTS_PER_ROW; + + AtomicInteger localSlotCounter = new AtomicInteger(0); + + ExportOnlyAEItemList viewHandler = new ExportOnlyAEItemList( + this, + SLOTS_PER_ROW, + () -> { + int globalSlotIndex = startSlotIndex + localSlotCounter.getAndIncrement(); + if (globalSlotIndex < this.aeItemHandler.getSlots()) { + return this.aeItemHandler.getInventory()[globalSlotIndex]; + } + return null; + }) { + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean isAutoPull() { + return true; + } + }; + + var root = (MEEnlargedTagStockingInputBusPartMachine.ExportOnlyAEStockingItemList) MEEnlargedTagStockingInputBusPartMachine.this.aeItemHandler; + Runnable rootCb = root::fireContentsChanged; + + for (int s = 0; s < SLOTS_PER_ROW; s++) { + int global = startSlotIndex + s; + if (global >= 0 && global < MEEnlargedTagStockingInputBusPartMachine.this.aeItemHandler.getSlots()) { + MEEnlargedTagStockingInputBusPartMachine.this.aeItemHandler.getInventory()[global] + .setOnContentsChanged(rootCb); + } + } + + AEItemConfigWidget rowWidget = new AEItemConfigWidget(listStartX, listStartY, viewHandler) { + + @Override + public boolean hasStackInConfig(GenericStack stack) { + return MEEnlargedTagStockingInputBusPartMachine.this.aeItemHandler.hasStackInConfig(stack, true); + } + }; + + rowWidget.setVisible(false); + allRowWidgets.add(rowWidget); + group.addWidget(rowWidget); + } + + group.addWidget(scrollbar); + onScrollChanged.accept(0); + + return group; + } + + // We can't call super to avoid the auto-pull toggle... + @Override + public void attachConfigurators(ConfiguratorPanel configuratorPanel) { + configuratorPanel.attachConfigurators(new IFancyConfiguratorButton.Toggle( + GuiTextures.BUTTON_POWER.getSubTexture(0, 0, 1, 0.5), + GuiTextures.BUTTON_POWER.getSubTexture(0, 0.5, 1, 0.5), + this::isWorkingEnabled, (clickData, pressed) -> this.setWorkingEnabled(pressed)) + .setTooltipsSupplier(pressed -> List.of( + Component.translatable( + pressed ? "behaviour.soft_hammer.enabled" : "behaviour.soft_hammer.disabled")))); + + for (var direction : Direction.values()) { + if (this.getCoverContainer().hasCover(direction)) { + var configurator = this.getCoverContainer().getCoverAtSide(direction).getConfigurator(); + if (configurator != null) + configuratorPanel.attachConfigurators(configurator); + } + } + + configuratorPanel.attachConfigurators(new IFancyConfiguratorButton.Toggle( + GuiTextures.BUTTON_DISTINCT_BUSES.getSubTexture(0, 0.5, 1, 0.5), + GuiTextures.BUTTON_DISTINCT_BUSES.getSubTexture(0, 0, 1, 0.5), + this::isDistinct, (clickData, pressed) -> setDistinct(pressed)) + .setTooltipsSupplier(pressed -> List.of( + Component.translatable("gtceu.multiblock.universal.distinct") + .setStyle(Style.EMPTY.withColor(ChatFormatting.YELLOW)) + .append(Component.translatable(pressed ? "gtceu.multiblock.universal.distinct.yes" : + "gtceu.multiblock.universal.distinct.no"))))); + + configuratorPanel.attachConfigurators(new CircuitFancyConfigurator(circuitInventory.storage)); + configuratorPanel.attachConfigurators(new AutoStockingFancyConfigurator(this)); + } + + private class ExportOnlyAEStockingItemList extends ExportOnlyAEItemList { + + public ExportOnlyAEStockingItemList(MetaMachine holder, int slots) { + super(holder, slots, + () -> MEEnlargedTagStockingInputBusPartMachine.this.new ExportOnlyAETagStockingItemSlot()); + } + + @Override + public boolean isAutoPull() { + return true; + } + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean hasStackInConfig(GenericStack stack, boolean checkExternal) { + boolean inThisBus = super.hasStackInConfig(stack, false); + if (inThisBus) return true; + return checkExternal && MEEnlargedTagStockingInputBusPartMachine.this.testConfiguredInOtherPart(stack); + } + + public void fireContentsChanged() { + super.onContentsChanged(); + } + } + + private class ExportOnlyAETagStockingItemSlot extends ExportOnlyAEItemSlot { + + public ExportOnlyAETagStockingItemSlot() {} + + public ExportOnlyAETagStockingItemSlot(@Nullable GenericStack config, GenericStack stock) { + super(config, stock); + } + + @Override + public ItemStack extractItem(int slot, int amount, boolean simulate) { + if (slot == 0 && this.stock != null && this.config != null) { + if (!MEEnlargedTagStockingInputBusPartMachine.this.isOnline()) return ItemStack.EMPTY; + + IGrid grid = MEEnlargedTagStockingInputBusPartMachine.this.getMainNode().getGrid(); + if (grid == null) return ItemStack.EMPTY; + + MEStorage aeNetwork = grid.getStorageService().getInventory(); + Actionable action = simulate ? Actionable.SIMULATE : Actionable.MODULATE; + AEKey key = this.config.what(); + + long extracted = aeNetwork.extract(key, amount, action, + MEEnlargedTagStockingInputBusPartMachine.this.actionSource); + + if (extracted > 0L) { + ItemStack result; + if (key instanceof AEItemKey itemKey) result = itemKey.toStack((int) extracted); + else result = ItemStack.EMPTY; + + if (!simulate) { + this.stock = ExportOnlyAESlot.copy(this.stock, this.stock.amount() - extracted); + if (this.stock.amount() == 0L) this.stock = null; + if (this.onContentsChanged != null) this.onContentsChanged.run(); + } + return result; + } + } + return ItemStack.EMPTY; + } + + @Override + public ExportOnlyAETagStockingItemSlot copy() { + return MEEnlargedTagStockingInputBusPartMachine.this.new ExportOnlyAETagStockingItemSlot( + this.config == null ? null : copy(this.config), + this.stock == null ? null : copy(this.stock)); + } + } +} diff --git a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputHatchPartMachine.java b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputHatchPartMachine.java new file mode 100644 index 0000000..4c83e35 --- /dev/null +++ b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/MEEnlargedTagStockingInputHatchPartMachine.java @@ -0,0 +1,536 @@ +package net.neganote.gtutilities.integration.ae2.machine; + +import com.gregtechceu.gtceu.api.gui.GuiTextures; +import com.gregtechceu.gtceu.api.gui.fancy.ConfiguratorPanel; +import com.gregtechceu.gtceu.api.gui.fancy.IFancyConfiguratorButton; +import com.gregtechceu.gtceu.api.gui.widget.ToggleButtonWidget; +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.api.machine.fancyconfigurator.AutoStockingFancyConfigurator; +import com.gregtechceu.gtceu.api.machine.fancyconfigurator.CircuitFancyConfigurator; +import com.gregtechceu.gtceu.api.machine.trait.NotifiableFluidTank; +import com.gregtechceu.gtceu.config.ConfigHolder; +import com.gregtechceu.gtceu.integration.ae2.gui.widget.AEFluidConfigWidget; +import com.gregtechceu.gtceu.integration.ae2.machine.MEStockingHatchPartMachine; +import com.gregtechceu.gtceu.integration.ae2.slot.*; +import com.gregtechceu.gtceu.integration.ae2.utils.AEUtil; + +import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; +import com.lowdragmc.lowdraglib.gui.texture.TextTexture; +import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; +import com.lowdragmc.lowdraglib.gui.widget.Widget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.lowdragmc.lowdraglib.utils.Position; +import com.lowdragmc.lowdraglib.utils.Size; + +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraftforge.fluids.FluidStack; +import net.neganote.gtutilities.common.gui.widgets.MultilineTextField; +import net.neganote.gtutilities.common.gui.widgets.SimpleScrollbarWidget; +import net.neganote.gtutilities.config.UtilConfig; +import net.neganote.gtutilities.utils.TagMatcher; + +import appeng.api.config.Actionable; +import appeng.api.networking.IGrid; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.storage.MEStorage; +import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class MEEnlargedTagStockingInputHatchPartMachine extends MEStockingHatchPartMachine { + + protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + MEEnlargedTagStockingInputHatchPartMachine.class, MEStockingHatchPartMachine.MANAGED_FIELD_HOLDER); + + @Persisted + @DescSynced + protected String whitelistExpr = ""; + @Persisted + @DescSynced + protected String blacklistExpr = ""; + @DescSynced + protected String Wltmp = ""; + @DescSynced + protected String Bltmp = ""; + + private Predicate tagAutoPullTest = ($) -> true; + + private transient String wlLast = null; + private transient String blLast = null; + private transient TagMatcher.Compiled wlCompiled = TagMatcher.compile(""); + private transient TagMatcher.Compiled blCompiled = TagMatcher.compile(""); + + private final transient Object2ByteOpenHashMap decisionCache = new Object2ByteOpenHashMap<>(); + private static final int DECISION_CACHE_LIMIT = 16384; + + private static final int SLOTS_PER_ROW = 8; + private static final int TOTAL_ROWS = Math.min(64, UtilConfig.INSTANCE.features.enlargedStockingSizeRows); + private static final int TOTAL_SLOTS = SLOTS_PER_ROW * TOTAL_ROWS; + + public MEEnlargedTagStockingInputHatchPartMachine(IMachineBlockEntity holder, Object... args) { + super(holder, args); + decisionCache.defaultReturnValue((byte) -1); + } + + @Override + public ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + @Override + protected NotifiableFluidTank createTank(int initialCapacity, int slots, Object... args) { + this.aeFluidHandler = new ExportOnlyAEStockingFluidList(this, TOTAL_SLOTS); + return this.aeFluidHandler; + } + + @Override + public void onLoad() { + super.onLoad(); + if (!isRemote()) { + if (super.isAutoPull()) { + super.setAutoPull(false); + } + + invalidateFilterCaches(); + + if (updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateTankSubscription(); + } + } + } + + @Override + public void autoIO() { + if (!isRemote() && super.isAutoPull()) { + super.setAutoPull(false); + } + + if (this.getTicksPerCycle() == 0) { + this.setTicksPerCycle(ConfigHolder.INSTANCE.compat.ae2.updateIntervals); + } + + if (this.getOffsetTimer() % (long) this.getTicksPerCycle() == 0L) { + if (!isRemote() && updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateTankSubscription(); + } + } + } + + private static String norm(@Nullable String s) { + if (s == null) return ""; + s = s.trim(); + return s; + } + + private void invalidateFilterCaches() { + wlLast = null; + blLast = null; + decisionCache.clear(); + } + + private void ensureCompiledUpToDate() { + String wl = norm(whitelistExpr); + String bl = norm(blacklistExpr); + + if (!Objects.equals(wl, wlLast)) { + wlLast = wl; + wlCompiled = TagMatcher.compile(wl); + decisionCache.clear(); + } + if (!Objects.equals(bl, blLast)) { + blLast = bl; + blCompiled = TagMatcher.compile(bl); + decisionCache.clear(); + } + + if (decisionCache.size() > DECISION_CACHE_LIMIT) { + decisionCache.clear(); + } + } + + protected boolean isAllowed(AEFluidKey key) { + ensureCompiledUpToDate(); + + if ((wlLast == null || wlLast.isEmpty()) && (blLast == null || blLast.isEmpty())) return false; + + byte cached = decisionCache.getByte(key); + if (cached != -1) { + return cached == 1; + } + + boolean allowed; + if (blLast != null && !blLast.isEmpty() && blCompiled != null && blCompiled.isValid()) { + if (TagMatcher.doesFluidMatch(key, blCompiled)) { + decisionCache.put(key, (byte) 0); + return false; + } + } + + if (wlLast != null && !wlLast.isEmpty() && wlCompiled != null && wlCompiled.isValid()) { + allowed = TagMatcher.doesFluidMatch(key, wlCompiled); + } else { + allowed = false; + } + + decisionCache.put(key, allowed ? (byte) 1 : (byte) 0); + return allowed; + } + + protected void refreshListFromTags() { + IGrid grid = this.getMainNode().getGrid(); + if (grid == null) { + this.aeFluidHandler.clearInventory(0); + return; + } + + MEStorage networkStorage = grid.getStorageService().getInventory(); + var counter = networkStorage.getAvailableStacks(); + + PriorityQueue> top = new PriorityQueue<>( + Comparator.comparingLong(Object2LongMap.Entry::getLongValue)); + + for (Object2LongMap.Entry entry : counter) { + long amount = entry.getLongValue(); + AEKey what = entry.getKey(); + + if (amount <= 0) continue; + if (!(what instanceof AEFluidKey fluidKey)) continue; + + if (!isAllowed(fluidKey)) continue; + + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, this.actionSource); + if (request == 0L) continue; + + if (tagAutoPullTest != null && !tagAutoPullTest.test(new GenericStack(fluidKey, amount))) continue; + + if (top.size() < TOTAL_SLOTS) { + top.offer(entry); + } else if (amount > Objects.requireNonNull(top.peek()).getLongValue()) { + top.poll(); + top.offer(entry); + } + } + + int itemAmount = top.size(); + + int index; + for (index = 0; index < TOTAL_SLOTS && !top.isEmpty(); index++) { + Object2LongMap.Entry entry = top.poll(); + AEKey what = entry.getKey(); + long amount = entry.getLongValue(); + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, this.actionSource); + + ExportOnlyAEFluidSlot slot = this.aeFluidHandler.getInventory()[itemAmount - index - 1]; + slot.setConfig(new GenericStack(what, 1L)); + slot.setStock(new GenericStack(what, request)); + } + this.aeFluidHandler.clearInventory(index); + markDirty(); + self().getHolder().self().setChanged(); + } + + @Override + public void setAutoPullTest(Predicate autoPullTest) { + this.tagAutoPullTest = autoPullTest; + super.setAutoPullTest(autoPullTest); + } + + @Override + protected InteractionResult onScrewdriverClick(Player playerIn, InteractionHand hand, Direction gridSide, + BlockHitResult hitResult) { + if (!this.isRemote()) { + playerIn.sendSystemMessage(Component.literal("This bus is always Tag-Mode (no manual/autoPull toggle).")); + } + return InteractionResult.sidedSuccess(this.isRemote()); + } + + @Override + protected CompoundTag writeConfigToTag() { + CompoundTag tag = super.writeConfigToTag(); + tag.putString("WhitelistExpr", whitelistExpr == null ? "" : whitelistExpr); + tag.putString("BlacklistExpr", blacklistExpr == null ? "" : blacklistExpr); + tag.putBoolean("TagMode", true); + return tag; + } + + @Override + protected void readConfigFromTag(CompoundTag tag) { + super.readConfigFromTag(tag); + if (!isRemote() && super.isAutoPull()) { + super.setAutoPull(false); + } + + if (tag.contains("WhitelistExpr")) whitelistExpr = tag.getString("WhitelistExpr"); + if (tag.contains("BlacklistExpr")) blacklistExpr = tag.getString("BlacklistExpr"); + + invalidateFilterCaches(); + + if (!isRemote() && updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateTankSubscription(); + } + } + + @Override + public Widget createUIWidget() { + int visibleRows = 3; + int rowHeight = 40; + int listStartY = 85; + int listStartX = 10; + int scrollbarX = 158; + + final List allRowWidgets = new ArrayList<>(); + + Consumer onScrollChanged = (currentScroll) -> { + for (int i = 0; i < allRowWidgets.size(); i++) { + AEFluidConfigWidget row = allRowWidgets.get(i); + int relativeIndex = i - currentScroll; + if (relativeIndex >= 0 && relativeIndex < visibleRows) { + row.setVisible(true); + row.setSelfPosition(listStartX, listStartY + (relativeIndex * rowHeight)); + } else { + row.setVisible(false); + } + } + }; + + SimpleScrollbarWidget scrollbar = new SimpleScrollbarWidget( + scrollbarX, + listStartY, + visibleRows * rowHeight - 4, + onScrollChanged); + + scrollbar.setRange(0, TOTAL_ROWS - visibleRows, 1); + + WidgetGroup group = new WidgetGroup(new Position(0, 0), new Size(178, 200)) { + + @Override + public boolean mouseWheelMove(double mouseX, double mouseY, double wheelDelta) { + if (super.mouseWheelMove(mouseX, mouseY, wheelDelta)) { + return true; + } + if (isMouseOver(getPositionX(), getPositionY(), getSizeWidth(), getSizeHeight(), mouseX, mouseY)) { + return scrollbar.mouseWheelMove(mouseX, mouseY, wheelDelta); + } + return false; + } + }; + + int y = 6; + group.addWidget(new LabelWidget(3, y, () -> this.isOnline ? + "gtceu.gui.me_network.online" : + "gtceu.gui.me_network.offline")); + + group.addWidget(new ToggleButtonWidget(176 - 55, 2, 50, 16, () -> false, pressed -> { + whitelistExpr = pressed ? Wltmp : whitelistExpr; + blacklistExpr = pressed ? Bltmp : blacklistExpr; + + invalidateFilterCaches(); + + if (updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateTankSubscription(); + } + }).setTexture(new GuiTextureGroup(GuiTextures.VANILLA_BUTTON, new TextTexture("Confirm")), + new GuiTextureGroup(GuiTextures.VANILLA_BUTTON, new TextTexture("Confirm")))); + + y += 14; + var WLField = new MultilineTextField( + 7, y, 160, 25, + () -> Wltmp, + v -> { Wltmp = v; }, + Component.literal("Whitelist tags...")); + group.addWidget(WLField); + + y += 29; + var BLField = new MultilineTextField( + 7, y, 160, 25, + () -> Bltmp, + v -> { Bltmp = v; }, + Component.literal("Blacklist tags...")); + group.addWidget(BLField); + + WLField.setDirectly(whitelistExpr); + BLField.setDirectly(blacklistExpr); + + for (int i = 0; i < TOTAL_ROWS; i++) { + final int currentRowIndex = i; + final int startSlotIndex = currentRowIndex * SLOTS_PER_ROW; + + AtomicInteger localSlotCounter = new AtomicInteger(0); + + ExportOnlyAEFluidList viewHandler = new ExportOnlyAEFluidList( + this, + SLOTS_PER_ROW, + () -> { + int globalSlotIndex = startSlotIndex + localSlotCounter.getAndIncrement(); + if (globalSlotIndex < this.aeFluidHandler.getTanks()) { + return this.aeFluidHandler.getInventory()[globalSlotIndex]; + } + return null; + }) { + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean isAutoPull() { + return true; + } + }; + + var root = (MEEnlargedTagStockingInputHatchPartMachine.ExportOnlyAEStockingFluidList) MEEnlargedTagStockingInputHatchPartMachine.this.aeFluidHandler; + Runnable rootCb = root::fireContentsChanged; + + for (int s = 0; s < SLOTS_PER_ROW; s++) { + int global = startSlotIndex + s; + if (global >= 0 && global < MEEnlargedTagStockingInputHatchPartMachine.this.aeFluidHandler.getTanks()) { + MEEnlargedTagStockingInputHatchPartMachine.this.aeFluidHandler.getInventory()[global] + .setOnContentsChanged(rootCb); + } + } + + AEFluidConfigWidget rowWidget = new AEFluidConfigWidget(listStartX, listStartY, viewHandler) { + + @Override + public boolean hasStackInConfig(GenericStack stack) { + return MEEnlargedTagStockingInputHatchPartMachine.this.aeFluidHandler.hasStackInConfig(stack, true); + } + }; + rowWidget.setVisible(false); + allRowWidgets.add(rowWidget); + group.addWidget(rowWidget); + } + + group.addWidget(scrollbar); + onScrollChanged.accept(0); + + return group; + } + + // We can't call super to avoid the auto-pull toggle... + @Override + public void attachConfigurators(ConfiguratorPanel configuratorPanel) { + configuratorPanel.attachConfigurators(new IFancyConfiguratorButton.Toggle( + GuiTextures.BUTTON_POWER.getSubTexture(0, 0, 1, 0.5), + GuiTextures.BUTTON_POWER.getSubTexture(0, 0.5, 1, 0.5), + this::isWorkingEnabled, (clickData, pressed) -> this.setWorkingEnabled(pressed)) + .setTooltipsSupplier(pressed -> List.of( + Component.translatable( + pressed ? "behaviour.soft_hammer.enabled" : "behaviour.soft_hammer.disabled")))); + for (var direction : Direction.values()) { + if (this.getCoverContainer().hasCover(direction)) { + var configurator = this.getCoverContainer().getCoverAtSide(direction).getConfigurator(); + if (configurator != null) + configuratorPanel.attachConfigurators(configurator); + } + } + configuratorPanel.attachConfigurators(new CircuitFancyConfigurator(circuitInventory.storage)); + configuratorPanel.attachConfigurators(new AutoStockingFancyConfigurator(this)); + } + + private class ExportOnlyAEStockingFluidList extends ExportOnlyAEFluidList { + + public ExportOnlyAEStockingFluidList(MetaMachine holder, int slots) { + super(holder, slots, + () -> MEEnlargedTagStockingInputHatchPartMachine.this.new ExportOnlyAETagStockingFluidSlot()); + } + + @Override + public boolean isAutoPull() { + return true; + } + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean hasStackInConfig(GenericStack stack, boolean checkExternal) { + boolean inThisBus = super.hasStackInConfig(stack, false); + if (inThisBus) return true; + return checkExternal && MEEnlargedTagStockingInputHatchPartMachine.this.testConfiguredInOtherPart(stack); + } + + public void fireContentsChanged() { + super.onContentsChanged(); + } + } + + private class ExportOnlyAETagStockingFluidSlot extends ExportOnlyAEFluidSlot { + + public ExportOnlyAETagStockingFluidSlot() { + super(); + } + + public ExportOnlyAETagStockingFluidSlot(@Nullable GenericStack config, @Nullable GenericStack stock) { + super(config, stock); + } + + @Override + public ExportOnlyAEFluidSlot copy() { + return new MEEnlargedTagStockingInputHatchPartMachine.ExportOnlyAETagStockingFluidSlot( + this.config == null ? null : copy(this.config), + this.stock == null ? null : copy(this.stock)); + } + + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + if (this.stock != null && this.config != null) { + if (!isOnline()) return FluidStack.EMPTY; + MEStorage aeNetwork = getMainNode().getGrid().getStorageService().getInventory(); + + Actionable actionable = action.simulate() ? Actionable.SIMULATE : Actionable.MODULATE; + var key = config.what(); + long extracted = aeNetwork.extract(key, maxDrain, actionable, actionSource); + + if (extracted > 0) { + FluidStack resultStack = key instanceof AEFluidKey fluidKey ? + AEUtil.toFluidStack(fluidKey, extracted) : FluidStack.EMPTY; + if (action.execute()) { + this.stock = ExportOnlyAESlot.copy(stock, stock.amount() - extracted); + if (this.stock.amount() == 0) { + this.stock = null; + } + if (this.onContentsChanged != null) { + this.onContentsChanged.run(); + } + } + return resultStack; + } + } + return FluidStack.EMPTY; + } + } +} diff --git a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputBusPartMachine.java b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputBusPartMachine.java new file mode 100644 index 0000000..9eda414 --- /dev/null +++ b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputBusPartMachine.java @@ -0,0 +1,462 @@ +package net.neganote.gtutilities.integration.ae2.machine; + +import com.gregtechceu.gtceu.api.gui.GuiTextures; +import com.gregtechceu.gtceu.api.gui.fancy.ConfiguratorPanel; +import com.gregtechceu.gtceu.api.gui.fancy.IFancyConfiguratorButton; +import com.gregtechceu.gtceu.api.gui.widget.ToggleButtonWidget; +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.api.machine.fancyconfigurator.AutoStockingFancyConfigurator; +import com.gregtechceu.gtceu.api.machine.fancyconfigurator.CircuitFancyConfigurator; +import com.gregtechceu.gtceu.api.machine.trait.NotifiableItemStackHandler; +import com.gregtechceu.gtceu.config.ConfigHolder; +import com.gregtechceu.gtceu.integration.ae2.gui.widget.AEItemConfigWidget; +import com.gregtechceu.gtceu.integration.ae2.machine.MEStockingBusPartMachine; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEItemList; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAEItemSlot; +import com.gregtechceu.gtceu.integration.ae2.slot.ExportOnlyAESlot; + +import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; +import com.lowdragmc.lowdraglib.gui.texture.TextTexture; +import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; +import com.lowdragmc.lowdraglib.gui.widget.Widget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.lowdragmc.lowdraglib.utils.Position; +import com.lowdragmc.lowdraglib.utils.Size; + +import net.minecraft.ChatFormatting; +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.BlockHitResult; +import net.neganote.gtutilities.common.gui.widgets.MultilineTextField; +import net.neganote.gtutilities.utils.TagMatcher; + +import appeng.api.config.Actionable; +import appeng.api.networking.IGrid; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.storage.MEStorage; +import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import org.jetbrains.annotations.Nullable; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.function.Predicate; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class METagStockingInputBusPartMachine extends MEStockingBusPartMachine { + + protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + METagStockingInputBusPartMachine.class, MEStockingBusPartMachine.MANAGED_FIELD_HOLDER); + + @Persisted + @DescSynced + protected String whitelistExpr = ""; + @Persisted + @DescSynced + protected String blacklistExpr = ""; + @DescSynced + protected String Wltmp = ""; + @DescSynced + protected String Bltmp = ""; + + private Predicate tagAutoPullTest = ($) -> true; + + private transient String wlLast = null; + private transient String blLast = null; + private transient TagMatcher.Compiled wlCompiled = TagMatcher.compile(""); + private transient TagMatcher.Compiled blCompiled = TagMatcher.compile(""); + + private final transient Object2ByteOpenHashMap decisionCache = new Object2ByteOpenHashMap<>(); + private static final int DECISION_CACHE_LIMIT = 8192; + + public METagStockingInputBusPartMachine(IMachineBlockEntity holder, Object... args) { + super(holder, args); + decisionCache.defaultReturnValue((byte) -1); + } + + @Override + public ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + @Override + protected NotifiableItemStackHandler createInventory(Object... args) { + this.aeItemHandler = new ExportOnlyAETagStockingItemList(this, CONFIG_SIZE); + return this.aeItemHandler; + } + + @Override + public void onLoad() { + super.onLoad(); + if (!isRemote()) { + if (super.isAutoPull()) { + super.setAutoPull(false); + } + + invalidateFilterCaches(); + + if (updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateInventorySubscription(); + } + } + } + + @Override + public void autoIO() { + if (!isRemote() && super.isAutoPull()) { + super.setAutoPull(false); + } + + if (this.getTicksPerCycle() == 0) { + this.setTicksPerCycle(ConfigHolder.INSTANCE.compat.ae2.updateIntervals); + } + + if (this.getOffsetTimer() % (long) this.getTicksPerCycle() == 0L) { + if (!isRemote() && updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateInventorySubscription(); + } + } + } + + private static String norm(@Nullable String s) { + if (s == null) return ""; + s = s.trim(); + return s; + } + + private void invalidateFilterCaches() { + wlLast = null; + blLast = null; + decisionCache.clear(); + } + + private void ensureCompiledUpToDate() { + String wl = norm(whitelistExpr); + String bl = norm(blacklistExpr); + + if (!Objects.equals(wl, wlLast)) { + wlLast = wl; + wlCompiled = TagMatcher.compile(wl); + decisionCache.clear(); + } + if (!Objects.equals(bl, blLast)) { + blLast = bl; + blCompiled = TagMatcher.compile(bl); + decisionCache.clear(); + } + + if (decisionCache.size() > DECISION_CACHE_LIMIT) { + decisionCache.clear(); + } + } + + protected boolean isAllowed(AEItemKey key) { + ensureCompiledUpToDate(); + + if ((wlLast == null || wlLast.isEmpty()) && (blLast == null || blLast.isEmpty())) return false; + + byte cached = decisionCache.getByte(key); + if (cached != -1) { + return cached == 1; + } + + boolean allowed; + if (blLast != null && !blLast.isEmpty() && blCompiled != null && blCompiled.isValid()) { + if (TagMatcher.doesItemMatch(key, blCompiled)) { + decisionCache.put(key, (byte) 0); + return false; + } + } + + if (wlLast != null && !wlLast.isEmpty() && wlCompiled != null && wlCompiled.isValid()) { + allowed = TagMatcher.doesItemMatch(key, wlCompiled); + } else { + allowed = false; + } + + decisionCache.put(key, allowed ? (byte) 1 : (byte) 0); + return allowed; + } + + protected void refreshListFromTags() { + IGrid grid = this.getMainNode().getGrid(); + if (grid == null) { + this.aeItemHandler.clearInventory(0); + return; + } + + MEStorage networkStorage = grid.getStorageService().getInventory(); + var counter = networkStorage.getAvailableStacks(); + + PriorityQueue> top = new PriorityQueue<>( + Comparator.comparingLong(Object2LongMap.Entry::getLongValue)); + + for (Object2LongMap.Entry entry : counter) { + long amount = entry.getLongValue(); + AEKey what = entry.getKey(); + + if (amount <= 0) continue; + if (!(what instanceof AEItemKey itemKey)) continue; + + if (!isAllowed(itemKey)) continue; + + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, this.actionSource); + if (request == 0L) continue; + + if (tagAutoPullTest != null && !tagAutoPullTest.test(new GenericStack(itemKey, amount))) continue; + + if (top.size() < CONFIG_SIZE) { + top.offer(entry); + } else if (amount > Objects.requireNonNull(top.peek()).getLongValue()) { + top.poll(); + top.offer(entry); + } + } + + int itemAmount = top.size(); + + int index; + for (index = 0; index < CONFIG_SIZE && !top.isEmpty(); index++) { + Object2LongMap.Entry entry = top.poll(); + AEKey what = entry.getKey(); + long amount = entry.getLongValue(); + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, this.actionSource); + + ExportOnlyAEItemSlot slot = this.aeItemHandler.getInventory()[itemAmount - index - 1]; + slot.setConfig(new GenericStack(what, 1L)); + slot.setStock(new GenericStack(what, request)); + } + this.aeItemHandler.clearInventory(index); + markDirty(); + self().getHolder().self().setChanged(); + } + + @Override + public Widget createUIWidget() { + WidgetGroup group = new WidgetGroup(new Position(0, 0), new Size(176, 150)); + + int y = 6; + group.addWidget(new LabelWidget(3, y, () -> this.isOnline ? + "gtceu.gui.me_network.online" : + "gtceu.gui.me_network.offline")); + + group.addWidget(new ToggleButtonWidget(176 - 55, 2, 50, 16, () -> false, pressed -> { + whitelistExpr = pressed ? Wltmp : whitelistExpr; + blacklistExpr = pressed ? Bltmp : blacklistExpr; + + invalidateFilterCaches(); + + if (updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateInventorySubscription(); + } + }).setTexture(new GuiTextureGroup(GuiTextures.VANILLA_BUTTON, new TextTexture("Confirm")), + new GuiTextureGroup(GuiTextures.VANILLA_BUTTON, new TextTexture("Confirm")))); + + y += 14; + var WLField = new MultilineTextField( + 7, y, 160, 25, + () -> Wltmp, + v -> { + Wltmp = v; + }, + Component.literal("Whitelist tags...")); + group.addWidget(WLField); + + y += 29; + var BLField = new MultilineTextField( + 7, y, 160, 25, + () -> Bltmp, + v -> { + Bltmp = v; + }, + Component.literal("Blacklist tags...")); + group.addWidget(BLField); + + WLField.setDirectly(whitelistExpr); + BLField.setDirectly(blacklistExpr); + + y += 29; + + group.addWidget(new AEItemConfigWidget(15, y, this.aeItemHandler) { + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean isAutoPull() { + return true; + } + }); + + return group; + } + + @Override + protected InteractionResult onScrewdriverClick(Player playerIn, InteractionHand hand, Direction gridSide, + BlockHitResult hitResult) { + if (!this.isRemote()) { + playerIn.sendSystemMessage(Component.literal("This bus is always Tag-Mode (no manual/autoPull toggle).")); + } + return InteractionResult.sidedSuccess(this.isRemote()); + } + + @Override + protected CompoundTag writeConfigToTag() { + CompoundTag tag = super.writeConfigToTag(); + tag.putString("WhitelistExpr", whitelistExpr); + tag.putString("BlacklistExpr", blacklistExpr); + tag.putBoolean("TagMode", true); + return tag; + } + + @Override + protected void readConfigFromTag(CompoundTag tag) { + super.readConfigFromTag(tag); + if (!isRemote() && super.isAutoPull()) { + super.setAutoPull(false); + } + + if (tag.contains("WhitelistExpr")) whitelistExpr = tag.getString("WhitelistExpr"); + if (tag.contains("BlacklistExpr")) blacklistExpr = tag.getString("BlacklistExpr"); + + invalidateFilterCaches(); + + if (!isRemote() && updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateInventorySubscription(); + } + } + + @Override + public void setAutoPullTest(Predicate autoPullTest) { + this.tagAutoPullTest = autoPullTest; + super.setAutoPullTest(autoPullTest); + } + + // We can't call super to avoid the auto-pull toggle... + @Override + public void attachConfigurators(ConfiguratorPanel configuratorPanel) { + configuratorPanel.attachConfigurators(new IFancyConfiguratorButton.Toggle( + GuiTextures.BUTTON_POWER.getSubTexture(0, 0, 1, 0.5), + GuiTextures.BUTTON_POWER.getSubTexture(0, 0.5, 1, 0.5), + this::isWorkingEnabled, (clickData, pressed) -> this.setWorkingEnabled(pressed)) + .setTooltipsSupplier(pressed -> List.of( + Component.translatable( + pressed ? "behaviour.soft_hammer.enabled" : "behaviour.soft_hammer.disabled")))); + for (var direction : Direction.values()) { + if (this.getCoverContainer().hasCover(direction)) { + var configurator = this.getCoverContainer().getCoverAtSide(direction).getConfigurator(); + if (configurator != null) + configuratorPanel.attachConfigurators(configurator); + } + } + configuratorPanel.attachConfigurators(new IFancyConfiguratorButton.Toggle( + GuiTextures.BUTTON_DISTINCT_BUSES.getSubTexture(0, 0.5, 1, 0.5), + GuiTextures.BUTTON_DISTINCT_BUSES.getSubTexture(0, 0, 1, 0.5), + this::isDistinct, (clickData, pressed) -> setDistinct(pressed)) + .setTooltipsSupplier(pressed -> List.of( + Component.translatable("gtceu.multiblock.universal.distinct") + .setStyle(Style.EMPTY.withColor(ChatFormatting.YELLOW)) + .append(Component.translatable(pressed ? "gtceu.multiblock.universal.distinct.yes" : + "gtceu.multiblock.universal.distinct.no"))))); + configuratorPanel.attachConfigurators(new CircuitFancyConfigurator(circuitInventory.storage)); + configuratorPanel.attachConfigurators(new AutoStockingFancyConfigurator(this)); + } + + private class ExportOnlyAETagStockingItemList extends ExportOnlyAEItemList { + + public ExportOnlyAETagStockingItemList(MetaMachine holder, int slots) { + super(holder, slots, () -> METagStockingInputBusPartMachine.this.new ExportOnlyAETagStockingItemSlot()); + } + + @Override + public boolean isAutoPull() { + return true; + } + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean hasStackInConfig(GenericStack stack, boolean checkExternal) { + boolean inThisBus = super.hasStackInConfig(stack, false); + if (inThisBus) return true; + return checkExternal && METagStockingInputBusPartMachine.this.testConfiguredInOtherPart(stack); + } + } + + private class ExportOnlyAETagStockingItemSlot extends ExportOnlyAEItemSlot { + + public ExportOnlyAETagStockingItemSlot() {} + + public ExportOnlyAETagStockingItemSlot(@Nullable GenericStack config, GenericStack stock) { + super(config, stock); + } + + @Override + public ItemStack extractItem(int slot, int amount, boolean simulate) { + if (slot == 0 && this.stock != null && this.config != null) { + if (!METagStockingInputBusPartMachine.this.isOnline()) return ItemStack.EMPTY; + + IGrid grid = METagStockingInputBusPartMachine.this.getMainNode().getGrid(); + if (grid == null) return ItemStack.EMPTY; + + MEStorage aeNetwork = grid.getStorageService().getInventory(); + Actionable action = simulate ? Actionable.SIMULATE : Actionable.MODULATE; + AEKey key = this.config.what(); + + long extracted = aeNetwork.extract(key, amount, action, + METagStockingInputBusPartMachine.this.actionSource); + if (extracted > 0L) { + ItemStack result; + if (key instanceof AEItemKey itemKey) result = itemKey.toStack((int) extracted); + else result = ItemStack.EMPTY; + + if (!simulate) { + this.stock = ExportOnlyAESlot.copy(this.stock, this.stock.amount() - extracted); + if (this.stock.amount() == 0L) this.stock = null; + if (this.onContentsChanged != null) this.onContentsChanged.run(); + } + return result; + } + } + return ItemStack.EMPTY; + } + + @Override + public ExportOnlyAETagStockingItemSlot copy() { + return METagStockingInputBusPartMachine.this.new ExportOnlyAETagStockingItemSlot( + this.config == null ? null : copy(this.config), + this.stock == null ? null : copy(this.stock)); + } + } +} diff --git a/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputHatchPartMachine.java b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputHatchPartMachine.java new file mode 100644 index 0000000..3f8ea55 --- /dev/null +++ b/src/main/java/net/neganote/gtutilities/integration/ae2/machine/METagStockingInputHatchPartMachine.java @@ -0,0 +1,452 @@ +package net.neganote.gtutilities.integration.ae2.machine; + +import com.gregtechceu.gtceu.api.gui.GuiTextures; +import com.gregtechceu.gtceu.api.gui.fancy.ConfiguratorPanel; +import com.gregtechceu.gtceu.api.gui.fancy.IFancyConfiguratorButton; +import com.gregtechceu.gtceu.api.gui.widget.ToggleButtonWidget; +import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; +import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.api.machine.fancyconfigurator.AutoStockingFancyConfigurator; +import com.gregtechceu.gtceu.api.machine.fancyconfigurator.CircuitFancyConfigurator; +import com.gregtechceu.gtceu.api.machine.trait.NotifiableFluidTank; +import com.gregtechceu.gtceu.config.ConfigHolder; +import com.gregtechceu.gtceu.integration.ae2.gui.widget.AEFluidConfigWidget; +import com.gregtechceu.gtceu.integration.ae2.machine.MEHatchPartMachine; +import com.gregtechceu.gtceu.integration.ae2.machine.MEStockingHatchPartMachine; +import com.gregtechceu.gtceu.integration.ae2.slot.*; +import com.gregtechceu.gtceu.integration.ae2.utils.AEUtil; + +import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; +import com.lowdragmc.lowdraglib.gui.texture.TextTexture; +import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; +import com.lowdragmc.lowdraglib.gui.widget.Widget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.lowdragmc.lowdraglib.utils.Position; +import com.lowdragmc.lowdraglib.utils.Size; + +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraftforge.fluids.FluidStack; +import net.neganote.gtutilities.common.gui.widgets.MultilineTextField; +import net.neganote.gtutilities.utils.TagMatcher; + +import appeng.api.config.Actionable; +import appeng.api.networking.IGrid; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.api.storage.MEStorage; +import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import org.jetbrains.annotations.Nullable; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.function.Predicate; + +import javax.annotation.ParametersAreNonnullByDefault; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class METagStockingInputHatchPartMachine extends MEStockingHatchPartMachine { + + protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + METagStockingInputHatchPartMachine.class, MEStockingHatchPartMachine.MANAGED_FIELD_HOLDER); + + @Persisted + @DescSynced + protected String whitelistExpr = ""; + @Persisted + @DescSynced + protected String blacklistExpr = ""; + @DescSynced + protected String Wltmp = ""; + @DescSynced + protected String Bltmp = ""; + + private Predicate tagAutoPullTest = ($) -> true; + + private transient String wlLast = null; + private transient String blLast = null; + private transient TagMatcher.Compiled wlCompiled = TagMatcher.compile(""); + private transient TagMatcher.Compiled blCompiled = TagMatcher.compile(""); + + private final transient Object2ByteOpenHashMap decisionCache = new Object2ByteOpenHashMap<>(); + private static final int DECISION_CACHE_LIMIT = 8192; + + public METagStockingInputHatchPartMachine(IMachineBlockEntity holder, Object... args) { + super(holder, args); + decisionCache.defaultReturnValue((byte) -1); + } + + @Override + public ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + @Override + protected NotifiableFluidTank createTank(int initialCapacity, int slots, Object... args) { + this.aeFluidHandler = new ExportOnlyAETagStockingFluidList(this, MEHatchPartMachine.CONFIG_SIZE); + return this.aeFluidHandler; + } + + @Override + public void onLoad() { + super.onLoad(); + if (!isRemote()) { + if (super.isAutoPull()) { + super.setAutoPull(false); + } + + invalidateFilterCaches(); + + if (updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateTankSubscription(); + } + } + } + + @Override + public void autoIO() { + if (!isRemote() && super.isAutoPull()) { + super.setAutoPull(false); + } + + if (this.getTicksPerCycle() == 0) { + this.setTicksPerCycle(ConfigHolder.INSTANCE.compat.ae2.updateIntervals); + } + + if (this.getOffsetTimer() % (long) this.getTicksPerCycle() == 0L) { + if (!isRemote() && updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateTankSubscription(); + } + } + } + + private static String norm(@Nullable String s) { + if (s == null) return ""; + s = s.trim(); + return s; + } + + private void invalidateFilterCaches() { + wlLast = null; + blLast = null; + decisionCache.clear(); + } + + private void ensureCompiledUpToDate() { + String wl = norm(whitelistExpr); + String bl = norm(blacklistExpr); + + if (!Objects.equals(wl, wlLast)) { + wlLast = wl; + wlCompiled = TagMatcher.compile(wl); + decisionCache.clear(); + } + if (!Objects.equals(bl, blLast)) { + blLast = bl; + blCompiled = TagMatcher.compile(bl); + decisionCache.clear(); + } + + if (decisionCache.size() > DECISION_CACHE_LIMIT) { + decisionCache.clear(); + } + } + + protected boolean isAllowed(AEFluidKey key) { + ensureCompiledUpToDate(); + + if ((wlLast == null || wlLast.isEmpty()) && (blLast == null || blLast.isEmpty())) return false; + + byte cached = decisionCache.getByte(key); + if (cached != -1) { + return cached == 1; + } + + boolean allowed; + + if (blLast != null && !blLast.isEmpty() && blCompiled != null && blCompiled.isValid()) { + if (TagMatcher.doesFluidMatch(key, blCompiled)) { + decisionCache.put(key, (byte) 0); + return false; + } + } + + if (wlLast != null && !wlLast.isEmpty() && wlCompiled != null && wlCompiled.isValid()) { + allowed = TagMatcher.doesFluidMatch(key, wlCompiled); + } else { + allowed = false; + } + + decisionCache.put(key, allowed ? (byte) 1 : (byte) 0); + return allowed; + } + + protected void refreshListFromTags() { + IGrid grid = this.getMainNode().getGrid(); + if (grid == null) { + this.aeFluidHandler.clearInventory(0); + return; + } + + MEStorage networkStorage = grid.getStorageService().getInventory(); + var counter = networkStorage.getAvailableStacks(); + + PriorityQueue> top = new PriorityQueue<>( + Comparator.comparingLong(Object2LongMap.Entry::getLongValue)); + + for (Object2LongMap.Entry entry : counter) { + long amount = entry.getLongValue(); + AEKey what = entry.getKey(); + + if (amount <= 0) continue; + if (!(what instanceof AEFluidKey fluidKey)) continue; + + if (!isAllowed(fluidKey)) continue; + + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, this.actionSource); + if (request == 0L) continue; + + if (tagAutoPullTest != null && !tagAutoPullTest.test(new GenericStack(fluidKey, amount))) continue; + + if (top.size() < MEHatchPartMachine.CONFIG_SIZE) { + top.offer(entry); + } else if (amount > Objects.requireNonNull(top.peek()).getLongValue()) { + top.poll(); + top.offer(entry); + } + } + + int itemAmount = top.size(); + + int index; + for (index = 0; index < MEHatchPartMachine.CONFIG_SIZE && !top.isEmpty(); index++) { + Object2LongMap.Entry entry = top.poll(); + AEKey what = entry.getKey(); + long amount = entry.getLongValue(); + long request = networkStorage.extract(what, amount, Actionable.SIMULATE, this.actionSource); + + ExportOnlyAEFluidSlot slot = this.aeFluidHandler.getInventory()[itemAmount - index - 1]; + slot.setConfig(new GenericStack(what, 1L)); + slot.setStock(new GenericStack(what, request)); + } + this.aeFluidHandler.clearInventory(index); + markDirty(); + self().getHolder().self().setChanged(); + } + + @Override + public Widget createUIWidget() { + WidgetGroup group = new WidgetGroup(new Position(0, 0), new Size(176, 150)); + + int y = 6; + group.addWidget(new LabelWidget(3, y, () -> this.isOnline ? + "gtceu.gui.me_network.online" : + "gtceu.gui.me_network.offline")); + + group.addWidget(new ToggleButtonWidget(176 - 55, 2, 50, 16, () -> false, pressed -> { + whitelistExpr = pressed ? Wltmp : whitelistExpr; + blacklistExpr = pressed ? Bltmp : blacklistExpr; + + invalidateFilterCaches(); + + if (updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateTankSubscription(); + } + }).setTexture(new GuiTextureGroup(GuiTextures.VANILLA_BUTTON, new TextTexture("Confirm")), + new GuiTextureGroup(GuiTextures.VANILLA_BUTTON, new TextTexture("Confirm")))); + + y += 14; + var WLField = new MultilineTextField( + 7, y, 160, 25, + () -> Wltmp, + v -> { + Wltmp = v; + }, + Component.literal("Whitelist tags...")); + group.addWidget(WLField); + + y += 29; + var BLField = new MultilineTextField( + 7, y, 160, 25, + () -> Bltmp, + v -> { + Bltmp = v; + }, + Component.literal("Blacklist tags...")); + group.addWidget(BLField); + + WLField.setDirectly(whitelistExpr); + BLField.setDirectly(blacklistExpr); + + y += 29; + + group.addWidget(new AEFluidConfigWidget(15, y, this.aeFluidHandler) { + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean isAutoPull() { + return true; + } + }); + + return group; + } + + @Override + protected InteractionResult onScrewdriverClick(Player playerIn, InteractionHand hand, Direction gridSide, + BlockHitResult hitResult) { + if (!this.isRemote()) { + playerIn.sendSystemMessage(Component.literal("This bus is always Tag-Mode (no manual/autoPull toggle).")); + } + return InteractionResult.sidedSuccess(this.isRemote()); + } + + @Override + protected CompoundTag writeConfigToTag() { + CompoundTag tag = super.writeConfigToTag(); + tag.putString("WhitelistExpr", whitelistExpr); + tag.putString("BlacklistExpr", blacklistExpr); + tag.putBoolean("TagMode", true); + return tag; + } + + @Override + protected void readConfigFromTag(CompoundTag tag) { + super.readConfigFromTag(tag); + if (!isRemote() && super.isAutoPull()) { + super.setAutoPull(false); + } + + if (tag.contains("WhitelistExpr")) whitelistExpr = tag.getString("WhitelistExpr"); + if (tag.contains("BlacklistExpr")) blacklistExpr = tag.getString("BlacklistExpr"); + + invalidateFilterCaches(); + + if (!isRemote() && updateMEStatus()) { + refreshListFromTags(); + super.syncME(); + updateTankSubscription(); + } + } + + @Override + public void setAutoPullTest(Predicate autoPullTest) { + this.tagAutoPullTest = autoPullTest; + super.setAutoPullTest(autoPullTest); + } + + // We can't call super to avoid the auto-pull toggle... + @Override + public void attachConfigurators(ConfiguratorPanel configuratorPanel) { + configuratorPanel.attachConfigurators(new IFancyConfiguratorButton.Toggle( + GuiTextures.BUTTON_POWER.getSubTexture(0, 0, 1, 0.5), + GuiTextures.BUTTON_POWER.getSubTexture(0, 0.5, 1, 0.5), + this::isWorkingEnabled, (clickData, pressed) -> this.setWorkingEnabled(pressed)) + .setTooltipsSupplier(pressed -> List.of( + Component.translatable( + pressed ? "behaviour.soft_hammer.enabled" : "behaviour.soft_hammer.disabled")))); + for (var direction : Direction.values()) { + if (this.getCoverContainer().hasCover(direction)) { + var configurator = this.getCoverContainer().getCoverAtSide(direction).getConfigurator(); + if (configurator != null) + configuratorPanel.attachConfigurators(configurator); + } + } + configuratorPanel.attachConfigurators(new CircuitFancyConfigurator(circuitInventory.storage)); + configuratorPanel.attachConfigurators(new AutoStockingFancyConfigurator(this)); + } + + private class ExportOnlyAETagStockingFluidList extends ExportOnlyAEFluidList { + + public ExportOnlyAETagStockingFluidList(MetaMachine holder, int slots) { + super(holder, slots, () -> METagStockingInputHatchPartMachine.this.new ExportOnlyAETagStockingFluidSlot()); + } + + @Override + public boolean isAutoPull() { + return true; + } + + @Override + public boolean isStocking() { + return true; + } + + @Override + public boolean hasStackInConfig(GenericStack stack, boolean checkExternal) { + boolean inThisBus = super.hasStackInConfig(stack, false); + if (inThisBus) return true; + return checkExternal && METagStockingInputHatchPartMachine.this.testConfiguredInOtherPart(stack); + } + } + + private class ExportOnlyAETagStockingFluidSlot extends ExportOnlyAEFluidSlot { + + public ExportOnlyAETagStockingFluidSlot() { + super(); + } + + public ExportOnlyAETagStockingFluidSlot(@Nullable GenericStack config, @Nullable GenericStack stock) { + super(config, stock); + } + + @Override + public ExportOnlyAEFluidSlot copy() { + return new ExportOnlyAETagStockingFluidSlot( + this.config == null ? null : copy(this.config), + this.stock == null ? null : copy(this.stock)); + } + + @Override + public FluidStack drain(int maxDrain, FluidAction action) { + if (this.stock != null && this.config != null) { + if (!isOnline()) return FluidStack.EMPTY; + MEStorage aeNetwork = getMainNode().getGrid().getStorageService().getInventory(); + + Actionable actionable = action.simulate() ? Actionable.SIMULATE : Actionable.MODULATE; + var key = config.what(); + long extracted = aeNetwork.extract(key, maxDrain, actionable, actionSource); + + if (extracted > 0) { + FluidStack resultStack = key instanceof AEFluidKey fluidKey ? + AEUtil.toFluidStack(fluidKey, extracted) : FluidStack.EMPTY; + if (action.execute()) { + this.stock = ExportOnlyAESlot.copy(stock, stock.amount() - extracted); + if (this.stock.amount() == 0) { + this.stock = null; + } + if (this.onContentsChanged != null) { + this.onContentsChanged.run(); + } + } + return resultStack; + } + } + return FluidStack.EMPTY; + } + } +} diff --git a/src/main/java/net/neganote/gtutilities/integration/jade/UtilJadePlugin.java b/src/main/java/net/neganote/gtutilities/integration/jade/UtilJadePlugin.java index 83567ed..1b0cb9e 100644 --- a/src/main/java/net/neganote/gtutilities/integration/jade/UtilJadePlugin.java +++ b/src/main/java/net/neganote/gtutilities/integration/jade/UtilJadePlugin.java @@ -4,6 +4,7 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntity; +import net.neganote.gtutilities.integration.jade.provider.EnlargedMEStockingBusInformationProvider; import net.neganote.gtutilities.integration.jade.provider.ExpandedMEPatternBufferProvider; import net.neganote.gtutilities.integration.jade.provider.ExpandedMEPatternBufferProxyProvider; import net.neganote.gtutilities.integration.jade.provider.PTERBInformationProvider; @@ -21,6 +22,7 @@ public class UtilJadePlugin implements IWailaPlugin { public void register(IWailaCommonRegistration registration) { registration.registerBlockDataProvider(new PTERBInformationProvider(), BlockEntity.class); if (GTCEu.Mods.isAE2Loaded()) { + registration.registerBlockDataProvider(new EnlargedMEStockingBusInformationProvider(), BlockEntity.class); registration.registerBlockDataProvider(new ExpandedMEPatternBufferProvider(), BlockEntity.class); registration.registerBlockDataProvider(new ExpandedMEPatternBufferProxyProvider(), BlockEntity.class); } @@ -30,6 +32,7 @@ public void register(IWailaCommonRegistration registration) { public void registerClient(IWailaClientRegistration registration) { registration.registerBlockComponent(new PTERBInformationProvider(), Block.class); if (GTCEu.Mods.isAE2Loaded()) { + registration.registerBlockComponent(new EnlargedMEStockingBusInformationProvider(), Block.class); registration.registerBlockComponent(new ExpandedMEPatternBufferProvider(), Block.class); registration.registerBlockComponent(new ExpandedMEPatternBufferProxyProvider(), Block.class); } diff --git a/src/main/java/net/neganote/gtutilities/integration/jade/provider/EnlargedMEStockingBusInformationProvider.java b/src/main/java/net/neganote/gtutilities/integration/jade/provider/EnlargedMEStockingBusInformationProvider.java new file mode 100644 index 0000000..42ab968 --- /dev/null +++ b/src/main/java/net/neganote/gtutilities/integration/jade/provider/EnlargedMEStockingBusInformationProvider.java @@ -0,0 +1,134 @@ +package net.neganote.gtutilities.integration.jade.provider; + +import com.gregtechceu.gtceu.api.blockentity.MetaMachineBlockEntity; +import com.gregtechceu.gtceu.integration.ae2.machine.MEStockingBusPartMachine; +import com.gregtechceu.gtceu.integration.ae2.slot.IConfigurableSlot; + +import net.minecraft.ChatFormatting; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.neganote.gtutilities.GregTechModernUtilities; +import net.neganote.gtutilities.integration.ae2.machine.MEEnlargedStockingInputBusPartMachine; +import net.neganote.gtutilities.integration.ae2.machine.MEEnlargedTagStockingInputBusPartMachine; + +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import snownee.jade.api.BlockAccessor; +import snownee.jade.api.IBlockComponentProvider; +import snownee.jade.api.IServerDataProvider; +import snownee.jade.api.ITooltip; +import snownee.jade.api.config.IPluginConfig; + +public class EnlargedMEStockingBusInformationProvider + implements IBlockComponentProvider, + IServerDataProvider { + + private static final int MAX_ICONS = 36; + private static final int ICONS_PER_ROW = 9; + + @Override + public ResourceLocation getUid() { + return GregTechModernUtilities.id("enlarged_me_stocking_bus_info"); + } + + @Override + public int getDefaultPriority() { + return Integer.MAX_VALUE; + } + + private static boolean isTargetMachine(Object mm) { + return mm instanceof MEEnlargedStockingInputBusPartMachine || + mm instanceof MEEnlargedTagStockingInputBusPartMachine; + } + + @Override + public void appendServerData(CompoundTag out, BlockAccessor accessor) { + BlockEntity be = accessor.getBlockEntity(); + if (!(be instanceof MetaMachineBlockEntity mmbe)) return; + + Object mm = mmbe.getMetaMachine(); + if (!isTargetMachine(mm)) return; + + MEStockingBusPartMachine bus = (MEStockingBusPartMachine) mm; + var slotList = bus.getSlotList(); + int total = slotList.getConfigurableSlots(); + + ObjectOpenHashSet unique = new ObjectOpenHashSet<>(); + ListTag entries = new ListTag(); + + for (int i = 0; i < total && entries.size() < MAX_ICONS; i++) { + IConfigurableSlot slot = slotList.getConfigurableSlot(i); + if (slot == null) continue; + + GenericStack cfg = slot.getConfig(); + GenericStack st = slot.getStock(); + + AEItemKey key = null; + if (cfg != null && cfg.what() instanceof AEItemKey k) key = k; + else if (st != null && st.what() instanceof AEItemKey k) key = k; + + if (key == null) continue; + if (!unique.add(key)) continue; + + ItemStack icon = key.toStack(st == null ? 0 : (int) st.amount()); + + CompoundTag e = new CompoundTag(); + e.put("item", icon.save(new CompoundTag())); + entries.add(e); + } + + CompoundTag data = new CompoundTag(); + data.put("entries", entries); + out.put(getUid().toString(), data); + } + + @Override + public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) { + BlockEntity be = accessor.getBlockEntity(); + if (!(be instanceof MetaMachineBlockEntity mmbe)) return; + + Object mm = mmbe.getMetaMachine(); + if (!isTargetMachine(mm)) return; + + CompoundTag root = accessor.getServerData().getCompound(getUid().toString()); + if (root == null) return; + + ListTag entries = root.getList("entries", Tag.TAG_COMPOUND); + + tooltip.clear(); + var helper = tooltip.getElementHelper(); + + ItemStack picked = accessor.getPickedResult(); + Component title = !picked.isEmpty() ? + Component.literal(picked.getHoverName().getString()).withStyle(ChatFormatting.WHITE) : + Component.literal("Machine").withStyle(ChatFormatting.WHITE); + + tooltip.append(helper.text(title)); + + int rowIndex = -1; + int inRow = 0; + + for (int i = 0; i < entries.size() && i < MAX_ICONS; i++) { + ItemStack icon = ItemStack.of(entries.getCompound(i).getCompound("item")); + if (icon.isEmpty()) continue; + + if (rowIndex == -1 || inRow >= ICONS_PER_ROW) { + tooltip.add(helper.item(icon)); + rowIndex = tooltip.size() - 1; + inRow = 1; + } else { + tooltip.append(rowIndex, helper.item(icon)); + inRow++; + } + } + + tooltip.add(Component.literal("GregTech Modern Utilities") + .withStyle(ChatFormatting.BLUE, ChatFormatting.ITALIC)); + } +} diff --git a/src/main/java/net/neganote/gtutilities/recipe/UtilRecipes.java b/src/main/java/net/neganote/gtutilities/recipe/UtilRecipes.java index d423762..63a3e8e 100644 --- a/src/main/java/net/neganote/gtutilities/recipe/UtilRecipes.java +++ b/src/main/java/net/neganote/gtutilities/recipe/UtilRecipes.java @@ -122,6 +122,60 @@ public static void init(Consumer provider) { 'C', GTMachines.CHARGER_4[tier].asStack()); } } + + if (UtilConfig.INSTANCE.features.tagStockingEnabled) { + ASSEMBLER_RECIPES.recipeBuilder("me_tag_stocking_input_bus") + .inputItems(GTAEMachines.STOCKING_IMPORT_BUS_ME.asStack(), 1) + .inputItems(CustomTags.LuV_CIRCUITS, 2) + .inputItems(AEItems.ENGINEERING_PROCESSOR.asItem(), 4) + .inputItems(TAG_FILTER, 1) + .outputItems(UtilAEMachines.ME_TAG_STOCKING_INPUT_BUS) + .duration(400).EUt(VA[LuV]).save(provider); + + ASSEMBLER_RECIPES.recipeBuilder("me_tag_stocking_input_hatch") + .inputItems(GTAEMachines.STOCKING_IMPORT_HATCH_ME.asStack(), 1) + .inputItems(CustomTags.LuV_CIRCUITS, 2) + .inputItems(AEItems.ENGINEERING_PROCESSOR.asItem(), 4) + .inputItems(TAG_FLUID_FILTER, 1) + .outputItems(UtilAEMachines.ME_TAG_STOCKING_INPUT_HATCH) + .duration(400).EUt(VA[LuV]).save(provider); + } + + if (UtilConfig.INSTANCE.features.enlargedStockingEnabled) { + ASSEMBLER_RECIPES.recipeBuilder("me_enlarged_stocking_input_bus") + .inputItems(GTAEMachines.STOCKING_IMPORT_BUS_ME.asStack(), 1) + .inputItems(CustomTags.LuV_CIRCUITS, 2) + .inputItems(AEItems.ENGINEERING_PROCESSOR.asItem(), 4) + .inputItems(AEItems.CAPACITY_CARD.asItem(), + Math.min(64, UtilConfig.INSTANCE.features.enlargedStockingSizeRows)) + .outputItems(UtilAEMachines.ME_ENLARGED_STOCKING_INPUT_BUS) + .duration(400).EUt(VA[LuV]).save(provider); + + ASSEMBLER_RECIPES.recipeBuilder("me_enlarged_stocking_input_hatch") + .inputItems(GTAEMachines.STOCKING_IMPORT_HATCH_ME.asStack(), 1) + .inputItems(CustomTags.LuV_CIRCUITS, 2) + .inputItems(AEItems.ENGINEERING_PROCESSOR.asItem(), 4) + .inputItems(AEItems.CAPACITY_CARD.asItem(), + Math.min(64, UtilConfig.INSTANCE.features.enlargedStockingSizeRows)) + .outputItems(UtilAEMachines.ME_ENLARGED_STOCKING_INPUT_HATCH) + .duration(400).EUt(VA[LuV]).save(provider); + } + + if (UtilConfig.INSTANCE.features.enlargedStockingEnabled && UtilConfig.INSTANCE.features.tagStockingEnabled) { + ASSEMBLER_RECIPES.recipeBuilder("me_enlarged_tag_stocking_input_bus") + .inputItems(UtilAEMachines.ME_ENLARGED_STOCKING_INPUT_BUS, 1) + .inputItems(UtilAEMachines.ME_TAG_STOCKING_INPUT_BUS, 1) + .inputItems(CustomTags.ZPM_CIRCUITS, 2) + .outputItems(UtilAEMachines.ME_ENLARGED_TAG_STOCKING_INPUT_BUS) + .duration(600).EUt(VA[ZPM]).save(provider); + + ASSEMBLER_RECIPES.recipeBuilder("me_enlarged_tag_stocking_input_hatch") + .inputItems(UtilAEMachines.ME_ENLARGED_STOCKING_INPUT_HATCH, 1) + .inputItems(UtilAEMachines.ME_TAG_STOCKING_INPUT_HATCH, 1) + .inputItems(CustomTags.ZPM_CIRCUITS, 2) + .outputItems(UtilAEMachines.ME_ENLARGED_TAG_STOCKING_INPUT_HATCH) + .duration(600).EUt(VA[ZPM]).save(provider); + } } public static void register64AConverterRecipes(Consumer provider) { diff --git a/src/main/java/net/neganote/gtutilities/utils/TagMatcher.java b/src/main/java/net/neganote/gtutilities/utils/TagMatcher.java new file mode 100644 index 0000000..4d81269 --- /dev/null +++ b/src/main/java/net/neganote/gtutilities/utils/TagMatcher.java @@ -0,0 +1,417 @@ +package net.neganote.gtutilities.utils; + +import net.minecraft.core.Holder; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.material.Fluid; + +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class TagMatcher { + + public static Compiled compile(@Nullable String expr) { + String washed = washExpression(expr == null ? "" : expr); + if (washed.isBlank()) return Compiled.EMPTY; + + try { + List tokens = tokenize(washed); + Token[] rpn = toRpn(tokens); + + boolean needsTags = false; + for (Token t : rpn) { + if (t.type == TokenType.TAG) { + needsTags = true; + break; + } + } + + return new Compiled(rpn, true, needsTags); + } catch (RuntimeException ex) { + return Compiled.INVALID; + } + } + + public static boolean doesItemMatch(@Nullable AEItemKey item, String expr) { + if (item == null || expr == null || expr.isBlank()) return false; + Compiled c = compile(expr); + if (!c.isValid()) return false; + return doesItemMatch(item, c); + } + + public static boolean doesItemMatch(@Nullable AEItemKey item, @Nullable Compiled compiled) { + if (item == null || compiled == null || !compiled.isValid()) return false; + if (!compiled.needsTags()) return false; // no tags referenced => cannot match anything meaningful + ItemTagCache cache = ItemTagCache.of(item); + return evalRpn(compiled.rpn, cache.tags); + } + + public static boolean doesFluidMatch(@Nullable AEFluidKey fluid, String expr) { + if (fluid == null || expr == null || expr.isBlank()) return false; + Compiled c = compile(expr); + if (!c.isValid()) return false; + return doesFluidMatch(fluid, c); + } + + public static boolean doesFluidMatch(@Nullable AEFluidKey fluid, @Nullable Compiled compiled) { + if (fluid == null || compiled == null || !compiled.isValid()) return false; + if (!compiled.needsTags()) return false; + FluidTagCache cache = FluidTagCache.of(fluid); + return evalRpn(compiled.rpn, cache.tags); + } + + private static String washExpression(String expression) { + if (expression == null) return ""; + return expression.replace("&&", "&").replace("||", "|"); + } + + public static final class Compiled { + + final Token[] rpn; + @Getter + final boolean valid; + final boolean needsTags; + + private Compiled(Token[] rpn, boolean valid, boolean needsTags) { + this.rpn = rpn; + this.valid = valid; + this.needsTags = needsTags; + } + + public boolean needsTags() { + return needsTags; + } + + // Common singletons + static final Compiled EMPTY = new Compiled(new Token[0], true, false); + static final Compiled INVALID = new Compiled(new Token[0], false, false); + } + + public static final class ItemTagCache { + + final Set tags; + + private ItemTagCache(Set tags) { + this.tags = tags; + } + + private static final ThreadLocal>> TL = ThreadLocal.withInitial(IdentityHashMap::new); + + @SuppressWarnings("deprecation") + public static ItemTagCache of(AEItemKey key) { + Item item = key.getItem(); + Map> map = TL.get(); + Set tags = map.get(item); + if (tags == null) { + Holder holder = item.builtInRegistryHolder(); + // materialize once + tags = holder.tags() + .map(tk -> tk.location().toString()) + .collect(HashSet::new, Set::add, Set::addAll); + map.put(item, tags); + } + return new ItemTagCache(tags); + } + + public static void clearThreadLocal() { + TL.remove(); + } + } + + public static final class FluidTagCache { + + final Set tags; + + private FluidTagCache(Set tags) { + this.tags = tags; + } + + private static final ThreadLocal>> TL = ThreadLocal.withInitial(IdentityHashMap::new); + + @SuppressWarnings("deprecation") + public static FluidTagCache of(AEFluidKey key) { + Fluid fluid = key.getFluid(); + Map> map = TL.get(); + Set tags = map.get(fluid); + if (tags == null) { + Holder holder = fluid.builtInRegistryHolder(); + tags = holder.tags() + .map(tk -> tk.location().toString()) + .collect(HashSet::new, Set::add, Set::addAll); + map.put(fluid, tags); + } + return new FluidTagCache(tags); + } + + public static void clearThreadLocal() { + TL.remove(); + } + } + + private enum TokenType { + TAG, + OPERATOR, + LPAREN, + RPAREN + } + + private enum Operator { + + NOT("!", 3, true), + AND("&", 2, false), + XOR("^", 1, false), + OR("|", 0, false); + + final String symbol; + final int precedence; + final boolean rightAssociative; + + Operator(String symbol, int precedence, boolean rightAssociative) { + this.symbol = symbol; + this.precedence = precedence; + this.rightAssociative = rightAssociative; + } + + static Operator fromSymbol(char symbol) { + for (Operator op : values()) { + if (op.symbol.charAt(0) == symbol) return op; + } + return null; + } + } + + private record Token(TokenType type, String tagPattern, boolean hasWildcard, Operator op) { + + static Token tag(String raw) { + boolean wc = raw.indexOf('*') >= 0; + return new Token(TokenType.TAG, raw, wc, null); + } + + static Token op(Operator op) { + return new Token(TokenType.OPERATOR, null, false, op); + } + + static Token lparen() { + return new Token(TokenType.LPAREN, null, false, null); + } + + static Token rparen() { + return new Token(TokenType.RPAREN, null, false, null); + } + } + + private static List tokenize(String expression) { + List tokens = new ArrayList<>(); + StringBuilder currentTag = new StringBuilder(); + + boolean expectingOperand = true; + boolean lastIsTag = false; + int lp = 0; + + for (int i = 0; i < expression.length(); i++) { + char c = expression.charAt(i); + + if (c == '#') { + throw new IllegalArgumentException("Character '#' is not allowed in tag expressions (pos " + i + ")."); + } + if (Character.isWhitespace(c)) continue; + + Operator op = Operator.fromSymbol(c); + + if (c == '(') { + if (!expectingOperand) throw new IllegalArgumentException("Unexpected '(' at position " + i); + flushTag(currentTag, tokens); + tokens.add(Token.lparen()); + lp++; + lastIsTag = false; + + } else if (c == ')') { + if (expectingOperand && lp <= 0) throw new IllegalArgumentException("Unexpected ')' at position " + i); + flushTag(currentTag, tokens); + tokens.add(Token.rparen()); + expectingOperand = false; + lp--; + lastIsTag = false; + + } else if (op != null) { + if (op == Operator.NOT && expectingOperand) { + flushTag(currentTag, tokens); + tokens.add(Token.op(op)); + } else if (op != Operator.NOT) { + if (lastIsTag || !expectingOperand) { + flushTag(currentTag, tokens); + tokens.add(Token.op(op)); + expectingOperand = true; + } + } else { + throw new IllegalArgumentException("Unexpected operator '" + c + "' at position " + i); + } + lastIsTag = false; + + } else { + if (!expectingOperand) + throw new IllegalArgumentException("Unexpected character '" + c + "' at position " + i); + currentTag.append(c); + lastIsTag = true; + } + } + + flushTag(currentTag, tokens); + + if (tokens.isEmpty()) throw new IllegalArgumentException("Expression cannot be empty."); + if (lp > 0) throw new IllegalArgumentException("Missing ')' at the end of the expression."); + + Token last = tokens.get(tokens.size() - 1); + if (expectingOperand && last.type != TokenType.TAG && last.type != TokenType.RPAREN) { + throw new IllegalArgumentException("Expression ended unexpectedly."); + } + + return tokens; + } + + private static void flushTag(StringBuilder currentTag, List tokens) { + if (!currentTag.isEmpty()) { + tokens.add(Token.tag(currentTag.toString())); + currentTag.setLength(0); + } + } + + private static Token[] toRpn(List tokens) { + ArrayList out = new ArrayList<>(tokens.size()); + Deque stack = new ArrayDeque<>(); + + for (Token t : tokens) { + switch (t.type) { + case TAG -> out.add(t); + + case OPERATOR -> { + while (!stack.isEmpty() && stack.peek().type == TokenType.OPERATOR) { + Operator cur = t.op; + Operator top = stack.peek().op; + + boolean shouldPop = (!cur.rightAssociative && cur.precedence <= top.precedence) || + (cur.rightAssociative && cur.precedence < top.precedence); + + if (shouldPop) out.add(stack.pop()); + else break; + } + stack.push(t); + } + + case LPAREN -> stack.push(t); + + case RPAREN -> { + boolean found = false; + while (!stack.isEmpty()) { + Token top = stack.peek(); + if (top.type == TokenType.LPAREN) { + stack.pop(); + found = true; + break; + } + out.add(stack.pop()); + } + if (!found) throw new IllegalArgumentException("Mismatched parentheses."); + } + } + } + + while (!stack.isEmpty()) { + Token top = stack.pop(); + if (top.type == TokenType.LPAREN) throw new IllegalArgumentException("Mismatched parentheses."); + out.add(top); + } + + return out.toArray(Token[]::new); + } + + private static boolean evalRpn(Token[] rpn, Set actualTags) { + if (rpn.length == 0) return false; + + boolean[] stack = new boolean[rpn.length]; + int sp = 0; + + for (Token t : rpn) { + if (t.type == TokenType.TAG) { + boolean match; + if (!t.hasWildcard) { + match = actualTags.contains(t.tagPattern); + } else { + match = matchesAnyGlob(t.tagPattern, actualTags); + } + stack[sp++] = match; + + } else if (t.type == TokenType.OPERATOR) { + Operator op = t.op; + + if (op == Operator.NOT) { + if (sp < 1) throw new IllegalArgumentException("NOT needs 1 operand."); + stack[sp - 1] = !stack[sp - 1]; + } else { + if (sp < 2) throw new IllegalArgumentException(op.symbol + " needs 2 operands."); + boolean right = stack[--sp]; + boolean left = stack[--sp]; + boolean res = switch (op) { + case AND -> left && right; + case OR -> left || right; + case XOR -> left ^ right; + default -> throw new IllegalStateException("Unexpected op: " + op); + }; + stack[sp++] = res; + } + + } else { + throw new IllegalStateException("Paren token in RPN (should not happen)."); + } + } + + if (sp == 1) return stack[0]; + throw new IllegalArgumentException("Invalid expression: stack size " + sp); + } + + private static boolean matchesAnyGlob(String pattern, Set tags) { + if ("*".equals(pattern)) return true; + + for (String tag : tags) { + if (pattern.equals(tag)) return true; + if (globMatchStarOnly(pattern, tag)) return true; + } + return false; + } + + private static boolean globMatchStarOnly(String pattern, String text) { + int p = 0, t = 0; + int star = -1; + int match = 0; + + while (t < text.length()) { + if (p < pattern.length() && pattern.charAt(p) == text.charAt(t)) { + p++; + t++; + } else if (p < pattern.length() && pattern.charAt(p) == '*') { + star = p++; + match = t; + } else if (star != -1) { + p = star + 1; + t = ++match; + } else { + return false; + } + } + + while (p < pattern.length() && pattern.charAt(p) == '*') p++; + return p == pattern.length(); + } + + private TagMatcher() {} +} diff --git a/src/main/resources/assets/gtceu/textures/block/overlay/appeng/me_tag_bus.png b/src/main/resources/assets/gtceu/textures/block/overlay/appeng/me_tag_bus.png new file mode 100644 index 0000000..3ec23ea Binary files /dev/null and b/src/main/resources/assets/gtceu/textures/block/overlay/appeng/me_tag_bus.png differ diff --git a/src/main/resources/assets/gtceu/textures/block/overlay/appeng/me_tag_hatch.png b/src/main/resources/assets/gtceu/textures/block/overlay/appeng/me_tag_hatch.png new file mode 100644 index 0000000..4b65206 Binary files /dev/null and b/src/main/resources/assets/gtceu/textures/block/overlay/appeng/me_tag_hatch.png differ