diff --git a/dependencies.gradle b/dependencies.gradle index 4334e330e3c..eac4b48839f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -10,6 +10,10 @@ dependencies { // Configuration jarJar(modApi(forge.configuration.get())) + // Math Parser + jarJar(implementation(libs.mxparser.get())) + additionalRuntimeClasspath(libs.mxparser.get()) + // Mixin (& Extras) annotationProcessor(variantOf(libs.mixin) { classifier("processor") }) compileOnly(annotationProcessor(forge.mixinExtras.common.get())) @@ -119,4 +123,6 @@ dependencies { modExtraLocalRuntime(forge.ponder) modExtraLocalRuntime(variantOf(forge.create) { classifier("slim") }) modExtraLocalRuntime(forge.flywheel.forge) + + modLocalRuntime(forge.curios) } diff --git a/docs/content/Development/Data-Sync-System/Annotations.md b/docs/content/Development/Data-Sync-System/Annotations.md new file mode 100644 index 00000000000..58089a72798 --- /dev/null +++ b/docs/content/Development/Data-Sync-System/Annotations.md @@ -0,0 +1,89 @@ +--- +title: "Annotations" +--- + +# Annotations +The following annotations define the sync/save behaviour for an `ISyncManaged` object. + +### `@SaveField` + +The `@SaveField` annotation defines a field that should be saved to the server. `nbtKey` is optional, the key will default to the field name. +```java +@SaveField(nbtKey="nbtKeyToSaveTo") +public int mySaveInt = 10; +``` + +### `@SyncToClient` + +The `@SyncToClient` annotation defines a field with a value that should be synced to clients. + +!!! warning + Client sync fields **do not** automatically detect changes. When changing a client sync field, call `ISyncManaged.getSyncDataHolder().markClientSyncFieldDirty(FIELD_NAME)` +```java +@SaveField(nbtKey="nbtKeyToSaveTo") +@SyncToClient +public int mySaveAndSyncInt = 10; + +@SyncToClient +@RerenderOnChanged +public long mySyncRerenderLong = 10000L; + +public void serverTick() { + int newIntValue = getNewIntValue(); + long newLongValue = getNewLongValue(); + if (mySaveAndSyncInt != newIntValue) { + mySaveAndSyncInt = newIntValue; + getSyncDataHolder().markClientSyncFieldDirty("mySaveAndSyncInt"); + } + if (mySyncRerenderLong != newLongValue) { + mySyncRerenderLong = newLongValue; + getSyncDataHolder().markClientSyncFieldDirty("mySyncRerenderLong"); + } +} +``` + +### `@ClientFieldChangeListener` and `@RerenderOnChanged` + +The `@ClientFieldChangeListener` annotation defines a method to be called on the client when a client sync field has changed value; + +Annotating a `@SyncToClient` field with `@RerenderOnChanged` will cause clients to rerender the block entity when this field changes. + +```java +@SyncToClient +@SaveField +@RerenderOnChanged +public boolean isWorkingEnabled = true; + +@ClientFieldChangeListener(fieldName="isWorkingEnabled") +public void isWorkingChanged() { + setRenderState(getRenderState().setValue(GTMachineModelProperties.IS_WORKING_ENABLED, isWorkingEnabled)); +} +``` + +### `@FieldDataModifier` and `@CustomDataField` + + + +The `@FieldDataModifier`annotation defines custom processing to be performed on the NBT for a field, e.g. for compatibility reasons. + +The `@CustomDataField`annotation defines a field with a type too complex to be serialised using the normal system. Custom data fields must have exactly one load modifier and one data modifier. + +Field data modifiers on non-custom fields will be applied *after* standard serialisation/deserialisation, and will be called with an argument containing the current tag. + +```java +@CustomDataField +@SaveField +public VeryComplexType myVeryComplexValue = new VeryComplexType(); + +@FieldDataModifier(fieldName="myVeryComplexValue", target=FieldDataModifier.MODIFY_TARGET.LOAD_NBT) +public void loadVeryComplexValue(CompoundTag tag) { + // Process tag here. + myVeryComplexValue = new VeryComplexType(); +} + +@FieldDataModifier(fieldName="myVeryComplexValue", target=FieldDataModifier.MODIFY_TARGET.SAVE_NBT) +public CompoundTag saveVeryComplexValue(CompoundTag tag, boolean isSendingToClient) { + // Save data here. + return tag; +} +``` \ No newline at end of file diff --git a/docs/content/Development/Data-Sync-System/Migrating-From-LDLib-SyncData.md b/docs/content/Development/Data-Sync-System/Migrating-From-LDLib-SyncData.md new file mode 100644 index 00000000000..5802445b08e --- /dev/null +++ b/docs/content/Development/Data-Sync-System/Migrating-From-LDLib-SyncData.md @@ -0,0 +1,27 @@ +--- +title: "Migrating from LDLib SyncData" +--- +# Migrating from LDLib SyncData + +### General migration guidelines + +- Remove all `ManagedFieldHolder` fields. +- Replace `FieldManagedStorage` fields with `SyncDataHolder` fields. +- Replace `IEnhancedManaged` objects with `ISyncManaged`. +- Replace `IAsyncAutoSyncBlockEntity`, `IAutoPersistBlockEntity`, `IAutoSyncBlockEntity` and `IManagedBlockEntity` by extending `ManagedSyncBlockEntity`. + +### Annotations + +!!! warning +Client sync fields **do not** automatically detect changes. When changing a client sync field, call `ISyncManaged.syncDataHolder.markClientSyncFieldDirty(FIELD_NAME)` + +- `@DescSynced` -> `@SyncToClient` +- `@RequireRerender` -> `@RerenderOnChanged` +- `@Persisted` -> `@SaveField` +- `@UpdateListener` -> `@ClientFieldChangeListener` on listener method. +- `@DropSaved` - Removed, make machines implement `IDropSaveMachine` instead +- `@ReadOnlyManaged` and `@LazyManaged` See usage docs for instructions on complex sync objects + +### Other changes + + - `saveCustomPersistedData` & `loadCustomPersistedData` methods: Use `@CustomDataModifier` & `@FieldDataModifier` \ No newline at end of file diff --git a/docs/content/Development/Data-Sync-System/Usage.md b/docs/content/Development/Data-Sync-System/Usage.md new file mode 100644 index 00000000000..f9c5b13c32c --- /dev/null +++ b/docs/content/Development/Data-Sync-System/Usage.md @@ -0,0 +1,98 @@ +--- +title: "Usage" +--- + +## Usage + +### Registering classes with the sync system + +At the core of the system is the interface `ISyncManaged`, which represents a class that to be synchronised with the client or saved. +All block entities which should be synchronised or saved must extend the abstract class `ManagedSyncBlockEntity`. + +!!! warning + Block entities that inherit `ManagedSyncBlockEntity` must call `ManagedSyncBlockEntity::updateTick`***every tick*** within their ticker, or they will not be saved. + +```java +class MySyncObject implements ISyncManaged { + // Any class that directly implements ISyncManaged must have the following: + @Getter + protected final SyncDataHolder syncDataHolder = new SyncDataHolder(this); + + + /** + * Function called when the SyncDataHolder requests a rerender + */ + void scheduleRenderUpdate(); + + /** + * Function called to notify the server that this object has been updated and must be synced to clients + */ + void markAsChanged(); +} +``` + +### Registering fields to be managed by the system +See [Annotations](Annotations.md) + +### Type compatibility +The following field types are supported by default: +- Any class implementing `ISyncManaged` +- Any class implementing `INBTSerializable` +- All primitive types +- If `T`, `K` are supported types: + - `T[]` + - `Set` + - `List`, + - `Map` +- `String` +- `ItemStack` +- `FluidStack` +- `UUID` +- `BlockPos` +- `CompoundTag` +- `GTRecipe` +- `GTRecipeType` +- `MachineRenderState` +- `Material` +- `Component` + +### Adding support for additional types + +To add support for an additional type, call `ValueTransformers.registerClassTransformer(Class cls, IValueTransformer transformer)` or `ValueTransformers.registerInterfaceTransformer(Class cls, IValueTransformer transformer)` + +The `IValueTransformer` interface defines how a value of type `T` should be serialised. + +```java +public interface IValueTransformer { + + // If this type cannot be instanced purely from a serialised tag. + // All complex type typically have mustProvideObject true + default boolean mustProvideObject() { + return false; + } + + // A method for serialising a value into a tag + // Called when serialising a value to be sent to the client + default Tag serializeClientSyncNBT(@Nullable T value, ISyncManaged holder) { + return serializeNBT(value, holder); + } + + // A method for deserialising a value from a tag + // Called when deserialising a value on the client. + // If mustProvideObject == true, currentVal is the currently saved value. + default T deserializeClientNBT(Tag tag, ISyncManaged holder, @Nullable T currentVal) { + return deserializeNBT(tag, holder, currentVal); + } + + + // A method for serialising a value into a tag. + // The holder param is the object this sync value is attached to + Tag serializeNBT(T value, ISyncManaged holder); + + // A method for deserialising a value from a tag + // If mustProvideObject == true, currentVal is the currently saved value. + T deserializeNBT(Tag tag, ISyncManaged holder, @Nullable T currentVal); +} +``` + +Some types may be too complex to be processed using this system. For more complex NBT interactions, use the `@FieldDataModifier` and `@CustomDataField` annotations. \ No newline at end of file diff --git a/docs/content/Development/Data-Sync-System/index.md b/docs/content/Development/Data-Sync-System/index.md new file mode 100644 index 00000000000..ab476f8410b --- /dev/null +++ b/docs/content/Development/Data-Sync-System/index.md @@ -0,0 +1,12 @@ +--- +title: "Data Sync/Save system " +--- + +# Data Sync/Save System + +For serialising data to saves and synchronising with clients, a custom data sync system based on Java Annotations is used. + +- See [Usage](Usage.md) for a guide on using the system +- See [Annotations](Annotations.md) for a list of annotations provided by the system +- See [Migrating From LDLib SyncData](Migrating-From-LDLib-SyncData.md) for instructions on how to migrate from the LDLib SyncData system, which was used in versions 7.x and lower. + diff --git a/docs/content/Development/SyncData/.pages b/docs/content/Development/SyncData/.pages deleted file mode 100644 index 772812dd317..00000000000 --- a/docs/content/Development/SyncData/.pages +++ /dev/null @@ -1,5 +0,0 @@ -nav: - - index.md - - Using-SyncData.md - - Annotations - - ... diff --git a/docs/content/Development/SyncData/Annotations/.pages b/docs/content/Development/SyncData/Annotations/.pages deleted file mode 100644 index a094df08325..00000000000 --- a/docs/content/Development/SyncData/Annotations/.pages +++ /dev/null @@ -1,6 +0,0 @@ -nav: - - Persisted.md - - DescSynced.md - - RequireRerender.md - - UpdateListener.md - - ... \ No newline at end of file diff --git a/docs/content/Development/SyncData/Annotations/DescSynced.md b/docs/content/Development/SyncData/Annotations/DescSynced.md deleted file mode 100644 index 04848872b16..00000000000 --- a/docs/content/Development/SyncData/Annotations/DescSynced.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "@DescSynced" ---- - - -# Using `@DescSynced` \ No newline at end of file diff --git a/docs/content/Development/SyncData/Annotations/Persisted.md b/docs/content/Development/SyncData/Annotations/Persisted.md deleted file mode 100644 index d6bdaf78d44..00000000000 --- a/docs/content/Development/SyncData/Annotations/Persisted.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "@Persisted" ---- - - -# Using `@Persisted` \ No newline at end of file diff --git a/docs/content/Development/SyncData/Annotations/RequireRerender.md b/docs/content/Development/SyncData/Annotations/RequireRerender.md deleted file mode 100644 index f32382f036d..00000000000 --- a/docs/content/Development/SyncData/Annotations/RequireRerender.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "@RequireRerender" ---- - - -# Using `@RequireRerender` \ No newline at end of file diff --git a/docs/content/Development/SyncData/Annotations/UpdateListener.md b/docs/content/Development/SyncData/Annotations/UpdateListener.md deleted file mode 100644 index fbf84080cb5..00000000000 --- a/docs/content/Development/SyncData/Annotations/UpdateListener.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "@UpdateListener" ---- - - -# Using `@UpdateListener` \ No newline at end of file diff --git a/docs/content/Development/SyncData/Using-SyncData.md b/docs/content/Development/SyncData/Using-SyncData.md deleted file mode 100644 index 5c981698f51..00000000000 --- a/docs/content/Development/SyncData/Using-SyncData.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Using SyncData ---- - - -# How To Use SyncData Annotations - -For serializing and synchronizing fields, LDLib's annotation-based SyncData system is used. - -Please also refer to the [LDLib Wiki](https://github.com/Low-Drag-MC/LDLib-Architectury/wiki/SyncData-Annotations). - - -## Overview - -Here is an overview of the most important annotations: - -```java -public class MyMachine { - @Persisted // (1) - private String myPersistedField; - - @DescSynced // (2) - private String myClientsideRelevantField; - - @DescSynced @RequireRerender // (3) - private IO io; - - @DescSynced @UpdateListener(methodName = "runAdditionalUpdate") // (4) - private int fieldWithAdditionalClientUpdateLogic; - - - private void runAdditionalUpdate(int newValue, int oldValue) { - // Run additional clientside update code here - } -} -``` - -1. This field is automatically serialized to and deserialized from NBT data, that will be stored with its container. - By default, `@Persisted` only applies on the server side. - -2. For fields that need to be available on the client (or more specifically, remote) side, you can annotate them with - `@DescSynced` to make them available there as well. - Any changes made to the field on the server side will automatically be synchronized to the client side. - -3. If a change of a synced field's value requires rerendering the containing block (e.g. for different overlays based - on a cover's IO direction), simply add the `@RequireRerender` annotation to it. - Its renderer's code will then be called again every time the field changes. - -4. In some cases, a field may require some additional code to run on the client/remote side when it has been synced. - The `@UpdateListener` annotation allows you to define a method to be called in that case. \ No newline at end of file diff --git a/docs/content/Development/SyncData/index.md b/docs/content/Development/SyncData/index.md deleted file mode 100644 index 4abfcf8afbb..00000000000 --- a/docs/content/Development/SyncData/index.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: "LDLib's SyncData System" ---- - - -# LDLib's SyncData System - -For serializing and synchronizing fields, LDLib's annotation-based SyncData system is used. -Using this system, almost all boilerplate code regarding these topics can be omitted. - -For more info on the SyncData annotations, please refer to the following chapters, as well as to the -[LDLib Wiki](https://github.com/Low-Drag-MC/LDLib-Architectury/wiki/SyncData-Annotations). \ No newline at end of file diff --git a/docs/content/Modpacks/Examples/Example_Coil_Multiblock.md b/docs/content/Modpacks/Examples/Example_Coil_Multiblock.md new file mode 100644 index 00000000000..09fa577b996 --- /dev/null +++ b/docs/content/Modpacks/Examples/Example_Coil_Multiblock.md @@ -0,0 +1,102 @@ +--- +title: "Example Coil Multiblock" +--- + +### Superheated Pyrolyzing Oven Multiblock (by Phoenixvine) + + +Below is an example of a multiblock using the CoilWorkableElectricMultiblockMachine class and the pyrolyseOvenOverclock machine logic. + +### Multiblock +=== "JavaScript" + ```js title="superheated_pyrolyzing_oven_multiblock.js" + // In order to use multiblock logic extending beyond the normal WorkableElectricMultiblockMachine, (This is the multiblock type used by default for kubejs) you need to load a class. Coil multiblocks such as the Electric Blast Furnace, Pyrolyse Oven, and the Cracker use this class. + const CoilWorkableElectricMultiblockMachine = Java.loadClass("com.gregtechceu.gtceu.api.machine.multiblock.CoilWorkableElectricMultiblockMachine") + + GTCEuStartupEvents.registry('gtceu:machine', event => { + event.create("superheated_pyrolyzing_oven", "multiblock") + .machine((holder) => new CoilWorkableElectricMultiblockMachine(holder)) + .rotationState(RotationState.NON_Y_AXIS) + .recipeTypes('pyrolyse_oven') + .recipeModifiers( + [ + GTRecipeModifiers.PARALLEL_HATCH, + (machine, recipe) => GTRecipeModifiers.pyrolyseOvenOverclock(machine, recipe) + ] + ) + .appearanceBlock(GTBlocks.CASING_STEEL_SOLID) + .pattern(definition => FactoryBlockPattern.start() + .aisle("BBCCCBB", "BBCDCBB", "BBCCCBB", "BBCCCBB", "BBEEEBB", "BBEEEBB") + .aisle("BCCCCCB", "BCAFACB", "BCAFACB", "BCGGGCB", "BEAAAEB", "BEHHHEB") + .aisle("CCCCCCC", "CAAFAAC", "CAAFAAC", "CGGGGGC", "EAAAAAE", "EHHHHHE") + .aisle("CCCCCCC", "DFFFFFD", "CFFFFFC", "CGGGGGC", "EAAAAAE", "EHHMHHE") + .aisle("CCCCCCC", "CAAFAAC", "CAAFAAC", "CGGGGGC", "EAAAAAE", "EHHHHHE") + .aisle("BCCCCCB", "BCAFACB", "BCAFACB", "BCGGGCB", "BEAAAEB", "BEHHHEB") + .aisle("BBCCCBB", "BBCICBB", "BBCCCBB", "BBCCCBB", "BBEEEBB", "BBEEEBB") + .where('A', Predicates.blocks("minecraft:air")) + .where('B', Predicates.any()) + .where('C', Predicates.blocks('gtceu:solid_machine_casing').setMinGlobalLimited(10) + .or(Predicates.abilities(PartAbility.MAINTENANCE).setExactLimit(1)) + .or(Predicates.abilities(PartAbility.PARALLEL_HATCH).setMaxGlobalLimited(1)) + .or(Predicates.autoAbilities(definition.getRecipeTypes()))) + .where('D', Predicates.blocks("gtceu:steel_firebox_casing")) + .where('E', Predicates.blocks("gtceu:laminated_glass")) + .where('F', Predicates.blocks("gtceu:ptfe_pipe_casing")) + .where('G', Predicates.heatingCoils()) + .where('H', Predicates.blocks("gtceu:high_temperature_smelting_casing")) + .where('M', Predicates.abilities(PartAbility.MUFFLER).setExactLimit(1)) + .where('I', Predicates.controller(Predicates.blocks(definition.get()))) + .build()) + .workableCasingModel("gtceu:block/casings/solid/machine_casing_solid_steel", + "gtceu:block/multiblock/pyrolyse_oven"); + + }) + ``` + + +=== "Java" + ```java title="MultiMachines.java" + + public static final MultiblockMachineDefinition SUPERHEATED_PYROLYZING_OVEN = REGISTRATE + .multiblock("superheated_pyrolyzing_oven", (holder) -> new CoilWorkableElectricMultiblockMachine(holder)) + .rotationState(RotationState.NON_Y_AXIS) + .recipeType(GTRecipeTypes.PYROLYSE_RECIPES) + .recipeModifiers(GTRecipeModifiers.PARALLEL_HATCH, + (machine, recipe) -> GTRecipeModifiers.pyrolyseOvenOverclock(machine, recipe)) + .appearanceBlock(GTBlocks.CASING_STEEL_SOLID) + .pattern(definition -> FactoryBlockPattern.start() + .aisle("BBCCCBB", "BBCDCBB", "BBCCCBB", "BBCCCBB", "BBEEEBB", "BBEEEBB") + .aisle("BCCCCCB", "BCAFACB", "BCAFACB", "BCGGGCB", "BEAAAEB", "BEHHHEB") + .aisle("CCCCCCC", "CAAFAAC", "CAAFAAC", "CGGGGGC", "EAAAAAE", "EHHHHHE") + .aisle("CCCCCCC", "DFFFFFD", "CFFFFFC", "CGGGGGC", "EAAAAAE", "EHHMHHE") + .aisle("CCCCCCC", "CAAFAAC", "CAAFAAC", "CGGGGGC", "EAAAAAE", "EHHHHHE") + .aisle("BCCCCCB", "BCAFACB", "BCAFACB", "BCGGGCB", "BEAAAEB", "BEHHHEB") + .aisle("BBCCCBB", "BBCICBB", "BBCCCBB", "BBCCCBB", "BBEEEBB", "BBEEEBB") + .where('A', Predicates.air()) + .where('B', Predicates.any()) + .where('C', Predicates.blocks(GTBlocks.CASING_STEEL_SOLID.get()).setMinGlobalLimited(10) + .or(Predicates.abilities(PartAbility.MAINTENANCE).setExactLimit(1)) + .or(Predicates.abilities(PartAbility.PARALLEL_HATCH).setMaxGlobalLimited(1)) + .or(Predicates.autoAbilities(definition.getRecipeTypes()))) + .where('D', Predicates.blocks(GTBlocks.FIREBOX_STEEL.get())) + .where('E', Predicates.blocks(CASING_LAMINATED_GLASS.get())) + .where('F', Predicates.blocks(GTBlocks.CASING_POLYTETRAFLUOROETHYLENE_PIPE.get())) + .where('G', Predicates.heatingCoils()) + .where('H', Predicates.blocks(GCYMBlocks.CASING_HIGH_TEMPERATURE_SMELTING.get())) + .where('M', Predicates.abilities(PartAbility.MUFFLER).setExactLimit(1)) + .where('I', Predicates.controller(Predicates.blocks(definition.get()))) + .build()) + .workableCasingModel(GTCEu.id("block/casings/solid/machine_casing_solid_steel"), + GTCEu.id("block/multiblock/pyrolyse_oven")) + .register(); + ``` + +### Lang + +```json title="en_us.json" +{ + "block.gtceu.superheated_pyrolyzing_oven": "Superheated Pyrolyzing Oven", +} +``` + + diff --git a/docs/content/Modpacks/Examples/Example_Steam_Multiblock.md b/docs/content/Modpacks/Examples/Example_Steam_Multiblock.md new file mode 100644 index 00000000000..57b06f47885 --- /dev/null +++ b/docs/content/Modpacks/Examples/Example_Steam_Multiblock.md @@ -0,0 +1,87 @@ +--- +title: "Example Steam Multiblock" +--- + +### Large Steam Compressor Multiblock + +Below is an example of a multiblock using the SteamParallelMultiblockMachine class. +Steam multiblocks such as the Steam Grinder and Steam Oven use this class. + +### Multiblock + +=== "JavaScript" + ```js title="example_steam_multiblock_multiblock.js" + + // In order to use multiblock logic extending beyond the default multiblock type for KJS (WorkableElectricMultiblockMachine), you need to load a class. + const $SteamMulti = Java.loadClass('com.gregtechceu.gtceu.common.machine.multiblock.steam.SteamParallelMultiblockMachine'); + + GTCEuStartupEvents.registry('gtceu:machine', event => { + event.create('large_steam_compressor', 'multiblock') + .machine((holder) => new $SteamMulti(holder, 4)) + // The number in holder is the max amount of parallel it can use. + .rotationState(RotationState.NON_Y_AXIS) + .recipeType('compressor') + .recipeModifier((machine, recipe) => $SteamMulti.recipeModifier(machine, recipe), true) + .pattern(definition => FactoryBlockPattern.start() + .aisle("BCCCB", "BBCBB", "BBCBB", "BBBBB", "BBBBB") + .aisle("CDDDC", "BDBDB", "BDEDB", "BBDBB", "BBBBB") + .aisle("CDDDC", "CBBBC", "CEFEC", "BDDDB", "BBGBB") + .aisle("CDDDC", "BDBDB", "BDEDB", "BBDBB", "BBBBB") + .aisle("BCCCB", "BBHBB", "BBCBB", "BBBBB", "BBBBB") + .where('B', Predicates.any()) + .where('C', Predicates.blocks('gtceu:steam_machine_casing').setMinGlobalLimited(10) + .or(Predicates.abilities(PartAbility.STEAM_IMPORT_ITEMS).setMaxGlobalLimited(1)) + .or(Predicates.abilities(PartAbility.STEAM).setMaxGlobalLimited(1)) + .or(Predicates.abilities(PartAbility.STEAM_EXPORT_ITEMS).setMaxGlobalLimited(1))) + .where('D', Predicates.blocks("gtceu:industrial_steam_casing")) + .where('E', Predicates.blocks("gtceu:bronze_brick_casing")) + .where('F', Predicates.blocks("gtceu:bronze_firebox_casing")) + .where('G', Predicates.blocks("gtceu:bronze_machine_casing")) + .where('H', Predicates.controller(Predicates.blocks(definition.get()))) + .build()) + .workableCasingModel("gtceu:block/casings/steam/bronze/bottom", + "gtceu:block/machines/compressor") + }) + ``` + +=== "Java" + ```java title="MultiMachines.java" + public static final MultiblockMachineDefinition LARGE_STEAM_COMPRESSOR = REGISTRATE + .multiblock("large_steam_compressor", (holder) -> new SteamParallelMultiblockMachine(holder, 4)) + .rotationState(RotationState.NON_Y_AXIS) + .recipeType(COMPRESSOR_RECIPES) + .recipeModifier((machine, recipe) -> SteamParallelMultiblockMachine.recipeModifier(machine, recipe), true) + .pattern(definition -> FactoryBlockPattern.start() + .aisle("BCCCB", "BBCBB", "BBCBB", "BBBBB", "BBBBB") + .aisle("CDDDC", "BDBDB", "BDEDB", "BBDBB", "BBBBB") + .aisle("CDDDC", "CBBBC", "CEFEC", "BDDDB", "BBGBB") + .aisle("CDDDC", "BDBDB", "BDEDB", "BBDBB", "BBBBB") + .aisle("BCCCB", "BBHBB", "BBCBB", "BBBBB", "BBBBB") + .where('B', Predicates.any()) + .where('C', Predicates.blocks(GTBlocks.CASING_BRONZE_BRICKS.get()).setMinGlobalLimited(10) + .or(Predicates.abilities(PartAbility.STEAM_IMPORT_ITEMS).setMaxGlobalLimited(1)) + .or(Predicates.abilities(PartAbility.STEAM).setMaxGlobalLimited(1)) + .or(Predicates.abilities(PartAbility.STEAM_EXPORT_ITEMS).setMaxGlobalLimited(1))) + .where('D', Predicates.blocks(GCYMBlocks.CASING_INDUSTRIAL_STEAM.get())) + .where('E', Predicates.blocks(GTBlocks.BRONZE_BRICKS_HULL.get())) + .where('F', Predicates.blocks(GTBlocks.FIREBOX_BRONZE.get())) + .where('G', Predicates.blocks(GTBlocks.BRONZE_HULL.get())) + .where('H', Predicates.controller(Predicates.blocks(definition.get()))) + .build()) + .workableCasingModel(GTCEu.id("block/casings/steam/bronze/bottom"), + GTCEu.id("block/machines/compressor")) + .register(); + ``` + +### Lang + +```json title="en_us.json" +{ + "block.gtceu.large_steam_compressor": "Large Steam Compressor", +} +``` + + + + + diff --git a/docs/content/Modpacks/Examples/Example_Turbine.md b/docs/content/Modpacks/Examples/Example_Turbine.md new file mode 100644 index 00000000000..4abdeefe1d5 --- /dev/null +++ b/docs/content/Modpacks/Examples/Example_Turbine.md @@ -0,0 +1,94 @@ +--- +title: "Example Turbine" +--- + +### Example Turbine + +Below is an example of a multiblock using the LargeTurbineMachine class for making custom large turbines. + +### Multiblock + +=== "JavaScript" + ```js title="hyper_gas_turbine.js" + // In order to use multiblock logic extending beyond the normal WorkableElectricMultiblockMachine, (This is the multiblock type used by default for kubejs) you need to load a class. LargeTurbineMachines such as the gas, steam, and plasma turbines use this class. + const $LargeTurbineMachine = Java.loadClass("com.gregtechceu.gtceu.common.machine.multiblock.generator.LargeTurbineMachine") + + GTCEuStartupEvents.registry('gtceu:machine', event => { + event.create('hyper_gas_turbine', 'multiblock') + .machine((holder) => new $LargeTurbineMachine(holder, GTValues.LuV)) // The value shows one rotor holder tier above the recommended minimum rotor holder. The tier of rotor holder provides a boost based on the efficiency stat. + .rotationState(RotationState.NON_Y_AXIS) + .recipeTypes("gas_turbine") + .recipeModifiers([GTRecipeModifiers.OC_NON_PERFECT_SUBTICK, GTRecipeModifiers.BATCH_MODE, (machine, recipe) => GTRecipeModifiers.LargeTurbineMachine(machine, recipe)]) + .appearanceBlock(GTBlocks.CASING_TITANIUM_STABLE) + .pattern(definition => FactoryBlockPattern.start() + .aisle("BBBBBBB", "BBBCBBB", "BBBDBBB", "BBBCBBB", "BBBBBBB") + .aisle("BBBCBBB", "BBCACBB", "BBCFCBB", "BBCACBB", "BBBCBBB") + .aisle("BBCCCBB", "BCAAACB", "BCAFACB", "BCAFACB", "BBCCCBB") + .aisle("BCCCCCB", "CAAFAAC", "CFFFFFC", "CAFFFAC", "BCCECCB") + .aisle("BBCCCBB", "BCAAACB", "BCAFACB", "BCAFACB", "BBCCCBB") + .aisle("BBBCBBB", "BBCACBB", "BBCFCBB", "BBCACBB", "BBBCBBB") + .aisle("BBBBBBB", "BBBCBBB", "BBBGBBB", "BBBCBBB", "BBBBBBB") + .where("A", Predicates.blocks("minecraft:air")) + .where("B", Predicates.any()) + .where("C", Predicates.blocks("gtceu:stainless_steel_turbine_casing") + .or(Predicates.autoAbilities(definition.getRecipeTypes())) + .or(Predicates.abilities(PartAbility.MAINTENANCE).setExactLimit(1))) + .where("D", Predicates.ability(PartAbility.MUFFLER).setExactLimit(1)) + .where("E", Predicates.ability(PartAbility.ROTOR_HOLDER).setExactLimit(1)) + .where("F", Predicates.blocks("gtceu:stainless_steel_frame")) + .where("G", Predicates.controller(Predicates.blocks(definition.get()))) + .build()) + .workableCasingModel("gtceu:block/casings/mechanic/machine_casing_turbine_stainless_steel", + "gtceu:block/multiblock/generator/large_gas_turbine") + }); + ``` + +=== "Java" + ```java title="MultiMachines.java" + public static final MultiblockMachineDefinition HYPER_GAS_TURBINE = REGISTRATE + .multiblock("hyper_gas_turbine", (holder) -> new LargeTurbineMachine(holder, GTValues.LuV, 4)) // The value shows one rotor holder tier above the recommended minimum rotor holder. The tier of rotor holder provides a boost based on the efficiency stat. + .rotationState(RotationState.NON_Y_AXIS) + .recipeType(GTRecipeTypes.GAS_TURBINE_FUELS) + .recipeModifiers(GTRecipeModifiers.OC_NON_PERFECT_SUBTICK, GTRecipeModifiers.BATCH_MODE, LargeTurbineMachine::recipeModifier) + .pattern(definition -> FactoryBlockPattern.start() + .aisle("BBBBBBB", "BBBCBBB", "BBBDBBB", "BBBCBBB", "BBBBBBB") + .aisle("BBBCBBB", "BBCACBB", "BBCECBB", "BBCACBB", "BBBCBBB") + .aisle("BBCCCBB", "BCAAACB", "BCAEACB", "BCAEACB", "BBCCCBB") + .aisle("BCCCCCB", "CAAEAAC", "CEEEEEC", "CAEEEAC", "BCCFCCB") + .aisle("BBCCCBB", "BCAAACB", "BCAEACB", "BCAEACB", "BBCCCBB") + .aisle("BBBCBBB", "BBCACBB", "BBCECBB", "BBCACBB", "BBBCBBB") + .aisle("BBBBBBB", "BBBCBBB", "BBBGBBB", "BBBCBBB", "BBBBBBB") + .where("A", Predicates.blocks("minecraft:air")) + .where("B", Predicates.any()) + .where("C", Predicates.blocks("gtceu:stainless_steel_turbine_casing") + .or(Predicates.autoAbilities(definition.getRecipeTypes())) + .or(Predicates.abilities(PartAbility.MAINTENANCE).setExactLimit(1))) + .where("D", Predicates.ability(PartAbility.MUFFLER).setExactLimit(1)) + .where("F", Predicates.ability(PartAbility.ROTOR_HOLDER).setExactLimit(1)) + .where("E", Predicates.blocks("gtceu:stainless_steel_frame")) + .where("G", Predicates.controller(Predicates.blocks(definition.get()))) + .build()) + .workableCasingModel(GTCEu.id("block/casings/steam/bronze/bottom"), + GTCEu.id("block/machines/compressor")) + .register(); + ``` + + +### Lang + +```json title="en_us.json" +{ + "block.gtceu.hyper_gas_turbine": "Hyper Gas Turbine", +} +``` + + + + + + + + + + + diff --git a/docs/content/Modpacks/Examples/Greenhouse.md b/docs/content/Modpacks/Examples/Greenhouse.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gradle/forge.versions.toml b/gradle/forge.versions.toml index eac33484595..4aa1c87b8fb 100644 --- a/gradle/forge.versions.toml +++ b/gradle/forge.versions.toml @@ -11,6 +11,7 @@ ae2 = "15.0.18" kubejs = "2001.6.5-build.16" rhino = "2001.2.3-build.10" architectury = "9.2.14" +clothmath = "+" clothconfig = "11.1.136" theoneprobe = "1.20.1-10.0.1-3" curios = "5.9.1+1.20.1" @@ -76,6 +77,7 @@ flywheel-forge = { module = "dev.engine-room.flywheel:flywheel-forge-1.20.1 kubejs = { module = "dev.latvian.mods:kubejs-forge", version.ref = "kubejs" } rhino = { module = "dev.latvian.mods:rhino-forge", version.ref = "rhino" } architectury = { module = "dev.architectury:architectury-forge", version.ref = "architectury" } +clothmath = { module = "me.shedaniel.cloth:basic-math", version.ref = "clothmath" } clothconfig = { module = "me.shedaniel.cloth:cloth-config-forge", version.ref = "clothconfig" } theoneprobe = { module = "mcjty.theoneprobe:theoneprobe", version.ref = "theoneprobe" } curios = { module = "top.theillusivec4.curios:curios-forge", version.ref = "curios" } @@ -118,7 +120,7 @@ ftbchunks-cm = { module = "curse.maven:ftb-chunks-forge-314906", version. [bundles] jei = ["jei-common-api", "jei-forge-api", "jei-forge-impl"] -rei = ["rei-plugin", "rei-forge"] +rei = ["rei-plugin", "rei-forge", "clothmath"] rei-runtime = ["rei-forge", "architectury", "clothconfig"] kjs = ["kubejs", "rhino", "architectury"] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 96029a58b06..3824a10ea3d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ lombok = "8.14" jetbrains-annotations = "26.0.1" renderNurse = "0.0.12" mixin = "0.8.7" +mxparser = "6.1.0" [libraries] minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" } @@ -17,6 +18,7 @@ minecraftForge = { module = "net.minecraftforge:forge", version.ref = " jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "jetbrains-annotations" } renderNurse = { module = "net.neoforged:render-nurse", version.ref = "renderNurse" } mixin = { module = "org.spongepowered:mixin", version.ref = "mixin" } +mxparser = { module = "org.mariuszgromada.math:MathParser.org-mXparser", version.ref = "mxparser" } [plugins] modDevGradle = { id = "net.neoforged.moddev.legacyforge", version.ref = "modDevGradle" } diff --git a/src/generated/resources/assets/gtceu/blockstates/test_mui.json b/src/generated/resources/assets/gtceu/blockstates/test_mui.json new file mode 100644 index 00000000000..c96e32638d9 --- /dev/null +++ b/src/generated/resources/assets/gtceu/blockstates/test_mui.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=down": { + "model": "gtceu:block/machine/test_mui", + "x": 90 + }, + "facing=east": { + "model": "gtceu:block/machine/test_mui", + "y": 90 + }, + "facing=north": { + "model": "gtceu:block/machine/test_mui" + }, + "facing=south": { + "model": "gtceu:block/machine/test_mui", + "y": 180 + }, + "facing=up": { + "gtceu:z": 180, + "model": "gtceu:block/machine/test_mui", + "x": 270 + }, + "facing=west": { + "model": "gtceu:block/machine/test_mui", + "y": 270 + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtceu/blockstates/test_mui_new.json b/src/generated/resources/assets/gtceu/blockstates/test_mui_new.json new file mode 100644 index 00000000000..41ab9b075f2 --- /dev/null +++ b/src/generated/resources/assets/gtceu/blockstates/test_mui_new.json @@ -0,0 +1,28 @@ +{ + "variants": { + "facing=down": { + "model": "gtceu:block/machine/test_mui_new", + "x": 90 + }, + "facing=east": { + "model": "gtceu:block/machine/test_mui_new", + "y": 90 + }, + "facing=north": { + "model": "gtceu:block/machine/test_mui_new" + }, + "facing=south": { + "model": "gtceu:block/machine/test_mui_new", + "y": 180 + }, + "facing=up": { + "gtceu:z": 180, + "model": "gtceu:block/machine/test_mui_new", + "x": 270 + }, + "facing=west": { + "model": "gtceu:block/machine/test_mui_new", + "y": 270 + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtceu/lang/en_ud.json b/src/generated/resources/assets/gtceu/lang/en_ud.json index 6b3d465b059..91818fe5152 100644 --- a/src/generated/resources/assets/gtceu/lang/en_ud.json +++ b/src/generated/resources/assets/gtceu/lang/en_ud.json @@ -33,10 +33,11 @@ "behavior.portable_scanner.machine_progress": "%s / %s :pɐoꞀ/ssǝɹboɹԀ", "behavior.portable_scanner.machine_upwards_facing": "%s :buıɔɐℲ spɹɐʍd∩", "behavior.portable_scanner.mode.caption": "%s :ǝpoɯ ʎɐןdsıᗡ", - "behavior.portable_scanner.mode.show_all_info": "oɟuı ןןɐ ʍoɥS", + "behavior.portable_scanner.mode.show_all_info": ")oɟuı ןɐuɹǝʇuı buıpnןɔxǝ( oɟuı ןןɐ ʍoɥS", "behavior.portable_scanner.mode.show_block_info": "oɟuı ʞɔoןq ʍoɥS", "behavior.portable_scanner.mode.show_electrical_info": "oɟuı ןɐɔıɹʇɔǝןǝ ʍoɥS", "behavior.portable_scanner.mode.show_environmental_info": "oɟuı ןɐʇuǝɯuoɹıʌuǝ ʍoɥS", + "behavior.portable_scanner.mode.show_internal_info": "oɟuı buıbbnqǝp ןɐuɹǝʇuı ʍoɥS", "behavior.portable_scanner.mode.show_machine_info": "oɟuı ǝuıɥɔɐɯ ʍoɥS", "behavior.portable_scanner.mode.show_recipe_info": "oɟuı ǝdıɔǝɹ ʍoɥS", "behavior.portable_scanner.muffled": "˙pǝןɟɟnW", @@ -1119,6 +1120,8 @@ "block.gtceu.substation_capacitor.tooltip_filled": "∩Ǝ %dɟ§ :ʎʇıɔɐdɐƆ ʎbɹǝuƎɔ§", "block.gtceu.superconducting_coil": "ʞɔoןᗺ ןıoƆ buıʇɔnpuoɔɹǝdnS", "block.gtceu.tempered_glass": "ssɐן⅁ pǝɹǝdɯǝ⟘", + "block.gtceu.test_mui": "ınW ʇsǝ⟘", + "block.gtceu.test_mui_new": "ʍǝN ınW ʇsǝ⟘", "block.gtceu.the_end_marker": "puƎ ǝɥ⟘", "block.gtceu.the_nether_marker": "ɹǝɥʇǝN ǝɥ⟘", "block.gtceu.titanium_crate": "ǝʇɐɹƆ ɯnıuɐʇı⟘", @@ -1750,6 +1753,7 @@ "config.gtceu.option.compat": "ʇɐdɯoɔ", "config.gtceu.option.createCompat": "ʇɐdɯoƆǝʇɐǝɹɔ", "config.gtceu.option.debug": "bnqǝp", + "config.gtceu.option.debugUI": "I∩bnqǝp", "config.gtceu.option.debugWorldgen": "uǝbpןɹoMbnqǝp", "config.gtceu.option.defaultPaintingColor": "ɹoןoƆbuıʇuıɐԀʇןnɐɟǝp", "config.gtceu.option.defaultUIColor": "ɹoןoƆI∩ʇןnɐɟǝp", @@ -1777,6 +1781,7 @@ "config.gtceu.option.energyUsageMultiplier": "ɹǝıןdıʇןnWǝbɐs∩ʎbɹǝuǝ", "config.gtceu.option.environmentalHazardDecayRate": "ǝʇɐᴚʎɐɔǝᗡpɹɐzɐHןɐʇuǝɯuoɹıʌuǝ", "config.gtceu.option.environmentalHazards": "spɹɐzɐHןɐʇuǝɯuoɹıʌuǝ", + "config.gtceu.option.escRestoresLastText": "ʇxǝ⟘ʇsɐꞀsǝɹoʇsǝᴚɔsǝ", "config.gtceu.option.euToFeRatio": "oıʇɐᴚǝℲo⟘nǝ", "config.gtceu.option.extractorRecyclingYield": "pןǝıʎbuıןɔʎɔǝᴚɹoʇɔɐɹʇxǝ", "config.gtceu.option.feToEuRatio": "oıʇɐᴚnƎo⟘ǝɟ", @@ -1861,6 +1866,7 @@ "config.gtceu.option.renderGrowingPlants": "sʇuɐןԀbuıʍoɹ⅁ɹǝpuǝɹ", "config.gtceu.option.renderer": "ɹǝɹǝpuǝɹ", "config.gtceu.option.replaceMinedBlocksWith": "ɥʇıMsʞɔoןᗺpǝuıWǝɔɐןdǝɹ", + "config.gtceu.option.replaceVanillaTooltips": "sdıʇןoo⟘ɐןןıuɐΛǝɔɐןdǝɹ", "config.gtceu.option.replaceWithCobbleVersion": "uoısɹǝΛǝןqqoƆɥʇıMǝɔɐןdǝɹ", "config.gtceu.option.requireGTToolsForBlocks": "sʞɔoןᗺɹoℲsןoo⟘⟘⅁ǝɹınbǝɹ", "config.gtceu.option.rngDamageElectricTools": "sןoo⟘ɔıɹʇɔǝןƎǝbɐɯɐᗡbuɹ", @@ -1869,6 +1875,7 @@ "config.gtceu.option.shouldWeatherOrTerrainExplosion": "uoısoןdxƎuıɐɹɹǝ⟘ɹOɹǝɥʇɐǝMpןnoɥs", "config.gtceu.option.showDimensionTier": "ɹǝı⟘uoısuǝɯıᗡʍoɥs", "config.gtceu.option.smallBoilers": "sɹǝןıoᗺןןɐɯs", + "config.gtceu.option.smoothProgressBar": "ɹɐᗺssǝɹboɹԀɥʇooɯs", "config.gtceu.option.solarBoilerBaseOutput": "ʇndʇnOǝsɐᗺɹǝןıoᗺɹɐןos", "config.gtceu.option.solidBoilerBaseOutput": "ʇndʇnOǝsɐᗺɹǝןıoᗺpıןos", "config.gtceu.option.sprayCanChainLength": "ɥʇbuǝꞀuıɐɥƆuɐƆʎɐɹds", @@ -1885,11 +1892,14 @@ "config.gtceu.option.toolCraftingSounds": "spunoSbuıʇɟɐɹƆןooʇ", "config.gtceu.option.toolUseSounds": "spunoSǝs∩ןooʇ", "config.gtceu.option.tools": "sןooʇ", + "config.gtceu.option.tooltipPos": "soԀdıʇןooʇ", "config.gtceu.option.treeFellingDelay": "ʎɐןǝᗡbuıןןǝℲǝǝɹʇ", "config.gtceu.option.tungstensteelBoilerHeatSpeed": "pǝǝdSʇɐǝHɹǝןıoᗺןǝǝʇsuǝʇsbunʇ", "config.gtceu.option.tungstensteelBoilerMaxTemperature": "ǝɹnʇɐɹǝdɯǝ⟘xɐWɹǝןıoᗺןǝǝʇsuǝʇsbunʇ", + "config.gtceu.option.ui": "ın", "config.gtceu.option.universalHazards": "spɹɐzɐHןɐsɹǝʌıun", "config.gtceu.option.updateIntervals": "sןɐʌɹǝʇuIǝʇɐpdn", + "config.gtceu.option.useDarkThemeByDefault": "ʇןnɐɟǝᗡʎᗺǝɯǝɥ⟘ʞɹɐᗡǝsn", "config.gtceu.option.useVBO": "OᗺΛǝsn", "config.gtceu.option.voltageTierAdvImpeller": "ɹǝןןǝdɯIʌpⱯɹǝı⟘ǝbɐʇןoʌ", "config.gtceu.option.voltageTierAdvNanoSuit": "ʇınSouɐNʌpⱯɹǝı⟘ǝbɐʇןoʌ", @@ -2263,7 +2273,8 @@ "gtceu.creative.chest.ipc": "ǝןɔʎƆ ɹǝd sɯǝʇI", "gtceu.creative.chest.item": "ɯǝʇI", "gtceu.creative.chest.tpc": "ǝןɔʎƆ ɹǝd sʞɔı⟘", - "gtceu.creative.computation.average": "ʇ∩MƆ pǝʇsǝnbǝᴚ ǝbɐɹǝʌⱯ", + "gtceu.creative.computation.average": "%d :ʞɔıʇ/∩MƆ ǝbɐɹǝʌⱯ", + "gtceu.creative.computation.max_usage": ":ʞɔıʇ/∩MƆ xɐW", "gtceu.creative.energy.amperage": "ǝbɐɹǝdɯⱯ", "gtceu.creative.energy.sink": "ʞuıS", "gtceu.creative.energy.source": "ǝɔɹnoS", @@ -2405,6 +2416,7 @@ "gtceu.gui.fluid_amount": ":ʇunoɯⱯ pınןℲ", "gtceu.gui.fluid_auto_input.tooltip.disabled": "pǝןqɐsıᗡ ʇnduI-oʇnⱯ pınןℲ", "gtceu.gui.fluid_auto_input.tooltip.enabled": "pǝןqɐuƎ ʇnduI-oʇnⱯ pınןℲ", + "gtceu.gui.fluid_auto_output": "%s :ʇndʇnO pınןℲ", "gtceu.gui.fluid_auto_output.allow_input.disabled": "ǝpıs ʇndʇno ǝɥʇ ɯoɹɟ ʇnduı spınןɟ ǝןqɐsıp", "gtceu.gui.fluid_auto_output.allow_input.enabled": "ǝpıs ʇndʇno ǝɥʇ ɯoɹɟ ʇnduı spınןɟ ʍoןןɐ", "gtceu.gui.fluid_auto_output.disabled": "pǝןqɐsıᗡɔ§ :ʇndʇnO oʇnⱯ pınןℲ", @@ -2416,6 +2428,7 @@ "gtceu.gui.fluid_auto_output.tooltip.enabled": "pǝןqɐuƎ ʇndʇnO-oʇnⱯ pınןℲ", "gtceu.gui.fluid_auto_output.unselected.0": "ʇndʇnO oʇnⱯ pınןℲ", "gtceu.gui.fluid_auto_output.unselected.1": "˙ʇndʇno sʇı ǝɹnbıɟuoɔ oʇ ǝuıɥɔɐɯ ǝɥʇ ɟo ǝpıs ɐ ʇɔǝןǝSㄥ§", + "gtceu.gui.fluid_input_from_output": "%s :ʇndʇnO ɯoɹɟ ʇnduI pınןℲ", "gtceu.gui.fluid_lock.tooltip.disabled": "pǝןqɐsıᗡ buıʞɔoꞀ pınןℲ", "gtceu.gui.fluid_lock.tooltip.enabled": "pǝןqɐuƎ buıʞɔoꞀ pınןℲ", "gtceu.gui.fluid_voiding": "spınןℲ6§ buıpıoΛㄥ§", @@ -2424,6 +2437,7 @@ "gtceu.gui.fuel_amount": ":ʇunoɯⱯ ןǝnℲ", "gtceu.gui.item_auto_input.tooltip.disabled": "pǝןqɐsıᗡ ʇnduI-oʇnⱯ ɯǝʇI", "gtceu.gui.item_auto_input.tooltip.enabled": "pǝןqɐuƎ ʇnduI-oʇnⱯ ɯǝʇI", + "gtceu.gui.item_auto_output": "%s :ʇndʇnO ɯǝʇI", "gtceu.gui.item_auto_output.allow_input.disabled": "ǝpıs ʇndʇno ǝɥʇ ɯoɹɟ ʇnduı sɯǝʇı ǝןqɐsıp", "gtceu.gui.item_auto_output.allow_input.enabled": "ǝpıs ʇndʇno ǝɥʇ ɯoɹɟ ʇnduı sɯǝʇı ʍoןןɐ", "gtceu.gui.item_auto_output.disabled": "pǝןqɐsıᗡɔ§ :ʇndʇnO oʇnⱯ ɯǝʇI", @@ -2435,6 +2449,8 @@ "gtceu.gui.item_auto_output.tooltip.enabled": "pǝןqɐuƎ ʇndʇnO-oʇnⱯ ɯǝʇI", "gtceu.gui.item_auto_output.unselected.0": "ʇndʇnO oʇnⱯ ɯǝʇI", "gtceu.gui.item_auto_output.unselected.1": "˙ʇndʇno sʇı ǝɹnbıɟuoɔ oʇ ǝuıɥɔɐɯ ǝɥʇ ɟo ǝpıs ɐ ʇɔǝןǝSㄥ§", + "gtceu.gui.item_collector.range": " :ǝbuɐᴚ", + "gtceu.gui.item_input_from_output": "%s :ʇndʇnO ɯoɹɟ ʇnduI ɯǝʇI", "gtceu.gui.item_lock.tooltip.disabled": "pǝןqɐsıᗡ buıʞɔoꞀ ɯǝʇI", "gtceu.gui.item_lock.tooltip.enabled": "pǝןqɐuƎ buıʞɔoꞀ ɯǝʇI", "gtceu.gui.item_voiding": "sɯǝʇI9§ buıpıoΛㄥ§", @@ -3068,6 +3084,7 @@ "gtceu.machine.opv_gas_collector.tooltip": "uoısuǝɯıp ǝɥʇ uo buıpuǝdǝp ǝsɹǝʌıun ǝɥʇ ɯoɹɟ sɐ⅁ sʇɔǝןןoƆㄥ§", "gtceu.machine.opv_rock_crusher.tooltip": "ɹǝqɯɐɥƆ uoıʇɐɯɹoℲ ɔıuɐɔןoΛㄥ§", "gtceu.machine.parallel_hatch.display": "ʞɔoןqıʇןnɯ ǝɥʇ ɟo ןǝןןɐɹɐd ɯnɯıxɐɯ ǝɥʇ ʇsnظpⱯ", + "gtceu.machine.parallel_hatch.parallel_ui": "sןǝןןɐɹɐԀ", "gtceu.machine.parallel_hatch_mk5.tooltip": "˙ןǝןןɐɹɐd uı sǝdıɔǝɹ ㄣ oʇ dn unɹ oʇ sʍoןןⱯ", "gtceu.machine.parallel_hatch_mk6.tooltip": "˙ןǝןןɐɹɐd uı sǝdıɔǝɹ 9Ɩ oʇ dn unɹ oʇ sʍoןןⱯ", "gtceu.machine.parallel_hatch_mk7.tooltip": "˙ןǝןןɐɹɐd uı sǝdıɔǝɹ ㄣ9 oʇ dn unɹ oʇ sʍoןןⱯ", diff --git a/src/generated/resources/assets/gtceu/lang/en_us.json b/src/generated/resources/assets/gtceu/lang/en_us.json index a35429f8983..aa05501a15c 100644 --- a/src/generated/resources/assets/gtceu/lang/en_us.json +++ b/src/generated/resources/assets/gtceu/lang/en_us.json @@ -33,10 +33,11 @@ "behavior.portable_scanner.machine_progress": "Progress/Load: %s / %s", "behavior.portable_scanner.machine_upwards_facing": "Upwards Facing: %s", "behavior.portable_scanner.mode.caption": "Display mode: %s", - "behavior.portable_scanner.mode.show_all_info": "Show all info", + "behavior.portable_scanner.mode.show_all_info": "Show all info (excluding internal info)", "behavior.portable_scanner.mode.show_block_info": "Show block info", "behavior.portable_scanner.mode.show_electrical_info": "Show electrical info", "behavior.portable_scanner.mode.show_environmental_info": "Show environmental info", + "behavior.portable_scanner.mode.show_internal_info": "Show internal debugging info", "behavior.portable_scanner.mode.show_machine_info": "Show machine info", "behavior.portable_scanner.mode.show_recipe_info": "Show recipe info", "behavior.portable_scanner.muffled": "Muffled.", @@ -1119,6 +1120,8 @@ "block.gtceu.substation_capacitor.tooltip_filled": "§cEnergy Capacity: §f%d EU", "block.gtceu.superconducting_coil": "Superconducting Coil Block", "block.gtceu.tempered_glass": "Tempered Glass", + "block.gtceu.test_mui": "Test Mui", + "block.gtceu.test_mui_new": "Test Mui New", "block.gtceu.the_end_marker": "The End", "block.gtceu.the_nether_marker": "The Nether", "block.gtceu.titanium_crate": "Titanium Crate", @@ -1750,6 +1753,7 @@ "config.gtceu.option.compat": "compat", "config.gtceu.option.createCompat": "createCompat", "config.gtceu.option.debug": "debug", + "config.gtceu.option.debugUI": "debugUI", "config.gtceu.option.debugWorldgen": "debugWorldgen", "config.gtceu.option.defaultPaintingColor": "defaultPaintingColor", "config.gtceu.option.defaultUIColor": "defaultUIColor", @@ -1777,6 +1781,7 @@ "config.gtceu.option.energyUsageMultiplier": "energyUsageMultiplier", "config.gtceu.option.environmentalHazardDecayRate": "environmentalHazardDecayRate", "config.gtceu.option.environmentalHazards": "environmentalHazards", + "config.gtceu.option.escRestoresLastText": "escRestoresLastText", "config.gtceu.option.euToFeRatio": "euToFeRatio", "config.gtceu.option.extractorRecyclingYield": "extractorRecyclingYield", "config.gtceu.option.feToEuRatio": "feToEuRatio", @@ -1861,6 +1866,7 @@ "config.gtceu.option.renderGrowingPlants": "renderGrowingPlants", "config.gtceu.option.renderer": "renderer", "config.gtceu.option.replaceMinedBlocksWith": "replaceMinedBlocksWith", + "config.gtceu.option.replaceVanillaTooltips": "replaceVanillaTooltips", "config.gtceu.option.replaceWithCobbleVersion": "replaceWithCobbleVersion", "config.gtceu.option.requireGTToolsForBlocks": "requireGTToolsForBlocks", "config.gtceu.option.rngDamageElectricTools": "rngDamageElectricTools", @@ -1869,6 +1875,7 @@ "config.gtceu.option.shouldWeatherOrTerrainExplosion": "shouldWeatherOrTerrainExplosion", "config.gtceu.option.showDimensionTier": "showDimensionTier", "config.gtceu.option.smallBoilers": "smallBoilers", + "config.gtceu.option.smoothProgressBar": "smoothProgressBar", "config.gtceu.option.solarBoilerBaseOutput": "solarBoilerBaseOutput", "config.gtceu.option.solidBoilerBaseOutput": "solidBoilerBaseOutput", "config.gtceu.option.sprayCanChainLength": "sprayCanChainLength", @@ -1885,11 +1892,14 @@ "config.gtceu.option.toolCraftingSounds": "toolCraftingSounds", "config.gtceu.option.toolUseSounds": "toolUseSounds", "config.gtceu.option.tools": "tools", + "config.gtceu.option.tooltipPos": "tooltipPos", "config.gtceu.option.treeFellingDelay": "treeFellingDelay", "config.gtceu.option.tungstensteelBoilerHeatSpeed": "tungstensteelBoilerHeatSpeed", "config.gtceu.option.tungstensteelBoilerMaxTemperature": "tungstensteelBoilerMaxTemperature", + "config.gtceu.option.ui": "ui", "config.gtceu.option.universalHazards": "universalHazards", "config.gtceu.option.updateIntervals": "updateIntervals", + "config.gtceu.option.useDarkThemeByDefault": "useDarkThemeByDefault", "config.gtceu.option.useVBO": "useVBO", "config.gtceu.option.voltageTierAdvImpeller": "voltageTierAdvImpeller", "config.gtceu.option.voltageTierAdvNanoSuit": "voltageTierAdvNanoSuit", @@ -2263,7 +2273,8 @@ "gtceu.creative.chest.ipc": "Items per Cycle", "gtceu.creative.chest.item": "Item", "gtceu.creative.chest.tpc": "Ticks per Cycle", - "gtceu.creative.computation.average": "Average Requested CWUt", + "gtceu.creative.computation.average": "Average CWU/tick: %d", + "gtceu.creative.computation.max_usage": "Max CWU/tick:", "gtceu.creative.energy.amperage": "Amperage", "gtceu.creative.energy.sink": "Sink", "gtceu.creative.energy.source": "Source", @@ -2405,6 +2416,7 @@ "gtceu.gui.fluid_amount": "Fluid Amount:", "gtceu.gui.fluid_auto_input.tooltip.disabled": "Fluid Auto-Input Disabled", "gtceu.gui.fluid_auto_input.tooltip.enabled": "Fluid Auto-Input Enabled", + "gtceu.gui.fluid_auto_output": "Fluid Output: %s", "gtceu.gui.fluid_auto_output.allow_input.disabled": "disable fluids input from the output side", "gtceu.gui.fluid_auto_output.allow_input.enabled": "allow fluids input from the output side", "gtceu.gui.fluid_auto_output.disabled": "Fluid Auto Output: §cDisabled", @@ -2416,6 +2428,7 @@ "gtceu.gui.fluid_auto_output.tooltip.enabled": "Fluid Auto-Output Enabled", "gtceu.gui.fluid_auto_output.unselected.0": "Fluid Auto Output", "gtceu.gui.fluid_auto_output.unselected.1": "§7Select a side of the machine to configure its output.", + "gtceu.gui.fluid_input_from_output": "Fluid Input from Output: %s", "gtceu.gui.fluid_lock.tooltip.disabled": "Fluid Locking Disabled", "gtceu.gui.fluid_lock.tooltip.enabled": "Fluid Locking Enabled", "gtceu.gui.fluid_voiding": "§7Voiding §9Fluids", @@ -2424,6 +2437,7 @@ "gtceu.gui.fuel_amount": "Fuel Amount:", "gtceu.gui.item_auto_input.tooltip.disabled": "Item Auto-Input Disabled", "gtceu.gui.item_auto_input.tooltip.enabled": "Item Auto-Input Enabled", + "gtceu.gui.item_auto_output": "Item Output: %s", "gtceu.gui.item_auto_output.allow_input.disabled": "disable items input from the output side", "gtceu.gui.item_auto_output.allow_input.enabled": "allow items input from the output side", "gtceu.gui.item_auto_output.disabled": "Item Auto Output: §cDisabled", @@ -2435,6 +2449,8 @@ "gtceu.gui.item_auto_output.tooltip.enabled": "Item Auto-Output Enabled", "gtceu.gui.item_auto_output.unselected.0": "Item Auto Output", "gtceu.gui.item_auto_output.unselected.1": "§7Select a side of the machine to configure its output.", + "gtceu.gui.item_collector.range": "Range: ", + "gtceu.gui.item_input_from_output": "Item Input from Output: %s", "gtceu.gui.item_lock.tooltip.disabled": "Item Locking Disabled", "gtceu.gui.item_lock.tooltip.enabled": "Item Locking Enabled", "gtceu.gui.item_voiding": "§7Voiding §6Items", @@ -3068,6 +3084,7 @@ "gtceu.machine.opv_gas_collector.tooltip": "§7Collects Gas from the universe depending on the dimension", "gtceu.machine.opv_rock_crusher.tooltip": "§7Volcanic Formation Chamber", "gtceu.machine.parallel_hatch.display": "Adjust the maximum parallel of the multiblock", + "gtceu.machine.parallel_hatch.parallel_ui": "Parallels", "gtceu.machine.parallel_hatch_mk5.tooltip": "Allows to run up to 4 recipes in parallel.", "gtceu.machine.parallel_hatch_mk6.tooltip": "Allows to run up to 16 recipes in parallel.", "gtceu.machine.parallel_hatch_mk7.tooltip": "Allows to run up to 64 recipes in parallel.", diff --git a/src/generated/resources/assets/gtceu/models/block/machine/test_mui.json b/src/generated/resources/assets/gtceu/models/block/machine/test_mui.json new file mode 100644 index 00000000000..f86f60d96f9 --- /dev/null +++ b/src/generated/resources/assets/gtceu/models/block/machine/test_mui.json @@ -0,0 +1,18 @@ +{ + "parent": "minecraft:block/block", + "loader": "gtceu:machine", + "machine": "gtceu:test_mui", + "replaceable_textures": [ + "all" + ], + "variants": { + "": { + "model": { + "parent": "gtceu:block/machine/part/computer_monitor", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_clean_stainless_steel" + } + } + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtceu/models/block/machine/test_mui_new.json b/src/generated/resources/assets/gtceu/models/block/machine/test_mui_new.json new file mode 100644 index 00000000000..12fd1d59a84 --- /dev/null +++ b/src/generated/resources/assets/gtceu/models/block/machine/test_mui_new.json @@ -0,0 +1,42 @@ +{ + "parent": "minecraft:block/block", + "loader": "gtceu:machine", + "machine": "gtceu:test_mui_new", + "replaceable_textures": [ + "all" + ], + "variants": { + "recipe_logic_status=idle": { + "model": { + "parent": "gtceu:block/machine/part/computer_monitor", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_clean_stainless_steel" + } + } + }, + "recipe_logic_status=suspend": { + "model": { + "parent": "gtceu:block/machine/part/computer_monitor", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_clean_stainless_steel" + } + } + }, + "recipe_logic_status=waiting": { + "model": { + "parent": "gtceu:block/machine/part/computer_monitor", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_clean_stainless_steel" + } + } + }, + "recipe_logic_status=working": { + "model": { + "parent": "gtceu:block/machine/part/computer_monitor", + "textures": { + "all": "gtceu:block/casings/solid/machine_casing_clean_stainless_steel" + } + } + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtceu/models/item/test_mui.json b/src/generated/resources/assets/gtceu/models/item/test_mui.json new file mode 100644 index 00000000000..9ae958ad11c --- /dev/null +++ b/src/generated/resources/assets/gtceu/models/item/test_mui.json @@ -0,0 +1,3 @@ +{ + "parent": "gtceu:block/machine/test_mui" +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtceu/models/item/test_mui_new.json b/src/generated/resources/assets/gtceu/models/item/test_mui_new.json new file mode 100644 index 00000000000..e741c72dc8d --- /dev/null +++ b/src/generated/resources/assets/gtceu/models/item/test_mui_new.json @@ -0,0 +1,3 @@ +{ + "parent": "gtceu:block/machine/test_mui_new" +} \ No newline at end of file diff --git a/src/main/java/com/gregtechceu/gtceu/api/block/IMachineBlock.java b/src/main/java/com/gregtechceu/gtceu/api/block/IMachineBlock.java index 26ad770884b..3e9a0042a10 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/block/IMachineBlock.java +++ b/src/main/java/com/gregtechceu/gtceu/api/block/IMachineBlock.java @@ -4,6 +4,7 @@ import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; import com.gregtechceu.gtceu.api.machine.MachineDefinition; import com.gregtechceu.gtceu.api.machine.MetaMachine; +import com.gregtechceu.gtceu.syncsystem.ManagedSyncBlockEntity; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; @@ -64,9 +65,13 @@ default BlockEntityTicker getTicker(Level level, Bloc if (blockEntityType == getDefinition().getBlockEntityType()) { if (!level.isClientSide) { return (pLevel, pPos, pState, pTile) -> { + pTile.setChanged(); if (pTile instanceof IMachineBlockEntity metaMachine) { metaMachine.getMetaMachine().serverTick(); } + if (pTile instanceof ManagedSyncBlockEntity syncObj) { + syncObj.updateTick(); + } }; } else { return (pLevel, pPos, pState, pTile) -> { diff --git a/src/main/java/com/gregtechceu/gtceu/api/block/MetaMachineBlock.java b/src/main/java/com/gregtechceu/gtceu/api/block/MetaMachineBlock.java index 14c8f7597ea..aa86b9d715c 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/block/MetaMachineBlock.java +++ b/src/main/java/com/gregtechceu/gtceu/api/block/MetaMachineBlock.java @@ -118,7 +118,6 @@ public void setPlacedBy(Level pLevel, BlockPos pPos, BlockState pState, @Nullabl if (machine != null) { if (player instanceof ServerPlayer sPlayer) { machine.setOwnerUUID(sPlayer.getUUID()); - machine.markDirty(); } } if (machine instanceof IDropSaveMachine dropSaveMachine) { @@ -285,7 +284,6 @@ public InteractionResult use(BlockState state, Level world, BlockPos pos, Player if (machine != null && machine.getOwnerUUID() == null && player instanceof ServerPlayer sPlayer) { machine.setOwnerUUID(sPlayer.getUUID()); - machine.markDirty(); } Set types = ToolHelper.getToolTypes(itemStack); @@ -314,9 +312,15 @@ public InteractionResult use(BlockState state, Level world, BlockPos pos, Player var result = interactedMachine.onUse(state, world, pos, player, hand, hit); if (result != InteractionResult.PASS) return result; } - if (shouldOpenUi && machine instanceof IUIMachine uiMachine && + if (shouldOpenUi && MachineOwner.canOpenOwnerMachine(player, machine)) { - return uiMachine.tryToOpenUI(player, hand, hit); + if (machine.getDefinition().getUI() != null) { + return machine.getDefinition().getUI().tryToOpenUI(player, hand, hit); + } else if (machine instanceof IMuiMachine muiMachine) { + return muiMachine.tryToOpenUI(player, hand, hit); + } else if (machine instanceof IUIMachine uiMachine) { + return uiMachine.tryToOpenUI(player, hand, hit); + } } return shouldOpenUi ? InteractionResult.PASS : InteractionResult.CONSUME; } diff --git a/src/main/java/com/gregtechceu/gtceu/api/block/PipeBlock.java b/src/main/java/com/gregtechceu/gtceu/api/block/PipeBlock.java index c4079b9f324..91b4258f295 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/block/PipeBlock.java +++ b/src/main/java/com/gregtechceu/gtceu/api/block/PipeBlock.java @@ -20,6 +20,7 @@ import com.gregtechceu.gtceu.common.item.CoverPlaceBehavior; import com.gregtechceu.gtceu.config.ConfigHolder; import com.gregtechceu.gtceu.data.recipe.VanillaRecipeHelper; +import com.gregtechceu.gtceu.syncsystem.ManagedSyncBlockEntity; import com.gregtechceu.gtceu.utils.GTUtil; import com.lowdragmc.lowdraglib.client.renderer.IBlockRendererProvider; @@ -437,6 +438,9 @@ public BlockEntityTicker getTicker(Level level, Block if (pTile instanceof IPipeNode pipeNode) { pipeNode.serverTick(); } + if (pTile instanceof ManagedSyncBlockEntity syncObj) { + syncObj.updateTick(); + } }; } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/blockentity/MetaMachineBlockEntity.java b/src/main/java/com/gregtechceu/gtceu/api/blockentity/MetaMachineBlockEntity.java index db0589bd9c4..e1828208272 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/blockentity/MetaMachineBlockEntity.java +++ b/src/main/java/com/gregtechceu/gtceu/api/blockentity/MetaMachineBlockEntity.java @@ -16,15 +16,13 @@ import com.gregtechceu.gtceu.client.model.IBlockEntityRendererBakedModel; import com.gregtechceu.gtceu.client.model.machine.MachineRenderState; import com.gregtechceu.gtceu.common.datafixers.TagFixer; +import com.gregtechceu.gtceu.syncsystem.ManagedSyncBlockEntity; +import com.gregtechceu.gtceu.syncsystem.SyncDataHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.RerenderOnChanged; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.lowdragmc.lowdraglib.gui.texture.ResourceTexture; -import com.lowdragmc.lowdraglib.syncdata.IManaged; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.annotation.RequireRerender; -import com.lowdragmc.lowdraglib.syncdata.field.FieldManagedStorage; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; -import com.lowdragmc.lowdraglib.syncdata.managed.MultiManagedStorage; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.block.BlockRenderDispatcher; @@ -34,7 +32,7 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; -import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.AABB; @@ -53,20 +51,19 @@ import java.util.*; -public class MetaMachineBlockEntity extends BlockEntity implements IMachineBlockEntity, IManaged { +public class MetaMachineBlockEntity extends ManagedSyncBlockEntity implements IMachineBlockEntity { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( - MetaMachineBlockEntity.class); - - public final MultiManagedStorage managedStorage = new MultiManagedStorage(); @Getter - private final FieldManagedStorage syncStorage = new FieldManagedStorage(this); + protected final SyncDataHolder syncDataHolder = new SyncDataHolder(this); + @Getter + @SaveField(nbtKey = "machine") + @SyncToClient public final MetaMachine metaMachine; @Getter - @Persisted - @DescSynced - @RequireRerender + @SaveField + @SyncToClient + @RerenderOnChanged private MachineRenderState renderState; private final long offset = GTValues.RNG.nextInt(20); @@ -74,26 +71,6 @@ public MetaMachineBlockEntity(BlockEntityType type, BlockPos pos, BlockState super(type, pos, blockState); this.renderState = getDefinition().defaultRenderState(); this.metaMachine = getDefinition().createMetaMachine(this); - - this.getRootStorage().attach(getSyncStorage()); - } - - @Override - public MultiManagedStorage getRootStorage() { - return managedStorage; - } - - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - - @Override - public void onChanged() { - var level = getLevel(); - if (level != null && !level.isClientSide && level.getServer() != null) { - level.getServer().execute(this::setChanged); - } } @Override @@ -324,11 +301,29 @@ public AABB getRenderBoundingBox() { } @Override - public void load(CompoundTag tag) { + public void load(@NotNull CompoundTag tag) { TagFixer.fixFluidTags(tag); + if (!tag.contains("machine")) { + var compound = tag.copy(); + tag.put("machine", compound); + } super.load(tag); } + @Override + public void scheduleRenderUpdate() { + var pos = getBlockPos(); + var level = getLevel(); + if (level != null) { + var state = level.getBlockState(pos); + if (level.isClientSide) { + level.sendBlockUpdated(pos, state, state, Block.UPDATE_IMMEDIATE); + } else { + level.blockEvent(pos, state.getBlock(), 1, 0); + } + } + } + public static class AE2CallWrapper { public static LazyOptional getGridNodeHostCapability(Capability cap, MetaMachine machine, diff --git a/src/main/java/com/gregtechceu/gtceu/api/blockentity/PipeBlockEntity.java b/src/main/java/com/gregtechceu/gtceu/api/blockentity/PipeBlockEntity.java index 23f88ee3fd6..32913faacbc 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/blockentity/PipeBlockEntity.java +++ b/src/main/java/com/gregtechceu/gtceu/api/blockentity/PipeBlockEntity.java @@ -15,18 +15,13 @@ import com.gregtechceu.gtceu.common.data.GTMaterialBlocks; import com.gregtechceu.gtceu.common.data.GTMaterials; import com.gregtechceu.gtceu.common.datafixers.TagFixer; +import com.gregtechceu.gtceu.syncsystem.ManagedSyncBlockEntity; +import com.gregtechceu.gtceu.syncsystem.annotations.RerenderOnChanged; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.gregtechceu.gtceu.utils.GTUtil; import com.lowdragmc.lowdraglib.gui.texture.ResourceTexture; -import com.lowdragmc.lowdraglib.syncdata.IEnhancedManaged; -import com.lowdragmc.lowdraglib.syncdata.IManagedStorage; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.annotation.RequireRerender; -import com.lowdragmc.lowdraglib.syncdata.blockentity.IAsyncAutoSyncBlockEntity; -import com.lowdragmc.lowdraglib.syncdata.blockentity.IAutoPersistBlockEntity; -import com.lowdragmc.lowdraglib.syncdata.field.FieldManagedStorage; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.BlockPos; @@ -47,7 +42,6 @@ import com.mojang.datafixers.util.Pair; import lombok.Getter; -import lombok.Setter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -60,43 +54,36 @@ @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault public abstract class PipeBlockEntity & IPipeType, NodeDataType> - extends BlockEntity implements IPipeNode, IEnhancedManaged, - IAsyncAutoSyncBlockEntity, IAutoPersistBlockEntity, IToolGridHighlight, IToolable { + extends ManagedSyncBlockEntity + implements IPipeNode, IToolGridHighlight, IToolable { - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(PipeBlockEntity.class); - @Getter - private final FieldManagedStorage syncStorage = new FieldManagedStorage(this); private final long offset = GTValues.RNG.nextInt(20); @Getter - @DescSynced - @Persisted(key = "cover") + @SyncToClient + @SaveField(nbtKey = "cover") protected final PipeCoverContainer coverContainer; @Getter - @Setter - @DescSynced - @Persisted - @RequireRerender + @SyncToClient + @SaveField + @RerenderOnChanged protected int connections = Node.ALL_CLOSED; - @Setter - @DescSynced - @Persisted - @RequireRerender + @SyncToClient + @SaveField + @RerenderOnChanged private int blockedConnections = Node.ALL_CLOSED; private NodeDataType cachedNodeData; - @Persisted - @DescSynced - @RequireRerender + @SaveField + @SyncToClient + @RerenderOnChanged @Getter - @Setter private int paintingColor = -1; - @RequireRerender - @DescSynced - @Persisted - @Setter + @RerenderOnChanged + @SyncToClient + @SaveField @NotNull private Material frameMaterial = GTMaterials.NULL; private final List serverTicks; @@ -116,24 +103,6 @@ public void scheduleRenderUpdate() { IPipeNode.super.scheduleRenderUpdate(); } - @Override - public IManagedStorage getRootStorage() { - return syncStorage; - } - - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - - @Override - public void onChanged() { - var level = getLevel(); - if (level != null && !level.isClientSide && level.getServer() != null) { - level.getServer().execute(this::setChanged); - } - } - @Override public long getOffsetTimer() { return level == null ? offset : (level.getServer().getTickCount() + offset); @@ -151,6 +120,26 @@ public void clearRemoved() { coverContainer.onLoad(); } + public void setConnections(int connections) { + this.connections = connections; + syncDataHolder.markClientSyncFieldDirty("connections"); + } + + public void setBlockedConnections(int blocked) { + this.blockedConnections = blocked; + syncDataHolder.markClientSyncFieldDirty("blockedConnections"); + } + + public void setPaintingColor(int col) { + paintingColor = col; + syncDataHolder.markClientSyncFieldDirty("paintingColor"); + } + + public void setFrameMaterial(Material mat) { + frameMaterial = mat; + syncDataHolder.markClientSyncFieldDirty("frameMaterial"); + } + @Override public int getNumConnections() { int count = 0; @@ -225,6 +214,7 @@ public final void serverTick() { public void setBlocked(Direction side, boolean isBlocked) { if (level instanceof ServerLevel serverLevel && canHaveBlockedFaces()) { blockedConnections = withSideConnection(blockedConnections, side, isBlocked); + syncDataHolder.markClientSyncFieldDirty("blockedConnections"); setChanged(); LevelPipeNet worldPipeNet = getPipeBlock().getWorldPipeNet(serverLevel); PipeNet net = worldPipeNet.getNetFromPos(getBlockPos()); @@ -268,7 +258,7 @@ public void setConnection(Direction side, boolean connected, boolean fromNeighbo } connections = withSideConnection(connections, side, connected); - + syncDataHolder.markClientSyncFieldDirty("connections"); updateNetworkConnection(side, connected); // notify neighbor of change so Auto Output updates its ticking status getLevel().neighborChanged(getBlockPos().relative(side), getPipeBlock(), getBlockPos()); diff --git a/src/main/java/com/gregtechceu/gtceu/api/capability/ICoverable.java b/src/main/java/com/gregtechceu/gtceu/api/capability/ICoverable.java index 882f145bd53..51a3d94af9e 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/capability/ICoverable.java +++ b/src/main/java/com/gregtechceu/gtceu/api/capability/ICoverable.java @@ -37,9 +37,9 @@ public interface ICoverable extends ITickSubscription { BlockPos getPos(); - long getOffsetTimer(); + BlockState getState(); - void markDirty(); + long getOffsetTimer(); boolean isInValid(); @@ -88,7 +88,6 @@ default boolean placeCoverOnSide(Direction side, ItemStack itemStack, CoverDefin coverBehavior.onLoad(); setCoverAtSide(coverBehavior, side); notifyBlockUpdate(); - markDirty(); scheduleNeighborShapeUpdate(); // TODO achievement // AdvancementTriggers.FIRST_COVER_PLACE.trigger((PlayerMP) player); @@ -114,7 +113,6 @@ default boolean removeCover(boolean dropItself, Direction side, @Nullable Player } notifyBlockUpdate(); - markDirty(); scheduleNeighborShapeUpdate(); return true; } diff --git a/src/main/java/com/gregtechceu/gtceu/api/cover/CoverBehavior.java b/src/main/java/com/gregtechceu/gtceu/api/cover/CoverBehavior.java index 9d125bdb394..f40564dda7e 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/cover/CoverBehavior.java +++ b/src/main/java/com/gregtechceu/gtceu/api/cover/CoverBehavior.java @@ -10,13 +10,13 @@ import com.gregtechceu.gtceu.api.transfer.fluid.IFluidHandlerModifiable; import com.gregtechceu.gtceu.client.renderer.cover.ICoverRenderer; import com.gregtechceu.gtceu.client.renderer.cover.IDynamicCoverRenderer; +import com.gregtechceu.gtceu.syncsystem.ISyncManaged; +import com.gregtechceu.gtceu.syncsystem.ManagedSyncBlockEntity; +import com.gregtechceu.gtceu.syncsystem.SyncDataHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.lowdragmc.lowdraglib.gui.texture.ResourceTexture; -import com.lowdragmc.lowdraglib.syncdata.IEnhancedManaged; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.FieldManagedStorage; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.BlockPos; @@ -48,21 +48,20 @@ */ @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault -public abstract class CoverBehavior implements IEnhancedManaged, IToolGridHighlight { - - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(CoverBehavior.class); +public abstract class CoverBehavior implements ISyncManaged, IToolGridHighlight { @Getter - private final FieldManagedStorage syncStorage = new FieldManagedStorage(this); + protected final SyncDataHolder syncDataHolder = new SyncDataHolder(this); + public final CoverDefinition coverDefinition; public final ICoverable coverHolder; public final Direction attachedSide; @Getter - @Persisted - @DescSynced + @SaveField + @SyncToClient protected ItemStack attachItem = ItemStack.EMPTY; @Getter - @Persisted + @SaveField protected int redstoneSignalOutput = 0; public CoverBehavior(CoverDefinition definition, ICoverable coverHolder, Direction attachedSide) { @@ -74,21 +73,14 @@ public CoverBehavior(CoverDefinition definition, ICoverable coverHolder, Directi ////////////////////////////////////// // ***** Initialization ******// ////////////////////////////////////// - @Override public void scheduleRenderUpdate() { coverHolder.scheduleRenderUpdate(); } @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - - @Override - public void onChanged() { - var level = coverHolder.getLevel(); - if (level != null && !level.isClientSide && level.getServer() != null) { - level.getServer().execute(coverHolder::markDirty); + public void markAsChanged() { + if (coverHolder instanceof ManagedSyncBlockEntity syncEntity) { + syncEntity.markAsChanged(); } } @@ -115,6 +107,7 @@ public boolean canAttach() { public void onAttached(ItemStack itemStack, @Nullable ServerPlayer player) { attachItem = itemStack.copy(); attachItem.setCount(1); + syncDataHolder.markClientSyncFieldDirty("attachItem"); } public void onLoad() {} @@ -147,7 +140,6 @@ public void setRedstoneSignalOutput(int redstoneSignalOutput) { if (this.redstoneSignalOutput == redstoneSignalOutput) return; this.redstoneSignalOutput = redstoneSignalOutput; coverHolder.notifyBlockUpdate(); - coverHolder.markDirty(); } public boolean canConnectRedstone() { @@ -158,6 +150,12 @@ public boolean canConnectRedstone() { // ******* Interaction *******// ////////////////////////////////////// public InteractionResult onScrewdriverClick(Player playerIn, InteractionHand hand, BlockHitResult hitResult) { + if (this instanceof IMuiCover muiCover) { + if (playerIn instanceof ServerPlayer serverPlayer) { + com.gregtechceu.gtceu.common.mui.factory.CoverUIFactory.INSTANCE.open(serverPlayer, muiCover); + } + return InteractionResult.sidedSuccess(playerIn.level().isClientSide); + } if (this instanceof IUICover) { if (playerIn instanceof ServerPlayer serverPlayer) { CoverUIFactory.INSTANCE.openUI(this, serverPlayer); diff --git a/src/main/java/com/gregtechceu/gtceu/api/cover/IMuiCover.java b/src/main/java/com/gregtechceu/gtceu/api/cover/IMuiCover.java new file mode 100644 index 00000000000..404411a87dc --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/cover/IMuiCover.java @@ -0,0 +1,213 @@ +package com.gregtechceu.gtceu.api.cover; + +import com.gregtechceu.gtceu.api.mui.base.IUIHolder; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.drawable.ItemDrawable; +import com.gregtechceu.gtceu.api.mui.factory.SidedPosGuiData; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.utils.MouseData; +import com.gregtechceu.gtceu.api.mui.value.BoolValue; +import com.gregtechceu.gtceu.api.mui.value.sync.EnumSyncValue; +import com.gregtechceu.gtceu.api.mui.value.sync.IntSyncValue; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.api.mui.widget.ParentWidget; +import com.gregtechceu.gtceu.api.mui.widgets.ToggleButton; +import com.gregtechceu.gtceu.api.mui.widgets.layout.Flow; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.UISettings; +import com.gregtechceu.gtceu.common.mui.GTGuiTextures; +import com.gregtechceu.gtceu.common.mui.GTGuiTheme; +import com.gregtechceu.gtceu.common.mui.GTGuis; + +import net.minecraft.util.StringRepresentable; +import net.minecraft.world.item.ItemStack; + +public interface IMuiCover extends IUIHolder { + + default CoverBehavior self() { + return (CoverBehavior) this; + } + + default boolean isInvalid() { + return self().coverHolder.isInValid() || self().coverHolder.getCoverAtSide(self().attachedSide) != self(); + } + + default GTGuiTheme getUITheme() { + return GTGuiTheme.COVER; + } + + @Override + default ModularPanel buildUI(SidedPosGuiData data, PanelSyncManager syncManager, UISettings settings) { + IWidget widget = createCoverUI(data, syncManager, settings); + return GTGuis.createPanel(this.self(), 176, 166) + .background(GTGuiTextures.BACKGROUND) + .child(widget) + .bindPlayerInventory(); + } + + ParentWidget createCoverUI(SidedPosGuiData data, PanelSyncManager syncManager, UISettings settings); + + /* Helper methods for UI creation with covers that are commonly used */ + + /** + * The color used for Cover UI titles, and used in {@link #createTitleRow}. + */ + int UI_TITLE_COLOR = 0xFF222222; + /** + * The color used for Cover UI text. Available for reference, but is + * handled automatically by the {@link GTGuiTheme#COVER} theme. + */ + int UI_TEXT_COLOR = 0xFF555555; + + /** + * Create the Title bar widget for a Cover. + */ + static Flow createTitleRow(ItemStack stack) { + return Flow.row() + .height(16).coverChildrenWidth() + .child(new ItemDrawable(stack).asWidget().size(16).marginRight(4)) + .child(IKey.lang(stack.getHoverName()) + .color(UI_TITLE_COLOR) + .asWidget().heightRel(1.0f)); + } + + /** + * Create a new settings row for a Cover setting. + */ + default ParentWidget createSettingsRow() { + return new ParentWidget<>().height(16).widthRel(1.0f).marginBottom(2); + } + + default int getIncrementValue() { + return getIncrementValue(MouseData.create(-1)); + } + + default int getIncrementValue(MouseData data) { + int adjust = 1; + if (data.shift()) adjust *= 4; + if (data.ctrl()) adjust *= 16; + if (data.alt()) adjust *= 64; + return adjust; + } + + default IKey createAdjustOverlay(boolean increment) { + final StringBuilder builder = new StringBuilder(); + builder.append(increment ? '+' : '-'); + builder.append(getIncrementValue(MouseData.create(-1))); + + float scale = 1f; + if (builder.length() == 3) { + scale = 0.8f; + } else if (builder.length() == 4) { + scale = 0.6f; + } else if (builder.length() > 4) { + scale = 0.5f; + } + return IKey.str(builder.toString()) + .color(Color.WHITE.main) + .scale(scale); + } + + /** + * Get a BoolValue for use with toggle buttons which are "linked together," + * meaning only one of them can be pressed at a time. + */ + default > BoolValue.Dynamic boolValueOf(EnumSyncValue syncValue, T value) { + return new BoolValue.Dynamic(() -> syncValue.getValue() == value, $ -> syncValue.setValue(value)); + } + + /** + * Get a BoolValue for use with toggle buttons which are "linked together," + * meaning only one of them can be pressed at a time. + */ + default BoolValue.Dynamic boolValueOf(IntSyncValue syncValue, int value) { + return new BoolValue.Dynamic(() -> syncValue.getValue() == value, $ -> syncValue.setValue(value)); + } + + class EnumRowBuilder> { + + private EnumSyncValue syncValue; + private final Class enumValue; + private String lang; + private IDrawable[] background; + private IDrawable selectedBackground; + private IDrawable[] overlay; + + public EnumRowBuilder(Class enumValue) { + this.enumValue = enumValue; + } + + public EnumRowBuilder value(EnumSyncValue syncValue) { + this.syncValue = syncValue; + return this; + } + + public EnumRowBuilder lang(String lang) { + this.lang = lang; + return this; + } + + public EnumRowBuilder background(IDrawable... background) { + this.background = background; + return this; + } + + public EnumRowBuilder selectedBackground(IDrawable selectedBackground) { + this.selectedBackground = selectedBackground; + return this; + } + + public EnumRowBuilder overlay(IDrawable... overlay) { + this.overlay = overlay; + return this; + } + + public EnumRowBuilder overlay(int size, IDrawable... overlay) { + this.overlay = new IDrawable[overlay.length]; + for (int i = 0; i < overlay.length; i++) { + this.overlay[i] = overlay[i].asIcon().size(size); + } + return this; + } + + private BoolValue.Dynamic boolValueOf(EnumSyncValue syncValue, T value) { + return new BoolValue.Dynamic(() -> syncValue.getValue() == value, $ -> syncValue.setValue(value)); + } + + public Flow build() { + var row = Flow.row().marginBottom(2).coverChildrenHeight().widthRel(1f); + if (this.enumValue != null && this.syncValue != null) { + for (var enumVal : enumValue.getEnumConstants()) { + var button = new ToggleButton().size(18).marginRight(2) + .value(boolValueOf(this.syncValue, enumVal)); + + if (this.background != null && this.background.length > 0) + button.background(this.background); + else + button.background(GTGuiTextures.MC_BUTTON); + + if (this.selectedBackground != null) + button.selectedBackground(this.selectedBackground); + else + button.selectedBackground(GTGuiTextures.MC_BUTTON_DISABLED); + + if (this.overlay != null) + button.overlay(this.overlay[enumVal.ordinal()]); + + if (enumVal instanceof StringRepresentable serializable) { + button.addTooltipLine(IKey.lang(serializable.getSerializedName())); + } + row.child(button); + } + } + + if (this.lang != null && !this.lang.isEmpty()) + row.child(IKey.lang(this.lang).asWidget().align(Alignment.CenterRight).height(18)); + + return row; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/cover/IUICover.java b/src/main/java/com/gregtechceu/gtceu/api/cover/IUICover.java index 27d7b8cc56e..4161092b8cd 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/cover/IUICover.java +++ b/src/main/java/com/gregtechceu/gtceu/api/cover/IUICover.java @@ -26,11 +26,6 @@ default boolean isRemote() { return self().coverHolder.isRemote(); } - @Override - default void markAsDirty() { - self().coverHolder.markDirty(); - } - @Override default ModularUI createUI(Player entityPlayer) { var widget = createUIWidget(); @@ -48,4 +43,7 @@ default ModularUI createUI(Player entityPlayer) { default void onUIClosed() {} Widget createUIWidget(); + + @Override + default void markAsDirty() {} } diff --git a/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandler.java b/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandler.java index 5855d4c1be6..6932bf36a67 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandler.java +++ b/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandler.java @@ -7,15 +7,14 @@ import com.gregtechceu.gtceu.api.machine.MachineCoverContainer; import com.gregtechceu.gtceu.api.machine.MetaMachine; import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; +import com.gregtechceu.gtceu.syncsystem.ISyncManaged; +import com.gregtechceu.gtceu.syncsystem.SyncDataHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; import com.lowdragmc.lowdraglib.gui.widget.Widget; import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; -import com.lowdragmc.lowdraglib.syncdata.IEnhancedManaged; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.FieldManagedStorage; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.world.item.ItemStack; @@ -30,12 +29,15 @@ @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault -public abstract class FilterHandler> implements IEnhancedManaged { +public abstract class FilterHandler> implements ISyncManaged { - private final IEnhancedManaged container; + @Getter + private final SyncDataHolder syncDataHolder = new SyncDataHolder(this); + + private final ISyncManaged container; - @Persisted - @DescSynced + @SaveField + @SyncToClient @Getter private @NotNull ItemStack filterItem = ItemStack.EMPTY; @@ -47,7 +49,7 @@ public abstract class FilterHandler> implements IEnhan private @NotNull Consumer onFilterRemoved = (filter) -> {}; private @NotNull Consumer onFilterUpdated = (filter) -> {}; - public FilterHandler(IEnhancedManaged container) { + public FilterHandler(ISyncManaged container) { this.container = container; } @@ -141,6 +143,7 @@ private void updateFilter() { } this.filterItem = filterContainer.getStackInSlot(0); + syncDataHolder.markClientSyncFieldDirty("filterItem"); if (this.filter != null) { this.filter = null; @@ -178,27 +181,13 @@ private void updateFilterGroupUI() { } } - ////////////////////////////////////// - // ***** LDLib SyncData ******// - ////////////////////////////////////// - - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(FilterHandler.class); - - @Getter - private final FieldManagedStorage syncStorage = new FieldManagedStorage(this); - - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - @Override - public void onChanged() { - this.container.onChanged(); + public void markAsChanged() { + container.markAsChanged(); } @Override public void scheduleRenderUpdate() { - this.container.scheduleRenderUpdate(); + container.scheduleRenderUpdate(); } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandlers.java b/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandlers.java index d2ccc7aa085..50d74890ae4 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandlers.java +++ b/src/main/java/com/gregtechceu/gtceu/api/cover/filter/FilterHandlers.java @@ -1,13 +1,13 @@ package com.gregtechceu.gtceu.api.cover.filter; -import com.lowdragmc.lowdraglib.syncdata.IEnhancedManaged; +import com.gregtechceu.gtceu.syncsystem.ISyncManaged; import net.minecraft.world.item.ItemStack; import net.minecraftforge.fluids.FluidStack; public interface FilterHandlers { - static FilterHandler item(IEnhancedManaged container) { + static FilterHandler item(ISyncManaged container) { return new FilterHandler<>(container) { @Override @@ -27,7 +27,7 @@ protected boolean canInsertFilterItem(ItemStack itemStack) { }; } - static FilterHandler fluid(IEnhancedManaged container) { + static FilterHandler fluid(ISyncManaged container) { return new FilterHandler<>(container) { @Override diff --git a/src/main/java/com/gregtechceu/gtceu/api/gui/fancy/ConfiguratorPanel.java b/src/main/java/com/gregtechceu/gtceu/api/gui/fancy/ConfiguratorPanel.java index 6ab93be46bc..4762dfeb273 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/gui/fancy/ConfiguratorPanel.java +++ b/src/main/java/com/gregtechceu/gtceu/api/gui/fancy/ConfiguratorPanel.java @@ -444,6 +444,6 @@ public void onClose(Runnable closeCallback) { } private static int getAnimationTime() { - return ConfigHolder.INSTANCE.client.animationTime; + return ConfigHolder.INSTANCE.client.ui.animationTime; } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/gui/fancy/FancyMachineUIWidget.java b/src/main/java/com/gregtechceu/gtceu/api/gui/fancy/FancyMachineUIWidget.java index 5e62d3bf958..a9891226b20 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/gui/fancy/FancyMachineUIWidget.java +++ b/src/main/java/com/gregtechceu/gtceu/api/gui/fancy/FancyMachineUIWidget.java @@ -71,7 +71,7 @@ public FancyMachineUIWidget(IFancyUIProvider mainPage, int width, int height) { this.pageSwitcher = new PageSwitcher(this::switchPage); setBackground(GuiTextures.BACKGROUND.copy() - .setColor(Long.decode(ConfigHolder.INSTANCE.client.defaultUIColor).intValue() | 0xFF000000)); + .setColor(Long.decode(ConfigHolder.INSTANCE.client.ui.defaultUIColor).intValue() | 0xFF000000)); } @Override diff --git a/src/main/java/com/gregtechceu/gtceu/api/gui/widget/PatternPreviewWidget.java b/src/main/java/com/gregtechceu/gtceu/api/gui/widget/PatternPreviewWidget.java index 3855367f846..ec1ffa01db5 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/gui/widget/PatternPreviewWidget.java +++ b/src/main/java/com/gregtechceu/gtceu/api/gui/widget/PatternPreviewWidget.java @@ -389,7 +389,7 @@ private MBPattern initializePattern(MultiblockShapeInfo shapeInfo, HashSet parts = gatherBlockDrops(blockMap); diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/ComponentItem.java b/src/main/java/com/gregtechceu/gtceu/api/item/ComponentItem.java index b45213e753e..4c3b171a546 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/ComponentItem.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/ComponentItem.java @@ -1,9 +1,15 @@ package com.gregtechceu.gtceu.api.item; +import com.gregtechceu.gtceu.GTCEu; import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper; import com.gregtechceu.gtceu.api.capability.IElectricItem; import com.gregtechceu.gtceu.api.item.capability.ElectricItem; import com.gregtechceu.gtceu.api.item.component.*; +import com.gregtechceu.gtceu.api.mui.base.IItemUIHolder; +import com.gregtechceu.gtceu.api.mui.factory.PlayerInventoryGuiData; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.UISettings; import com.lowdragmc.lowdraglib.client.renderer.IItemRendererProvider; import com.lowdragmc.lowdraglib.client.renderer.IRenderer; @@ -49,7 +55,8 @@ @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault public class ComponentItem extends Item - implements HeldItemUIFactory.IHeldItemUIHolder, IItemRendererProvider, IComponentItem { + implements HeldItemUIFactory.IHeldItemUIHolder, IItemRendererProvider, IComponentItem, + IItemUIHolder { protected int burnTime = -1; @@ -443,4 +450,24 @@ public ItemStack getInfiniteChargedStack() { electricItem.setInfiniteCharge(true); return itemStack; } + + @Override + public @Nullable ModularPanel buildUI(PlayerInventoryGuiData data, PanelSyncManager syncManager, + UISettings settings) { + for (IItemComponent component : getComponents()) { + if (component instanceof IItemUIHolder uiHolder) { + return uiHolder.buildUI(data, syncManager, settings); + } + } + GTCEu.LOGGER.error("Tried to get UI of {} item when it does not have one!", data.getUsedItemStack()); + return null; + } + + @Override + public boolean shouldOpenUI() { + for (IItemComponent component : getComponents()) { + if (component instanceof IItemUIHolder holder) return holder.shouldOpenUI(); + } + return false; + } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/IMachineBlockEntity.java b/src/main/java/com/gregtechceu/gtceu/api/machine/IMachineBlockEntity.java index df3ff0930d0..d01fe0f0a98 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/IMachineBlockEntity.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/IMachineBlockEntity.java @@ -6,13 +6,7 @@ import com.gregtechceu.gtceu.api.item.tool.IToolGridHighlight; import com.gregtechceu.gtceu.client.model.machine.MachineRenderState; -import com.lowdragmc.lowdraglib.syncdata.blockentity.IAsyncAutoSyncBlockEntity; -import com.lowdragmc.lowdraglib.syncdata.blockentity.IAutoPersistBlockEntity; -import com.lowdragmc.lowdraglib.syncdata.blockentity.IRPCBlockEntity; -import com.lowdragmc.lowdraglib.syncdata.managed.MultiManagedStorage; - import net.minecraft.core.BlockPos; -import net.minecraft.nbt.CompoundTag; import net.minecraft.world.level.BlockAndTintGetter; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; @@ -28,8 +22,7 @@ *

* Also delivers most of the Information about TileEntities. */ -public interface IMachineBlockEntity extends IToolGridHighlight, IAsyncAutoSyncBlockEntity, IRPCBlockEntity, - IAutoPersistBlockEntity, IPaintable, IForgeBlockEntity { +public interface IMachineBlockEntity extends IToolGridHighlight, IPaintable, IForgeBlockEntity { ModelProperty MODEL_DATA_LEVEL = new ModelProperty<>(); ModelProperty MODEL_DATA_POS = new ModelProperty<>(); @@ -98,20 +91,6 @@ default MachineDefinition getDefinition() { long getOffset(); - MultiManagedStorage getRootStorage(); - - @Override - default void saveCustomPersistedData(CompoundTag tag, boolean forDrop) { - IAutoPersistBlockEntity.super.saveCustomPersistedData(tag, forDrop); - getMetaMachine().saveCustomPersistedData(tag, forDrop); - } - - @Override - default void loadCustomPersistedData(CompoundTag tag) { - IAutoPersistBlockEntity.super.loadCustomPersistedData(tag); - getMetaMachine().loadCustomPersistedData(tag); - } - @Override default int getPaintingColor() { return getMetaMachine().getPaintingColor(); diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/MachineCoverContainer.java b/src/main/java/com/gregtechceu/gtceu/api/machine/MachineCoverContainer.java index 9a122721d41..dc0feb8751d 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/MachineCoverContainer.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/MachineCoverContainer.java @@ -1,27 +1,17 @@ package com.gregtechceu.gtceu.api.machine; -import com.gregtechceu.gtceu.GTCEu; import com.gregtechceu.gtceu.api.capability.ICoverable; import com.gregtechceu.gtceu.api.cover.CoverBehavior; import com.gregtechceu.gtceu.api.cover.CoverDefinition; -import com.gregtechceu.gtceu.api.registry.GTRegistries; import com.gregtechceu.gtceu.api.transfer.fluid.IFluidHandlerModifiable; -import com.gregtechceu.gtceu.utils.GTUtil; - -import com.lowdragmc.lowdraglib.syncdata.IEnhancedManaged; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.annotation.ReadOnlyManaged; -import com.lowdragmc.lowdraglib.syncdata.annotation.UpdateListener; -import com.lowdragmc.lowdraglib.syncdata.field.FieldManagedStorage; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; -import com.lowdragmc.lowdraglib.syncdata.managed.IRef; +import com.gregtechceu.gtceu.syncsystem.ISyncManaged; +import com.gregtechceu.gtceu.syncsystem.SyncDataHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.*; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraftforge.items.IItemHandlerModifiable; @@ -30,43 +20,29 @@ import java.util.ArrayList; -public class MachineCoverContainer implements ICoverable, IEnhancedManaged { +public class MachineCoverContainer implements ICoverable, ISyncManaged { - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(MachineCoverContainer.class); @Getter - private final FieldManagedStorage syncStorage = new FieldManagedStorage(this); + private final SyncDataHolder syncDataHolder = new SyncDataHolder(this); @Getter private final MetaMachine machine; - @DescSynced - @Persisted - @UpdateListener(methodName = "onCoverSet") - @ReadOnlyManaged(onDirtyMethod = "onCoverDirty", - serializeMethod = "serializeCoverUid", - deserializeMethod = "deserializeCoverUid") + @SyncToClient + @SaveField + @RerenderOnChanged private CoverBehavior up, down, north, south, west, east; public MachineCoverContainer(MetaMachine machine) { this.machine = machine; } - @SuppressWarnings("unused") - private void onCoverSet(CoverBehavior newValue, CoverBehavior oldValue) { - if (newValue != oldValue && (newValue == null || oldValue == null)) { - scheduleRenderUpdate(); - } - } - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; + public void markAsChanged() { + machine.markAsChanged(); } @Override - public void onChanged() { - var level = getLevel(); - if (level != null && !level.isClientSide && level.getServer() != null) { - level.getServer().execute(this::markDirty); - } + public BlockState getState() { + return machine.getBlockState(); } @Override @@ -84,11 +60,6 @@ public long getOffsetTimer() { return machine.getOffsetTimer(); } - @Override - public void markDirty() { - machine.markDirty(); - } - @Override public void notifyBlockUpdate() { machine.notifyBlockUpdate(); @@ -170,9 +141,7 @@ public void setCoverAtSide(@Nullable CoverBehavior coverBehavior, Direction side case EAST -> east = coverBehavior; case NORTH -> north = coverBehavior; } - if (coverBehavior != null) { - coverBehavior.getSyncStorage().markAllDirty(); - } + getSyncDataHolder().resyncAllFields(); } @Override @@ -184,36 +153,4 @@ public IItemHandlerModifiable getItemHandlerCap(@Nullable Direction side, boolea public IFluidHandlerModifiable getFluidHandlerCap(@Nullable Direction side, boolean useCoverCapability) { return machine.getFluidHandlerCap(side, useCoverCapability); } - - @SuppressWarnings("unused") - private boolean onCoverDirty(CoverBehavior coverBehavior) { - if (coverBehavior != null) { - for (IRef ref : coverBehavior.getSyncStorage().getNonLazyFields()) { - ref.update(); - } - return coverBehavior.getSyncStorage().hasDirtySyncFields() || - coverBehavior.getSyncStorage().hasDirtyPersistedFields(); - } - return false; - } - - @SuppressWarnings("unused") - private CompoundTag serializeCoverUid(CoverBehavior coverBehavior) { - var uid = new CompoundTag(); - uid.putString("id", GTRegistries.COVERS.getKey(coverBehavior.coverDefinition).toString()); - uid.putInt("side", coverBehavior.attachedSide.ordinal()); - return uid; - } - - @SuppressWarnings("unused") - private CoverBehavior deserializeCoverUid(CompoundTag uid) { - var definitionId = new ResourceLocation(uid.getString("id")); - var side = GTUtil.DIRECTIONS[uid.getInt("side")]; - var definition = GTRegistries.COVERS.get(definitionId); - if (definition != null) { - return definition.createCoverBehavior(this, side); - } - GTCEu.LOGGER.error("couldn't find cover definition {}", definitionId); - throw new RuntimeException(); - } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/MachineDefinition.java b/src/main/java/com/gregtechceu/gtceu/api/machine/MachineDefinition.java index 73db1803309..164738d3e48 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/MachineDefinition.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/MachineDefinition.java @@ -6,6 +6,8 @@ import com.gregtechceu.gtceu.api.gui.editor.EditableMachineUI; import com.gregtechceu.gtceu.api.item.MetaMachineItem; import com.gregtechceu.gtceu.api.machine.feature.IRecipeLogicMachine; +import com.gregtechceu.gtceu.api.mui.factory.PanelFactory; +import com.gregtechceu.gtceu.api.mui.theme.ThemeAPI; import com.gregtechceu.gtceu.api.recipe.GTRecipe; import com.gregtechceu.gtceu.api.recipe.GTRecipeType; import com.gregtechceu.gtceu.api.recipe.modifier.RecipeModifier; @@ -127,6 +129,12 @@ public class MachineDefinition implements Supplier { private EditableMachineUI editableUI; @Getter @Setter + private PanelFactory UI; + @Getter + @Setter + private String themeId = ThemeAPI.DEFAULT_ID; + @Getter + @Setter private Reference2IntMap> recipeOutputLimits = new Reference2IntOpenHashMap<>(); @Getter diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/MetaMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/MetaMachine.java index 2db77492aa1..bed3c3cc006 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/MetaMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/MetaMachine.java @@ -32,15 +32,15 @@ import com.gregtechceu.gtceu.common.item.tool.behavior.ToolModeSwitchBehavior; import com.gregtechceu.gtceu.common.machine.owner.MachineOwner; import com.gregtechceu.gtceu.common.machine.owner.PlayerOwner; +import com.gregtechceu.gtceu.syncsystem.ISyncManaged; +import com.gregtechceu.gtceu.syncsystem.ManagedSyncBlockEntity; +import com.gregtechceu.gtceu.syncsystem.SyncDataHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.RerenderOnChanged; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.lowdragmc.lowdraglib.gui.texture.IGuiTexture; import com.lowdragmc.lowdraglib.gui.texture.ResourceTexture; -import com.lowdragmc.lowdraglib.syncdata.IEnhancedManaged; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.annotation.RequireRerender; -import com.lowdragmc.lowdraglib.syncdata.field.FieldManagedStorage; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import com.lowdragmc.lowdraglib.utils.DummyWorld; import net.minecraft.ChatFormatting; @@ -48,7 +48,6 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.locale.Language; -import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.util.RandomSource; import net.minecraft.world.InteractionHand; @@ -73,7 +72,6 @@ import com.mojang.datafixers.util.Pair; import lombok.Getter; -import lombok.Setter; import org.jetbrains.annotations.MustBeInvokedByOverriders; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -94,28 +92,27 @@ */ @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault -public class MetaMachine implements IEnhancedManaged, IToolable, ITickSubscription, IToolGridHighlight, +public class MetaMachine implements ISyncManaged, IToolable, ITickSubscription, IToolGridHighlight, IFancyTooltip, IPaintable, IRedstoneSignalMachine { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(MetaMachine.class); @Getter - private final FieldManagedStorage syncStorage = new FieldManagedStorage(this); - @Setter + protected final SyncDataHolder syncDataHolder = new SyncDataHolder(this); + @Getter - @Persisted - @DescSynced + @SaveField + @SyncToClient @Nullable private UUID ownerUUID; @Getter public final IMachineBlockEntity holder; @Getter - @DescSynced - @Persisted(key = "cover") + @SyncToClient + @SaveField(nbtKey = "cover") protected final MachineCoverContainer coverContainer; @Getter - @Persisted - @DescSynced - @RequireRerender + @SaveField + @SyncToClient + @RerenderOnChanged private int paintingColor = -1; @Getter protected final List traits; @@ -128,10 +125,6 @@ public MetaMachine(IMachineBlockEntity holder) { this.traits = new ArrayList<>(); this.serverTicks = new ArrayList<>(); this.waitingToAdd = new ArrayList<>(); - // bind sync storage - if (holder.getRootStorage() != null) { - this.holder.getRootStorage().attach(getSyncStorage()); - } } ////////////////////////////////////// @@ -139,15 +132,9 @@ public MetaMachine(IMachineBlockEntity holder) { ////////////////////////////////////// @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - - @Override - public void onChanged() { - var level = getLevel(); - if (level != null && !level.isClientSide && level.getServer() != null) { - level.getServer().execute(this::markDirty); + public void markAsChanged() { + if (getHolder() instanceof ManagedSyncBlockEntity syncBlockEntity) { + syncBlockEntity.markAsChanged(); } } @@ -160,7 +147,12 @@ public BlockPos getPos() { } public BlockState getBlockState() { - return holder.getSelf().getBlockState(); + return holder.self().getBlockState(); + } + + public void setOwnerUUID(UUID uuid) { + ownerUUID = uuid; + syncDataHolder.markClientSyncFieldDirty("ownerUUID"); } public boolean isRemote() { @@ -171,7 +163,6 @@ public void notifyBlockUpdate() { holder.notifyBlockUpdate(); } - @Override public void scheduleRenderUpdate() { holder.scheduleRenderUpdate(); } @@ -180,8 +171,7 @@ public void scheduleNeighborShapeUpdate() { Level level = getLevel(); BlockPos pos = getPos(); - if (level == null || pos == null) - return; + if (level == null) return; level.getBlockState(pos).updateNeighbourShapes(level, pos, Block.UPDATE_ALL); } @@ -190,6 +180,7 @@ public void setPaintingColor(int color) { if (color == this.paintingColor) return; this.paintingColor = color; + syncDataHolder.markClientSyncFieldDirty("paintingColor"); this.onPaintingColorChanged(color); MachineRenderState renderState = getRenderState(); @@ -204,12 +195,8 @@ public long getOffsetTimer() { return holder.getOffsetTimer(); } - public void markDirty() { - holder.getSelf().setChanged(); - } - public boolean isInValid() { - return holder.getSelf().isRemoved(); + return holder.self().isRemoved(); } public void onUnload() { @@ -233,24 +220,6 @@ public void onLoad() { } } - /** - * Use for data not able to be saved with the SyncData system, like optional mod compatiblity in internal machines. - * - * @param tag the CompoundTag to load data from - * @param forDrop if the save is done for dropping the machine as an item. - */ - public void saveCustomPersistedData(@NotNull CompoundTag tag, boolean forDrop) { - for (MachineTrait trait : this.getTraits()) { - trait.saveCustomPersistedData(tag, forDrop); - } - } - - public void loadCustomPersistedData(@NotNull CompoundTag tag) { - for (MachineTrait trait : this.getTraits()) { - trait.loadCustomPersistedData(tag); - } - } - ////////////////////////////////////// // ***** Tickable Manager ****// ////////////////////////////////////// @@ -624,7 +593,6 @@ public void setFrontFacing(Direction facing) { if (getLevel() != null && !getLevel().isClientSide) { notifyBlockUpdate(); - markDirty(); } } @@ -653,7 +621,6 @@ public void setUpwardsFacing(@NotNull Direction upwardsFacing) { blockState.setValue(GTBlockStateProperties.UPWARDS_FACING, upwardsFacing)); if (getLevel() != null && !getLevel().isClientSide) { notifyBlockUpdate(); - markDirty(); } } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/SimpleTieredMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/SimpleTieredMachine.java index 23a5ae17609..41bb3cbe453 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/SimpleTieredMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/SimpleTieredMachine.java @@ -23,18 +23,17 @@ import com.gregtechceu.gtceu.common.item.IntCircuitBehaviour; import com.gregtechceu.gtceu.config.ConfigHolder; import com.gregtechceu.gtceu.data.lang.LangHandler; +import com.gregtechceu.gtceu.syncsystem.annotations.RerenderOnChanged; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.gregtechceu.gtceu.utils.GTTransferUtils; +import com.gregtechceu.gtceu.utils.ISubscription; import com.lowdragmc.lowdraglib.gui.texture.GuiTextureGroup; import com.lowdragmc.lowdraglib.gui.texture.IGuiTexture; import com.lowdragmc.lowdraglib.gui.texture.ResourceTexture; import com.lowdragmc.lowdraglib.gui.util.ClickData; import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; -import com.lowdragmc.lowdraglib.syncdata.ISubscription; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.annotation.RequireRerender; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import com.lowdragmc.lowdraglib.utils.Position; import net.minecraft.Util; @@ -69,40 +68,37 @@ public class SimpleTieredMachine extends WorkableTieredMachine implements IAutoOutputBoth, IFancyUIMachine, IHasCircuitSlot { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(SimpleTieredMachine.class, - WorkableTieredMachine.MANAGED_FIELD_HOLDER); - - @Persisted - @DescSynced - @RequireRerender + @SaveField + @SyncToClient + @RerenderOnChanged protected Direction outputFacingItems; - @Persisted - @DescSynced - @RequireRerender + @SaveField + @SyncToClient + @RerenderOnChanged protected Direction outputFacingFluids; @Getter - @Persisted - @DescSynced - @RequireRerender + @SaveField + @SyncToClient + @RerenderOnChanged protected boolean autoOutputItems; @Getter - @Persisted - @DescSynced - @RequireRerender + @SaveField + @SyncToClient + @RerenderOnChanged protected boolean autoOutputFluids; @Getter @Setter - @Persisted + @SaveField protected boolean allowInputFromOutputSideItems; @Getter @Setter - @Persisted + @SaveField protected boolean allowInputFromOutputSideFluids; @Getter - @Persisted + @SaveField protected final CustomItemStackHandler chargerInventory; @Getter - @Persisted + @SaveField protected final NotifiableItemStackHandler circuitInventory; @Nullable protected TickableSubscription autoOutputSubs, batterySubs; @@ -121,10 +117,6 @@ public SimpleTieredMachine(IMachineBlockEntity holder, int tier, Int2IntFunction ////////////////////////////////////// // ***** Initialization ******// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } protected CustomItemStackHandler createChargerItemHandler(Object... args) { var handler = new CustomItemStackHandler() { @@ -215,6 +207,7 @@ public boolean hasAutoOutputItem() { public void setAutoOutputItems(boolean allow) { if (hasAutoOutputItem()) { this.autoOutputItems = allow; + syncDataHolder.markClientSyncFieldDirty("autoOutputItems"); updateAutoOutputSubscription(); } } @@ -223,6 +216,7 @@ public void setAutoOutputItems(boolean allow) { public void setAutoOutputFluids(boolean allow) { if (hasAutoOutputFluid()) { this.autoOutputFluids = allow; + syncDataHolder.markClientSyncFieldDirty("autoOutputFluids"); updateAutoOutputSubscription(); } } @@ -231,6 +225,7 @@ public void setAutoOutputFluids(boolean allow) { public void setOutputFacingFluids(@Nullable Direction outputFacing) { if (hasAutoOutputFluid()) { this.outputFacingFluids = outputFacing; + syncDataHolder.markClientSyncFieldDirty("outputFacingFluids"); updateAutoOutputSubscription(); } } @@ -239,6 +234,7 @@ public void setOutputFacingFluids(@Nullable Direction outputFacing) { public void setOutputFacingItems(@Nullable Direction outputFacing) { if (hasAutoOutputItem()) { this.outputFacingItems = outputFacing; + syncDataHolder.markClientSyncFieldDirty("outputFacingItems"); updateAutoOutputSubscription(); } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/TieredEnergyMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/TieredEnergyMachine.java index 92e5ed60b36..794b8e5e332 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/TieredEnergyMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/TieredEnergyMachine.java @@ -7,13 +7,12 @@ import com.gregtechceu.gtceu.api.machine.feature.ITieredMachine; import com.gregtechceu.gtceu.api.machine.trait.NotifiableEnergyContainer; import com.gregtechceu.gtceu.config.ConfigHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.lowdragmc.lowdraglib.gui.texture.IGuiTexture; import com.lowdragmc.lowdraglib.gui.texture.ProgressTexture; import com.lowdragmc.lowdraglib.gui.widget.ProgressWidget; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.util.Mth; @@ -24,10 +23,8 @@ @MethodsReturnNonnullByDefault public class TieredEnergyMachine extends TieredMachine implements ITieredMachine, IExplosionMachine { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(TieredEnergyMachine.class, - MetaMachine.MANAGED_FIELD_HOLDER); - @Persisted - @DescSynced + @SaveField + @SyncToClient public final NotifiableEnergyContainer energyContainer; protected TickableSubscription explosionSub; @@ -39,10 +36,6 @@ public TieredEnergyMachine(IMachineBlockEntity holder, int tier, Object... args) ////////////////////////////////////// // ***** Initialization ******// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } protected NotifiableEnergyContainer createEnergyContainer(Object... args) { long tierVoltage = GTValues.V[tier]; diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/WorkableTieredMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/WorkableTieredMachine.java index df32c32c7bd..e202bea06fe 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/WorkableTieredMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/WorkableTieredMachine.java @@ -5,12 +5,10 @@ import com.gregtechceu.gtceu.api.machine.feature.*; import com.gregtechceu.gtceu.api.machine.trait.*; import com.gregtechceu.gtceu.api.recipe.GTRecipeType; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.gregtechceu.gtceu.utils.GTUtil; - -import com.lowdragmc.lowdraglib.syncdata.ISubscription; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.gregtechceu.gtceu.utils.ISubscription; import com.mojang.blaze3d.MethodsReturnNonnullByDefault; import it.unimi.dsi.fastutil.ints.Int2IntFunction; @@ -30,18 +28,15 @@ public abstract class WorkableTieredMachine extends TieredEnergyMachine implements IRecipeLogicMachine, IMachineLife, IMufflableMachine, IOverclockMachine { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(WorkableTieredMachine.class, - TieredEnergyMachine.MANAGED_FIELD_HOLDER); - @Getter - @Persisted - @DescSynced + @SaveField + @SyncToClient public final RecipeLogic recipeLogic; @Getter public final GTRecipeType[] recipeTypes; @Getter @Setter - @Persisted + @SaveField public int activeRecipeType; @Getter public final Int2IntFunction tankScalingFunction; @@ -49,30 +44,29 @@ public abstract class WorkableTieredMachine extends TieredEnergyMachine implemen @Getter @Setter private ICleanroomProvider cleanroom; - @Persisted + @SaveField public final NotifiableItemStackHandler importItems; - @Persisted + @SaveField public final NotifiableItemStackHandler exportItems; - @Persisted + @SaveField public final NotifiableFluidTank importFluids; - @Persisted + @SaveField public final NotifiableFluidTank exportFluids; - @Persisted + @SaveField public final NotifiableComputationContainer importComputation; - @Persisted + @SaveField public final NotifiableComputationContainer exportComputation; @Getter protected final Map> capabilitiesProxy; @Getter protected final Map, List>>> capabilitiesFlat; - @Persisted + @SaveField @Getter protected int overclockTier; protected final List traitSubscriptions; - @Persisted - @DescSynced + @SaveField + @SyncToClient @Getter - @Setter protected boolean isMuffled; protected boolean previouslyMuffled = true; @@ -98,10 +92,6 @@ public WorkableTieredMachine(IMachineBlockEntity holder, int tier, Int2IntFuncti ////////////////////////////////////// // ***** Initialization ******// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } @Override protected NotifiableEnergyContainer createEnergyContainer(Object... args) { @@ -116,7 +106,8 @@ protected NotifiableEnergyContainer createEnergyContainer(Object... args) { } protected NotifiableItemStackHandler createImportItemHandler(Object... args) { - return new NotifiableItemStackHandler(this, getRecipeType().getMaxInputs(ItemRecipeCapability.CAP), IO.IN); + return new NotifiableItemStackHandler(this, getRecipeType().getMaxInputs(ItemRecipeCapability.CAP), IO.IN, + IO.BOTH); } protected NotifiableItemStackHandler createExportItemHandler(Object... args) { @@ -125,7 +116,7 @@ protected NotifiableItemStackHandler createExportItemHandler(Object... args) { protected NotifiableFluidTank createImportFluidHandler(Object... args) { return new NotifiableFluidTank(this, getRecipeType().getMaxInputs(FluidRecipeCapability.CAP), - this.tankScalingFunction.applyAsInt(this.getTier()), IO.IN); + this.tankScalingFunction.applyAsInt(this.getTier()), IO.IN, IO.BOTH); } protected NotifiableFluidTank createExportFluidHandler(Object... args) { @@ -188,6 +179,11 @@ public void onMachineRemoved() { clearInventory(exportItems.storage); } + public void setMuffled(boolean muffled) { + isMuffled = muffled; + syncDataHolder.markClientSyncFieldDirty("isMuffled"); + } + ////////////////////////////////////// // ******** OVERCLOCK *********// ////////////////////////////////////// diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IDropSaveMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IDropSaveMachine.java index bf4eaabb802..69169a80ab5 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IDropSaveMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IDropSaveMachine.java @@ -2,6 +2,8 @@ import net.minecraft.nbt.CompoundTag; +import javax.annotation.OverridingMethodsMustInvokeSuper; + /** * A machine that can save its contents when dropped. */ @@ -26,14 +28,12 @@ default boolean savePickClone() { * * @param tag The tag to save to. */ - default void saveToItem(CompoundTag tag) { - self().holder.saveManagedPersistentData(tag, true); - } + @OverridingMethodsMustInvokeSuper + void saveToItem(CompoundTag tag); /** * Loads the contents of the block entity from a compound tag. */ - default void loadFromItem(CompoundTag tag) { - self().holder.loadManagedPersistentData(tag); - } + @OverridingMethodsMustInvokeSuper + void loadFromItem(CompoundTag tag); } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IMuiMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IMuiMachine.java new file mode 100644 index 00000000000..e7ec6992d17 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IMuiMachine.java @@ -0,0 +1,35 @@ +package com.gregtechceu.gtceu.api.machine.feature; + +import com.gregtechceu.gtceu.api.mui.base.IUIHolder; +import com.gregtechceu.gtceu.api.mui.factory.PosGuiData; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.UISettings; +import com.gregtechceu.gtceu.common.mui.factory.MachineUIFactory; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.BlockHitResult; + +public interface IMuiMachine extends IUIHolder, IMachineFeature { + + @Override + ModularPanel buildUI(PosGuiData data, PanelSyncManager syncManager, UISettings settings); + + default boolean shouldOpenUI(Player player, InteractionHand hand, BlockHitResult hit) { + return true; + } + + default InteractionResult tryToOpenUI(Player player, InteractionHand hand, BlockHitResult hit) { + if (this.shouldOpenUI(player, hand, hit)) { + if (player instanceof ServerPlayer serverPlayer) { + MachineUIFactory.INSTANCE.open(serverPlayer, this); + } + return InteractionResult.sidedSuccess(player.level().isClientSide); + } else { + return InteractionResult.PASS; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IUIMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IUIMachine.java index 55db2a80cf5..5b504ccfa09 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IUIMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/IUIMachine.java @@ -43,7 +43,5 @@ default boolean isRemote() { } @Override - default void markAsDirty() { - self().markDirty(); - } + default void markAsDirty() {}; } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IDistinctPart.java b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IDistinctPart.java index 1c1947d7ed3..7922c636d3d 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IDistinctPart.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IDistinctPart.java @@ -1,36 +1,28 @@ package com.gregtechceu.gtceu.api.machine.feature.multiblock; -import com.gregtechceu.gtceu.api.gui.GuiTextures; -import com.gregtechceu.gtceu.api.gui.fancy.ConfiguratorPanel; -import com.gregtechceu.gtceu.api.gui.fancy.IFancyConfiguratorButton; - -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.Style; - -import java.util.List; - public interface IDistinctPart extends IMultiPart { boolean isDistinct(); void setDistinct(boolean isDistinct); - @Override - default void attachConfigurators(ConfiguratorPanel configuratorPanel) { - superAttachConfigurators(configuratorPanel); - 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"))))); - } - - default void superAttachConfigurators(ConfiguratorPanel configuratorPanel) { - IMultiPart.super.attachConfigurators(configuratorPanel); - } + /* + * @Override + * default void attachConfigurators(ConfiguratorPanel configuratorPanel) { + * superAttachConfigurators(configuratorPanel); + * 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"))))); + * } + * + * default void superAttachConfigurators(ConfiguratorPanel configuratorPanel) { + * IMultiPart.super.attachConfigurators(configuratorPanel); + * } + */ } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMaintenanceMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMaintenanceMachine.java index c97053ba47c..81fb6fbd40b 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMaintenanceMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMaintenanceMachine.java @@ -1,20 +1,12 @@ package com.gregtechceu.gtceu.api.machine.feature.multiblock; import com.gregtechceu.gtceu.api.GTValues; -import com.gregtechceu.gtceu.api.gui.GuiTextures; -import com.gregtechceu.gtceu.api.gui.fancy.IFancyTooltip; -import com.gregtechceu.gtceu.api.gui.fancy.TooltipsPanel; import com.gregtechceu.gtceu.api.machine.property.GTMachineModelProperties; import com.gregtechceu.gtceu.api.recipe.GTRecipe; import com.gregtechceu.gtceu.config.ConfigHolder; -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.Style; import net.minecraft.world.level.block.state.properties.BooleanProperty; -import java.util.ArrayList; - public interface IMaintenanceMachine extends IMultiPart { BooleanProperty MAINTENANCE_TAPED_PROPERTY = GTMachineModelProperties.IS_TAPED; @@ -152,39 +144,43 @@ default GTRecipe modifyRecipe(GTRecipe recipe) { // ******* FANCY GUI ********// ////////////////////////////////////// - @Override - default void attachFancyTooltipsToController(IMultiController controller, TooltipsPanel tooltipsPanel) { - attachTooltips(tooltipsPanel); - } - - @Override - default void attachTooltips(TooltipsPanel tooltipsPanel) { - if (ConfigHolder.INSTANCE.machines.enableMaintenance) { - tooltipsPanel.attachTooltips(new IFancyTooltip.Basic(() -> GuiTextures.MAINTENANCE_ICON, () -> { - var tooltips = new ArrayList(); - tooltips.add(Component.translatable("gtceu.multiblock.universal.has_problems_header") - .setStyle(Style.EMPTY.withColor(ChatFormatting.RED))); - - if ((getMaintenanceProblems() & 1) == 0) - tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.wrench")); - - if (((getMaintenanceProblems() >> 1) & 1) == 0) - tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.screwdriver")); - - if (((getMaintenanceProblems() >> 2) & 1) == 0) - tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.soft_mallet")); - - if (((getMaintenanceProblems() >> 3) & 1) == 0) - tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.hard_hammer")); - - if (((getMaintenanceProblems() >> 4) & 1) == 0) - tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.wire_cutter")); - - if (((getMaintenanceProblems() >> 5) & 1) == 0) - tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.crowbar")); + /* + * @Override + * default void attachFancyTooltipsToController(IMultiController controller, TooltipsPanel tooltipsPanel) { + * attachTooltips(tooltipsPanel); + * } + */ - return tooltips; - }, this::hasMaintenanceProblems, () -> null)); - } - } + /* + * @Override + * default void attachTooltips(TooltipsPanel tooltipsPanel) { + * if (ConfigHolder.INSTANCE.machines.enableMaintenance) { + * tooltipsPanel.attachTooltips(new IFancyTooltip.Basic(() -> GuiTextures.MAINTENANCE_ICON, () -> { + * var tooltips = new ArrayList(); + * tooltips.add(Component.translatable("gtceu.multiblock.universal.has_problems_header") + * .setStyle(Style.EMPTY.withColor(ChatFormatting.RED))); + * + * if ((getMaintenanceProblems() & 1) == 0) + * tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.wrench")); + * + * if (((getMaintenanceProblems() >> 1) & 1) == 0) + * tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.screwdriver")); + * + * if (((getMaintenanceProblems() >> 2) & 1) == 0) + * tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.soft_mallet")); + * + * if (((getMaintenanceProblems() >> 3) & 1) == 0) + * tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.hard_hammer")); + * + * if (((getMaintenanceProblems() >> 4) & 1) == 0) + * tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.wire_cutter")); + * + * if (((getMaintenanceProblems() >> 5) & 1) == 0) + * tooltips.add(Component.translatable("gtceu.multiblock.universal.problem.crowbar")); + * + * return tooltips; + * }, this::hasMaintenanceProblems, () -> null)); + * } + * } + */ } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMufflerMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMufflerMachine.java index 90a20071f83..dd0990b8b9c 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMufflerMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMufflerMachine.java @@ -3,21 +3,13 @@ import com.gregtechceu.gtceu.api.GTValues; import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper; import com.gregtechceu.gtceu.api.capability.IHazardParticleContainer; -import com.gregtechceu.gtceu.api.gui.GuiTextures; -import com.gregtechceu.gtceu.api.gui.fancy.IFancyTooltip; -import com.gregtechceu.gtceu.api.gui.fancy.TooltipsPanel; import com.gregtechceu.gtceu.api.machine.feature.IEnvironmentalHazardEmitter; import com.gregtechceu.gtceu.api.machine.multiblock.part.TieredPartMachine; import com.gregtechceu.gtceu.api.recipe.GTRecipe; import com.gregtechceu.gtceu.common.data.GTParticleTypes; -import net.minecraft.ChatFormatting; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.Style; import net.minecraft.world.item.ItemStack; -import java.util.List; - public interface IMufflerMachine extends IMultiPart, IEnvironmentalHazardEmitter { void recoverItemsTable(ItemStack... recoveryItems); @@ -85,18 +77,20 @@ default boolean afterWorking(IWorkableMultiController controller) { // ******* FANCY GUI ********// ////////////////////////////////////// - @Override - default void attachFancyTooltipsToController(IMultiController controller, TooltipsPanel tooltipsPanel) { - attachTooltips(tooltipsPanel); - } - - @Override - default void attachTooltips(TooltipsPanel tooltipsPanel) { - tooltipsPanel.attachTooltips(new IFancyTooltip.Basic( - () -> GuiTextures.INDICATOR_NO_STEAM.get(false), - () -> List.of(Component.translatable("gtceu.multiblock.universal.muffler_obstructed") - .setStyle(Style.EMPTY.withColor(ChatFormatting.RED))), - () -> !isFrontFaceFree(), - () -> null)); - } + /* + * @Override + * default void attachFancyTooltipsToController(IMultiController controller, TooltipsPanel tooltipsPanel) { + * attachTooltips(tooltipsPanel); + * } + * + * @Override + * default void attachTooltips(TooltipsPanel tooltipsPanel) { + * tooltipsPanel.attachTooltips(new IFancyTooltip.Basic( + * () -> GuiTextures.INDICATOR_NO_STEAM.get(false), + * () -> List.of(Component.translatable("gtceu.multiblock.universal.muffler_obstructed") + * .setStyle(Style.EMPTY.withColor(ChatFormatting.RED))), + * () -> !isFrontFaceFree(), + * () -> null)); + * } + */ } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMultiPart.java b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMultiPart.java index 0ab6861e6b4..7d50cf249e1 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMultiPart.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IMultiPart.java @@ -1,8 +1,8 @@ package com.gregtechceu.gtceu.api.machine.feature.multiblock; import com.gregtechceu.gtceu.api.gui.fancy.TooltipsPanel; -import com.gregtechceu.gtceu.api.machine.feature.IFancyUIMachine; import com.gregtechceu.gtceu.api.machine.feature.IMachineFeature; +import com.gregtechceu.gtceu.api.machine.feature.IMuiMachine; import com.gregtechceu.gtceu.api.machine.multiblock.WorkableMultiblockMachine; import com.gregtechceu.gtceu.api.machine.trait.RecipeHandlerList; import com.gregtechceu.gtceu.api.machine.trait.RecipeLogic; @@ -19,7 +19,7 @@ import java.util.List; import java.util.SortedSet; -public interface IMultiPart extends IMachineFeature, IFancyUIMachine { +public interface IMultiPart extends IMachineFeature, IMuiMachine { /** * Can it be shared among multi multiblock. diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IRotorHolderMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IRotorHolderMachine.java index 3f46cf2765b..f68f4fb4d8b 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IRotorHolderMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/feature/multiblock/IRotorHolderMachine.java @@ -1,25 +1,17 @@ package com.gregtechceu.gtceu.api.machine.feature.multiblock; import com.gregtechceu.gtceu.api.data.chemical.material.Material; -import com.gregtechceu.gtceu.api.gui.GuiTextures; -import com.gregtechceu.gtceu.api.gui.fancy.IFancyTooltip; -import com.gregtechceu.gtceu.api.gui.fancy.TooltipsPanel; import com.gregtechceu.gtceu.api.machine.property.GTMachineModelProperties; import com.gregtechceu.gtceu.api.pattern.util.RelativeDirection; import com.gregtechceu.gtceu.api.recipe.GTRecipe; import com.gregtechceu.gtceu.common.item.TurbineRotorBehaviour; -import net.minecraft.ChatFormatting; import net.minecraft.core.Direction; -import net.minecraft.network.chat.Component; -import net.minecraft.network.chat.Style; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.state.properties.BooleanProperty; import org.jetbrains.annotations.NotNull; -import java.util.List; - public interface IRotorHolderMachine extends IMultiPart { int SPEED_INCREMENT = 1; @@ -205,18 +197,20 @@ default GTRecipe modifyRecipe(GTRecipe recipe) { // ******* FANCY GUI ********// ////////////////////////////////////// - @Override - default void attachFancyTooltipsToController(IMultiController controller, TooltipsPanel tooltipsPanel) { - attachTooltips(tooltipsPanel); - } - - @Override - default void attachTooltips(TooltipsPanel tooltipsPanel) { - tooltipsPanel.attachTooltips(new IFancyTooltip.Basic( - () -> GuiTextures.INDICATOR_NO_STEAM.get(false), - () -> List.of(Component.translatable("gtceu.multiblock.universal.rotor_obstructed") - .setStyle(Style.EMPTY.withColor(ChatFormatting.RED))), - () -> !isFrontFaceFree(), - () -> null)); - } + /* + * @Override + * default void attachFancyTooltipsToController(IMultiController controller, TooltipsPanel tooltipsPanel) { + * attachTooltips(tooltipsPanel); + * } + * + * @Override + * default void attachTooltips(TooltipsPanel tooltipsPanel) { + * tooltipsPanel.attachTooltips(new IFancyTooltip.Basic( + * () -> GuiTextures.INDICATOR_NO_STEAM.get(false), + * () -> List.of(Component.translatable("gtceu.multiblock.universal.rotor_obstructed") + * .setStyle(Style.EMPTY.withColor(ChatFormatting.RED))), + * () -> !isFrontFaceFree(), + * () -> null)); + * } + */ } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/MultiblockControllerMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/MultiblockControllerMachine.java index cc5535e2fbb..9e70e772dea 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/MultiblockControllerMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/MultiblockControllerMachine.java @@ -13,12 +13,10 @@ import com.gregtechceu.gtceu.api.pattern.MultiblockState; import com.gregtechceu.gtceu.api.pattern.MultiblockWorldSavedData; import com.gregtechceu.gtceu.client.model.machine.MachineRenderState; - -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.annotation.RequireRerender; -import com.lowdragmc.lowdraglib.syncdata.annotation.UpdateListener; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.ClientFieldChangeListener; +import com.gregtechceu.gtceu.syncsystem.annotations.RerenderOnChanged; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.BlockPos; @@ -31,7 +29,6 @@ import net.minecraft.world.phys.BlockHitResult; import lombok.Getter; -import lombok.Setter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -49,24 +46,20 @@ @MethodsReturnNonnullByDefault public class MultiblockControllerMachine extends MetaMachine implements IMultiController { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( - MultiblockControllerMachine.class, MetaMachine.MANAGED_FIELD_HOLDER); private MultiblockState multiblockState; private final List parts = new ArrayList<>(); private @Nullable IParallelHatch parallelHatch = null; @Getter - @DescSynced - @UpdateListener(methodName = "onPartsUpdated") + @SyncToClient private BlockPos[] partPositions = new BlockPos[0]; @Getter - @Persisted - @DescSynced - @RequireRerender + @SaveField + @SyncToClient + @RerenderOnChanged protected boolean isFormed; @Getter - @Setter - @Persisted - @DescSynced + @SaveField + @SyncToClient protected boolean isFlipped; public MultiblockControllerMachine(IMachineBlockEntity holder) { @@ -76,10 +69,6 @@ public MultiblockControllerMachine(IMachineBlockEntity holder) { ////////////////////////////////////// // ***** Initialization ******// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } @Override public MultiblockMachineDefinition getDefinition() { @@ -111,10 +100,16 @@ public MultiblockState getMultiblockState() { return multiblockState; } + public void setFlipped(boolean flipped) { + isFlipped = flipped; + syncDataHolder.markClientSyncFieldDirty("isFlipped"); + } + @SuppressWarnings("unused") - protected void onPartsUpdated(BlockPos[] newValue, BlockPos[] oldValue) { + @ClientFieldChangeListener(fieldName = "partPositions") + protected void onPartsUpdated() { parts.clear(); - for (var pos : newValue) { + for (var pos : partPositions) { if (getMachine(getLevel(), pos) instanceof IMultiPart part) { parts.add(part); } @@ -124,6 +119,7 @@ protected void onPartsUpdated(BlockPos[] newValue, BlockPos[] oldValue) { protected void updatePartPositions() { this.partPositions = this.parts.isEmpty() ? new BlockPos[0] : this.parts.stream().map(part -> part.self().getPos()).toArray(BlockPos[]::new); + syncDataHolder.markClientSyncFieldDirty("partPositions"); } @Override @@ -174,6 +170,7 @@ public void asyncCheckPattern(long periodID) { @Override public void onStructureFormed() { isFormed = true; + syncDataHolder.markClientSyncFieldDirty("isFormed"); MachineRenderState renderState = getRenderState(); if (renderState.hasProperty(GTMachineModelProperties.IS_FORMED)) { setRenderState(renderState.setValue(GTMachineModelProperties.IS_FORMED, true)); @@ -259,7 +256,6 @@ public void setUpwardsFacing(@NotNull Direction upwardsFacing) { blockState.setValue(GTBlockStateProperties.UPWARDS_FACING, upwardsFacing)); if (getLevel() != null && !getLevel().isClientSide) { notifyBlockUpdate(); - markDirty(); checkPattern(); } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/TieredWorkableElectricMultiblockMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/TieredWorkableElectricMultiblockMachine.java index b3fa9310bee..4954cbb2915 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/TieredWorkableElectricMultiblockMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/TieredWorkableElectricMultiblockMachine.java @@ -4,9 +4,7 @@ import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; import com.gregtechceu.gtceu.api.machine.feature.IOverclockMachine; import com.gregtechceu.gtceu.api.machine.feature.ITieredMachine; - -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; import net.minecraft.MethodsReturnNonnullByDefault; @@ -19,11 +17,8 @@ public class TieredWorkableElectricMultiblockMachine extends WorkableElectricMultiblockMachine implements ITieredMachine, IOverclockMachine { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( - TieredWorkableElectricMultiblockMachine.class, WorkableElectricMultiblockMachine.MANAGED_FIELD_HOLDER); - private final int tier; - @Persisted + @SaveField @Getter protected int overclockTier; @@ -35,10 +30,6 @@ public TieredWorkableElectricMultiblockMachine(IMachineBlockEntity holder, int t ////////////////////////////////////// // ***** Initialization ******// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } ////////////////////////////////////// // ******** OVERCLOCK *********// diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/WorkableElectricMultiblockMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/WorkableElectricMultiblockMachine.java index 3ad662bb8cf..3585044097d 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/WorkableElectricMultiblockMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/WorkableElectricMultiblockMachine.java @@ -18,12 +18,11 @@ import com.gregtechceu.gtceu.api.misc.EnergyContainerList; import com.gregtechceu.gtceu.api.recipe.modifier.RecipeModifierList; import com.gregtechceu.gtceu.common.data.GTRecipeModifiers; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; import com.gregtechceu.gtceu.utils.GTUtil; import com.lowdragmc.lowdraglib.gui.modular.ModularUI; import com.lowdragmc.lowdraglib.gui.widget.*; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.network.chat.Component; @@ -43,13 +42,11 @@ public class WorkableElectricMultiblockMachine extends WorkableMultiblockMachine implements IFancyUIMachine, IDisplayUIMachine, ITieredMachine, IOverclockMachine { - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( - WorkableElectricMultiblockMachine.class, WorkableMultiblockMachine.MANAGED_FIELD_HOLDER); // runtime protected EnergyContainerList energyContainer; @Getter protected int tier; - @Persisted + @SaveField @Getter protected boolean batchEnabled; @@ -57,11 +54,6 @@ public WorkableElectricMultiblockMachine(IMachineBlockEntity holder, Object... a super(holder, args); } - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - ////////////////////////////////////// // *** Multiblock Lifecycle ***// ////////////////////////////////////// diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/WorkableMultiblockMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/WorkableMultiblockMachine.java index 5d15879b1b0..d1065d67482 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/WorkableMultiblockMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/WorkableMultiblockMachine.java @@ -17,11 +17,9 @@ import com.gregtechceu.gtceu.api.recipe.GTRecipe; import com.gregtechceu.gtceu.api.recipe.GTRecipeType; import com.gregtechceu.gtceu.client.model.machine.MachineRenderState; - -import com.lowdragmc.lowdraglib.syncdata.ISubscription; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; +import com.gregtechceu.gtceu.utils.ISubscription; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.BlockPos; @@ -47,21 +45,19 @@ public abstract class WorkableMultiblockMachine extends MultiblockControllerMachine implements IWorkableMultiController, IMufflableMachine { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( - WorkableMultiblockMachine.class, MultiblockControllerMachine.MANAGED_FIELD_HOLDER); @Nullable @Getter @Setter private ICleanroomProvider cleanroom; @Getter - @Persisted - @DescSynced + @SaveField + @SyncToClient public final RecipeLogic recipeLogic; @Getter private final GTRecipeType[] recipeTypes; @Getter @Setter - @Persisted + @SaveField private int activeRecipeType; @Getter protected final Map> capabilitiesProxy; @@ -69,9 +65,8 @@ public abstract class WorkableMultiblockMachine extends MultiblockControllerMach protected final Map, List>>> capabilitiesFlat; protected final List traitSubscriptions; @Getter - @Setter - @Persisted - @DescSynced + @SaveField + @SyncToClient protected boolean isMuffled; protected boolean previouslyMuffled = true; @Nullable @@ -79,8 +74,8 @@ public abstract class WorkableMultiblockMachine extends MultiblockControllerMach protected LongSet activeBlocks; @Getter - @Persisted - @DescSynced + @SaveField + @SyncToClient protected VoidingMode voidingMode = VoidingMode.VOID_NONE; public WorkableMultiblockMachine(IMachineBlockEntity holder, Object... args) { @@ -93,15 +88,15 @@ public WorkableMultiblockMachine(IMachineBlockEntity holder, Object... args) { this.traitSubscriptions = new ArrayList<>(); } + public void setMuffled(boolean muffled) { + isMuffled = muffled; + syncDataHolder.markClientSyncFieldDirty("isMuffled"); + } + ////////////////////////////////////// // ***** Initialization ******// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - @Override public void onUnload() { super.onUnload(); diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/part/MultiblockPartMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/part/MultiblockPartMachine.java index 4d0f47585c1..afab86f7914 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/part/MultiblockPartMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/part/MultiblockPartMachine.java @@ -9,11 +9,14 @@ import com.gregtechceu.gtceu.api.machine.property.GTMachineModelProperties; import com.gregtechceu.gtceu.api.machine.trait.IRecipeHandlerTrait; import com.gregtechceu.gtceu.api.machine.trait.RecipeHandlerList; +import com.gregtechceu.gtceu.api.mui.factory.PosGuiData; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; import com.gregtechceu.gtceu.client.model.machine.MachineRenderState; - -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.UpdateListener; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.UISettings; +import com.gregtechceu.gtceu.common.mui.GTGuis; +import com.gregtechceu.gtceu.syncsystem.annotations.ClientFieldChangeListener; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.BlockPos; @@ -39,11 +42,7 @@ @MethodsReturnNonnullByDefault public class MultiblockPartMachine extends MetaMachine implements IMultiPart { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(MultiblockPartMachine.class, - MetaMachine.MANAGED_FIELD_HOLDER); - - @DescSynced - @UpdateListener(methodName = "onControllersUpdated") + @SyncToClient protected final Set controllerPositions = new ObjectOpenHashSet<>(8); protected final SortedSet controllers = new ReferenceLinkedOpenHashSet<>(8); @@ -57,11 +56,6 @@ public MultiblockPartMachine(IMachineBlockEntity holder) { // ***** Initialization ******// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - @Override public boolean hasController(BlockPos controllerPos) { return controllerPositions.contains(controllerPos); @@ -74,9 +68,10 @@ public boolean isFormed() { // Not sure if necessary, but added to match the Controller class @SuppressWarnings("unused") - public void onControllersUpdated(Set newPositions, Set old) { + @ClientFieldChangeListener(fieldName = "controllerPositions") + public void onControllersUpdated() { controllers.clear(); - for (BlockPos blockPos : newPositions) { + for (BlockPos blockPos : controllerPositions) { if (MetaMachine.getMachine(getLevel(), blockPos) instanceof IMultiController controller) { controllers.add(controller); } @@ -88,7 +83,7 @@ public void onControllersUpdated(Set newPositions, Set old) public SortedSet getControllers() { // Necessary to rebuild the set of controllers on client-side if (controllers.size() != controllerPositions.size()) { - onControllersUpdated(controllerPositions, Collections.emptySet()); + onControllersUpdated(); } return Collections.unmodifiableSortedSet(controllers); } @@ -150,6 +145,7 @@ public void removedFromController(IMultiController controller) { setRenderState(renderState.setValue(GTMachineModelProperties.IS_FORMED, false)); } } + syncDataHolder.markClientSyncFieldDirty("controllerPositions"); } @MustBeInvokedByOverriders @@ -158,6 +154,7 @@ public void addedToController(IMultiController controller) { controllerPositions.add(controller.self().getPos()); controllers.add(controller); + syncDataHolder.markClientSyncFieldDirty("controllerPositions"); MachineRenderState renderState = getRenderState(); if (renderState.hasProperty(GTMachineModelProperties.IS_FORMED)) { setRenderState(renderState.setValue(GTMachineModelProperties.IS_FORMED, true)); @@ -177,4 +174,9 @@ public BlockState getFormedAppearance(BlockState sourceState, BlockPos sourcePos if (!replacePartModelWhenFormed()) return null; return IMultiPart.super.getFormedAppearance(sourceState, sourcePos, side); } + + @Override + public ModularPanel buildUI(PosGuiData data, PanelSyncManager syncManager, UISettings settings) { + return GTGuis.createPanel(this, 176, 166); + } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/part/TieredIOPartMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/part/TieredIOPartMachine.java index d0bc262c0f7..89accde7c8e 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/part/TieredIOPartMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/multiblock/part/TieredIOPartMachine.java @@ -3,17 +3,13 @@ import com.gregtechceu.gtceu.api.capability.IControllable; import com.gregtechceu.gtceu.api.capability.recipe.IO; import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; - -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.annotation.RequireRerender; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.RerenderOnChanged; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import net.minecraft.MethodsReturnNonnullByDefault; import lombok.Getter; -import lombok.Setter; -import org.jetbrains.annotations.Nullable; import javax.annotation.ParametersAreNonnullByDefault; @@ -21,19 +17,15 @@ @MethodsReturnNonnullByDefault public class TieredIOPartMachine extends TieredPartMachine implements IControllable { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(TieredIOPartMachine.class, - MultiblockPartMachine.MANAGED_FIELD_HOLDER); - protected final IO io; /** * AUTO IO working? */ @Getter - @Setter - @Persisted - @DescSynced - @RequireRerender + @SaveField + @SyncToClient + @RerenderOnChanged protected boolean workingEnabled; public TieredIOPartMachine(IMachineBlockEntity holder, int tier, IO io) { @@ -42,23 +34,27 @@ public TieredIOPartMachine(IMachineBlockEntity holder, int tier, IO io) { this.workingEnabled = true; } + @Override + public void setWorkingEnabled(boolean workingEnabled) { + this.workingEnabled = workingEnabled; + syncDataHolder.markClientSyncFieldDirty("workingEnabled"); + } + ////////////////////////////////////// // ***** Initialization ******// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - - @Nullable - @Override - public PageGroupingData getPageGroupingData() { - return switch (this.io) { - case IN -> new PageGroupingData("gtceu.multiblock.page_switcher.io.import", 1); - case OUT -> new PageGroupingData("gtceu.multiblock.page_switcher.io.export", 2); - case BOTH -> new PageGroupingData("gtceu.multiblock.page_switcher.io.both", 3); - case NONE -> null; - }; - } + /* + * @Nullable + * + * @Override + * public PageGroupingData getPageGroupingData() { + * return switch (this.io) { + * case IN -> new PageGroupingData("gtceu.multiblock.page_switcher.io.import", 1); + * case OUT -> new PageGroupingData("gtceu.multiblock.page_switcher.io.export", 2); + * case BOTH -> new PageGroupingData("gtceu.multiblock.page_switcher.io.both", 3); + * case NONE -> null; + * }; + * } + */ } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SimpleSteamMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SimpleSteamMachine.java index e34ee2d0277..634ee83fded 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SimpleSteamMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SimpleSteamMachine.java @@ -22,11 +22,10 @@ import com.gregtechceu.gtceu.api.recipe.modifier.RecipeModifier; import com.gregtechceu.gtceu.client.model.machine.MachineRenderState; import com.gregtechceu.gtceu.common.recipe.condition.VentCondition; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; import com.lowdragmc.lowdraglib.gui.modular.ModularUI; import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import com.lowdragmc.lowdraglib.utils.Position; import net.minecraft.MethodsReturnNonnullByDefault; @@ -48,16 +47,13 @@ @MethodsReturnNonnullByDefault public class SimpleSteamMachine extends SteamWorkableMachine implements IExhaustVentMachine, IUIMachine { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(SimpleSteamMachine.class, - SteamWorkableMachine.MANAGED_FIELD_HOLDER); - - @Persisted + @SaveField public final NotifiableItemStackHandler importItems; - @Persisted + @SaveField public final NotifiableItemStackHandler exportItems; @Getter @Setter - @Persisted + @SaveField private boolean needsVenting; public SimpleSteamMachine(IMachineBlockEntity holder, boolean isHighPressure, Object... args) { @@ -76,11 +72,6 @@ public SimpleSteamMachine(IMachineBlockEntity holder, boolean isHighPressure, Ob // ***** Initialization *****// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - @Override protected NotifiableFluidTank createSteamTank(Object... args) { return new NotifiableFluidTank(this, 1, 16 * FluidType.BUCKET_VOLUME, IO.IN); diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamBoilerMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamBoilerMachine.java index 36357ef0e81..c5743a2c0b7 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamBoilerMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamBoilerMachine.java @@ -2,38 +2,38 @@ import com.gregtechceu.gtceu.api.GTValues; import com.gregtechceu.gtceu.api.capability.recipe.IO; -import com.gregtechceu.gtceu.api.gui.GuiTextures; -import com.gregtechceu.gtceu.api.gui.UITemplate; -import com.gregtechceu.gtceu.api.gui.widget.TankWidget; import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity; import com.gregtechceu.gtceu.api.machine.MetaMachine; import com.gregtechceu.gtceu.api.machine.TickableSubscription; -import com.gregtechceu.gtceu.api.machine.feature.IDataInfoProvider; -import com.gregtechceu.gtceu.api.machine.feature.IExplosionMachine; -import com.gregtechceu.gtceu.api.machine.feature.IInteractedMachine; -import com.gregtechceu.gtceu.api.machine.feature.IUIMachine; +import com.gregtechceu.gtceu.api.machine.feature.*; import com.gregtechceu.gtceu.api.machine.trait.NotifiableFluidTank; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.drawable.UITexture; +import com.gregtechceu.gtceu.api.mui.factory.PosGuiData; +import com.gregtechceu.gtceu.api.mui.value.sync.FluidSlotSyncHandler; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.api.mui.widgets.ProgressWidget; +import com.gregtechceu.gtceu.api.mui.widgets.SlotGroupWidget; +import com.gregtechceu.gtceu.api.mui.widgets.layout.Row; +import com.gregtechceu.gtceu.api.mui.widgets.slot.FluidSlot; import com.gregtechceu.gtceu.api.recipe.GTRecipe; import com.gregtechceu.gtceu.api.recipe.modifier.ModifierFunction; import com.gregtechceu.gtceu.api.recipe.modifier.RecipeModifier; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.UISettings; import com.gregtechceu.gtceu.common.data.GTMaterials; +import com.gregtechceu.gtceu.common.data.mui.GTMuiWidgets; import com.gregtechceu.gtceu.common.item.PortableScannerBehavior; +import com.gregtechceu.gtceu.common.mui.GTGuiTextures; +import com.gregtechceu.gtceu.common.mui.GTGuis; import com.gregtechceu.gtceu.config.ConfigHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.gregtechceu.gtceu.utils.FormattingUtil; import com.gregtechceu.gtceu.utils.GTTransferUtils; - -import com.lowdragmc.lowdraglib.gui.modular.ModularUI; -import com.lowdragmc.lowdraglib.gui.texture.ProgressTexture; -import com.lowdragmc.lowdraglib.gui.widget.ImageWidget; -import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; -import com.lowdragmc.lowdraglib.gui.widget.ProgressWidget; -import com.lowdragmc.lowdraglib.syncdata.ISubscription; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.gregtechceu.gtceu.utils.ISubscription; import net.minecraft.MethodsReturnNonnullByDefault; -import net.minecraft.client.resources.language.I18n; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.particles.ParticleTypes; @@ -67,19 +67,16 @@ @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault public abstract class SteamBoilerMachine extends SteamWorkableMachine - implements IUIMachine, IExplosionMachine, IDataInfoProvider, + implements IMuiMachine, IExplosionMachine, IDataInfoProvider, IInteractedMachine { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(SteamBoilerMachine.class, - SteamWorkableMachine.MANAGED_FIELD_HOLDER); - - @Persisted + @SaveField public final NotifiableFluidTank waterTank; - @Persisted - @DescSynced + @SaveField + @SyncToClient @Getter private int currentTemperature; - @Persisted + @SaveField @Getter private int timeBeforeCoolingDown; @Getter @@ -98,10 +95,6 @@ public SteamBoilerMachine(IMachineBlockEntity holder, boolean isHighPressure, Ob ////////////////////////////////////// // ***** Initialization *****// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } @Override protected NotifiableFluidTank createSteamTank(Object... args) { @@ -194,7 +187,9 @@ protected void updateCurrentTemperature() { currentTemperature -= getCoolDownRate(); timeBeforeCoolingDown = getCooldownInterval(); } - } else--timeBeforeCoolingDown; + } else { + --timeBeforeCoolingDown; + } if (getOffsetTimer() % 10 == 0) { if (currentTemperature >= 100) { @@ -208,7 +203,9 @@ protected void updateCurrentTemperature() { } if (this.hasNoWater && hasDrainedWater) { doExplosion(2.0f); - } else this.hasNoWater = !hasDrainedWater; + } else { + this.hasNoWater = !hasDrainedWater; + } if (filledSteam == 0 && hasDrainedWater && getLevel() instanceof ServerLevel serverLevel) { final float x = getPos().getX() + 0.5F; final float y = getPos().getY() + 0.5F; @@ -231,9 +228,12 @@ protected void updateCurrentTemperature() { // bypass capability check for special case behavior steamTank.drainInternal(FluidType.BUCKET_VOLUME * 4, FluidAction.EXECUTE); } - } else this.hasNoWater = false; + } else { + this.hasNoWater = false; + } } updateSteamSubscription(); + syncDataHolder.markClientSyncFieldDirty("currentTemperature"); } protected int getCooldownInterval() { @@ -321,31 +321,69 @@ public InteractionResult onUse(BlockState state, Level world, BlockPos pos, Play ////////////////////////////////////// // ********** GUI ***********// ////////////////////////////////////// - @Override - public ModularUI createUI(Player entityPlayer) { - return new ModularUI(176, 166, this, entityPlayer) - .background(GuiTextures.BACKGROUND_STEAM.get(isHighPressure)) - .widget(new LabelWidget(6, 6, getBlockState().getBlock().getDescriptionId())) - .widget(new ProgressWidget(this::getTemperaturePercent, 96, 26, 10, 54) - .setProgressTexture(GuiTextures.PROGRESS_BAR_BOILER_EMPTY.get(isHighPressure), - GuiTextures.PROGRESS_BAR_BOILER_HEAT) - .setFillDirection(ProgressTexture.FillDirection.DOWN_TO_UP) - .setDynamicHoverTips(pct -> I18n.get("gtceu.multiblock.large_boiler.temperature", - currentTemperature + 274, getMaxTemperature() + 274))) - .widget(new TankWidget(waterTank.getStorages()[0], 83, 26, 10, 54, false, true) - .setShowAmount(false) - .setFillDirection(ProgressTexture.FillDirection.DOWN_TO_UP) - .setBackground(GuiTextures.PROGRESS_BAR_BOILER_EMPTY.get(isHighPressure))) - .widget(new TankWidget(steamTank.getStorages()[0], 70, 26, 10, 54, true, false) - .setShowAmount(false) - .setFillDirection(ProgressTexture.FillDirection.DOWN_TO_UP) - .setBackground(GuiTextures.PROGRESS_BAR_BOILER_EMPTY.get(isHighPressure))) - .widget(new ImageWidget(43, 44, 18, 18, GuiTextures.CANISTER_OVERLAY_STEAM.get(isHighPressure))) - .widget(UITemplate.bindPlayerInventory(entityPlayer.getInventory(), - GuiTextures.SLOT_STEAM.get(isHighPressure), 7, 84, true)); + public ModularPanel buildUI(PosGuiData data, PanelSyncManager syncManager, UISettings settings) { + ModularPanel panel = GTGuis.createPanel(this, 176, 166); + panel.child(GTMuiWidgets.createTitleBar(this.getDefinition(), 176)); + + UITexture progressTexture = isHighPressure() ? GTGuiTextures.PROGRESS_BAR_BOILER_EMPTY_STEEL : + GTGuiTextures.PROGRESS_BAR_BOILER_EMPTY_BRONZE; + + panel.child(new Row() + .top(12) + .left(50) + .coverChildren() + .childPadding(10) + .child(new FluidSlot() + .syncHandler(new FluidSlotSyncHandler(waterTank.getStorages()[0])) + .size(14, 54) + .displayAmount(false)) + .child(new FluidSlot() + .syncHandler(new FluidSlotSyncHandler(steamTank.getStorages()[0]) + .canFillSlot(false).canDrainSlot(true)) + .alwaysShowFull(true) + .size(14, 54) + .displayAmount(false)) + .child(new ProgressWidget() + .texture(progressTexture, + GTGuiTextures.PROGRESS_BAR_BOILER_HEAT, 54) + .size(14, 54) + .progress(this::getTemperaturePercent) + .direction(ProgressWidget.Direction.UP) + .tooltipAutoUpdate(true) + .tooltipBuilder((r) -> r.addLine(IKey + .lang(Component.translatable("gtceu.fluid.temperature", getCurrentTemperature())))))) + .child(SlotGroupWidget.playerInventory(false).bottom(7).left(7)); + + return panel; } + /* + * @Override + * public ModularUI createUI(Player entityPlayer) { + * return new ModularUI(176, 166, this, entityPlayer) + * .background(GuiTextures.BACKGROUND_STEAM.get(isHighPressure)) + * .widget(new LabelWidget(6, 6, getBlockState().getBlock().getDescriptionId())) + * .widget(new ProgressWidget(this::getTemperaturePercent, 96, 26, 10, 54) + * .setProgressTexture(GuiTextures.PROGRESS_BAR_BOILER_EMPTY.get(isHighPressure), + * GuiTextures.PROGRESS_BAR_BOILER_HEAT) + * .setFillDirection(ProgressTexture.FillDirection.DOWN_TO_UP) + * .setDynamicHoverTips(pct -> I18n.get("gtceu.multiblock.large_boiler.temperature", + * currentTemperature + 274, getMaxTemperature() + 274))) + * .widget(new TankWidget(waterTank.getStorages()[0], 83, 26, 10, 54, false, true) + * .setShowAmount(false) + * .setFillDirection(ProgressTexture.FillDirection.DOWN_TO_UP) + * .setBackground(GuiTextures.PROGRESS_BAR_BOILER_EMPTY.get(isHighPressure))) + * .widget(new TankWidget(steamTank.getStorages()[0], 70, 26, 10, 54, true, false) + * .setShowAmount(false) + * .setFillDirection(ProgressTexture.FillDirection.DOWN_TO_UP) + * .setBackground(GuiTextures.PROGRESS_BAR_BOILER_EMPTY.get(isHighPressure))) + * .widget(new ImageWidget(43, 44, 18, 18, GuiTextures.CANISTER_OVERLAY_STEAM.get(isHighPressure))) + * .widget(UITemplate.bindPlayerInventory(entityPlayer.getInventory(), + * GuiTextures.SLOT_STEAM.get(isHighPressure), 7, 84, true)); + * } + */ + ////////////////////////////////////// // ********* Client *********// ////////////////////////////////////// diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamMachine.java index 52e8fc65de9..55644ab2f39 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamMachine.java @@ -6,9 +6,7 @@ import com.gregtechceu.gtceu.api.machine.property.GTMachineModelProperties; import com.gregtechceu.gtceu.api.machine.trait.NotifiableFluidTank; import com.gregtechceu.gtceu.common.data.GTMaterials; - -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.world.level.block.state.properties.BooleanProperty; @@ -21,14 +19,11 @@ @MethodsReturnNonnullByDefault public abstract class SteamMachine extends MetaMachine implements ITieredMachine { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(SteamMachine.class, - MetaMachine.MANAGED_FIELD_HOLDER); - public static final BooleanProperty STEEL_PROPERTY = GTMachineModelProperties.IS_STEEL_MACHINE; @Getter public final boolean isHighPressure; - @Persisted + @SaveField public final NotifiableFluidTank steamTank; public SteamMachine(IMachineBlockEntity holder, boolean isHighPressure, Object... args) { @@ -41,10 +36,6 @@ public SteamMachine(IMachineBlockEntity holder, boolean isHighPressure, Object.. ////////////////////////////////////// // ***** Initialization *****// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } @Override public int getTier() { diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamWorkableMachine.java b/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamWorkableMachine.java index 5721c0a9435..304cd9039f5 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamWorkableMachine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/steam/SteamWorkableMachine.java @@ -15,13 +15,12 @@ import com.gregtechceu.gtceu.api.machine.trait.RecipeHandlerList; import com.gregtechceu.gtceu.api.machine.trait.RecipeLogic; import com.gregtechceu.gtceu.api.recipe.GTRecipeType; +import com.gregtechceu.gtceu.syncsystem.annotations.RerenderOnChanged; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; +import com.gregtechceu.gtceu.utils.ISubscription; import com.lowdragmc.lowdraglib.gui.texture.ResourceTexture; -import com.lowdragmc.lowdraglib.syncdata.ISubscription; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.annotation.RequireRerender; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.BlockPos; @@ -47,27 +46,25 @@ public abstract class SteamWorkableMachine extends SteamMachine implements IRecipeLogicMachine, IMufflableMachine, IMachineLife { - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(SteamWorkableMachine.class, - SteamMachine.MANAGED_FIELD_HOLDER); @Nullable @Getter @Setter private ICleanroomProvider cleanroom; @Getter - @Persisted - @DescSynced + @SaveField + @SyncToClient public final RecipeLogic recipeLogic; @Getter public final GTRecipeType[] recipeTypes; @Getter @Setter public int activeRecipeType; - @Persisted - @DescSynced - @RequireRerender + @SaveField + @SyncToClient + @RerenderOnChanged protected Direction outputFacing; - @Persisted - @DescSynced + @SaveField + @SyncToClient @Getter @Setter protected boolean isMuffled; @@ -92,10 +89,6 @@ public SteamWorkableMachine(IMachineBlockEntity holder, boolean isHighPressure, ////////////////////////////////////// // ***** Initialization *****// ////////////////////////////////////// - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } @Override public void onLoad() { diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/FluidTankProxyTrait.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/FluidTankProxyTrait.java index 19fa260639b..607ae9ca84b 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/FluidTankProxyTrait.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/FluidTankProxyTrait.java @@ -5,8 +5,6 @@ import com.gregtechceu.gtceu.api.transfer.fluid.IFluidHandlerModifiable; import com.gregtechceu.gtceu.utils.GTTransferUtils; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; - import net.minecraft.core.Direction; import net.minecraftforge.fluids.FluidStack; @@ -18,7 +16,6 @@ @Accessors(chain = true) public class FluidTankProxyTrait extends MachineTrait implements IFluidHandlerModifiable, ICapabilityTrait { - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(FluidTankProxyTrait.class); @Getter public final IO capabilityIO; @Setter @@ -30,11 +27,6 @@ public FluidTankProxyTrait(MetaMachine machine, IO capabilityIO) { this.capabilityIO = capabilityIO; } - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - ////////////////////////////////////// // ******* Capability ********// ////////////////////////////////////// diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/IRecipeHandlerTrait.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/IRecipeHandlerTrait.java index 29e39511ad2..9ca83d5bb52 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/IRecipeHandlerTrait.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/IRecipeHandlerTrait.java @@ -2,8 +2,7 @@ import com.gregtechceu.gtceu.api.capability.recipe.IO; import com.gregtechceu.gtceu.api.capability.recipe.IRecipeHandler; - -import com.lowdragmc.lowdraglib.syncdata.ISubscription; +import com.gregtechceu.gtceu.utils.ISubscription; public interface IRecipeHandlerTrait extends IRecipeHandler { diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/ItemHandlerProxyTrait.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/ItemHandlerProxyTrait.java index 169ef746b3d..b2bb0a8e7e0 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/ItemHandlerProxyTrait.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/ItemHandlerProxyTrait.java @@ -4,8 +4,6 @@ import com.gregtechceu.gtceu.api.machine.MetaMachine; import com.gregtechceu.gtceu.utils.GTTransferUtils; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; - import net.minecraft.core.Direction; import net.minecraft.world.item.ItemStack; import net.minecraftforge.items.IItemHandlerModifiable; @@ -19,7 +17,6 @@ @Accessors(chain = true) public class ItemHandlerProxyTrait extends MachineTrait implements IItemHandlerModifiable, ICapabilityTrait { - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(ItemHandlerProxyTrait.class); @Getter public final IO capabilityIO; @Setter @@ -32,11 +29,6 @@ public ItemHandlerProxyTrait(MetaMachine machine, IO capabilityIO) { this.capabilityIO = capabilityIO; } - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - ////////////////////////////////////// // ******* Capability ********// ////////////////////////////////////// diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/MachineTrait.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/MachineTrait.java index 1b9faabef81..65976e2c88f 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/MachineTrait.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/MachineTrait.java @@ -2,17 +2,14 @@ import com.gregtechceu.gtceu.api.machine.MetaMachine; import com.gregtechceu.gtceu.client.model.machine.MachineRenderState; - -import com.lowdragmc.lowdraglib.syncdata.IEnhancedManaged; -import com.lowdragmc.lowdraglib.syncdata.field.FieldManagedStorage; +import com.gregtechceu.gtceu.syncsystem.ISyncManaged; +import com.gregtechceu.gtceu.syncsystem.SyncDataHolder; import net.minecraft.core.Direction; -import net.minecraft.nbt.CompoundTag; import net.minecraftforge.client.model.data.ModelData; import lombok.Getter; import lombok.Setter; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.function.Predicate; @@ -21,10 +18,10 @@ * represents an abstract capability held by machine. Such as item, fluid, energy, etc. * All trait should be added while MetaMachine is creating. you cannot modify it on the fly。 */ -public abstract class MachineTrait implements IEnhancedManaged { +public abstract class MachineTrait implements ISyncManaged { @Getter - private final FieldManagedStorage syncStorage = new FieldManagedStorage(this); + protected final SyncDataHolder syncDataHolder = new SyncDataHolder(this); @Getter protected final MetaMachine machine; @@ -42,8 +39,8 @@ public final boolean hasCapability(@Nullable Direction side) { } @Override - public void onChanged() { - machine.onChanged(); + public void markAsChanged() { + machine.markAsChanged(); } public void onMachineLoad() {} @@ -60,17 +57,6 @@ public void setRenderState(MachineRenderState state) { getMachine().setRenderState(state); } - /** - * Use for data not able to be saved with the SyncData system, like optional mod compatiblity in internal machines. - * - * @param tag the CompoundTag to load data from - * @param forDrop if the save is done for dropping the machine as an item. - */ - public void saveCustomPersistedData(@NotNull CompoundTag tag, boolean forDrop) {} - - public void loadCustomPersistedData(@NotNull CompoundTag tag) {} - - @Override public void scheduleRenderUpdate() { machine.scheduleRenderUpdate(); } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableEnergyContainer.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableEnergyContainer.java index a8cf22eccad..46cc1872da9 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableEnergyContainer.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableEnergyContainer.java @@ -15,12 +15,10 @@ import com.gregtechceu.gtceu.api.recipe.GTRecipe; import com.gregtechceu.gtceu.api.recipe.ingredient.EnergyStack; import com.gregtechceu.gtceu.config.ConfigHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.gregtechceu.gtceu.utils.GTUtil; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; - import net.minecraft.core.Direction; import net.minecraftforge.energy.IEnergyStorage; import net.minecraftforge.items.IItemHandlerModifiable; @@ -36,13 +34,11 @@ public class NotifiableEnergyContainer extends NotifiableRecipeHandlerTrait implements IEnergyContainer { - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( - NotifiableEnergyContainer.class, NotifiableRecipeHandlerTrait.MANAGED_FIELD_HOLDER); @Getter protected IO handlerIO; @Getter - @Persisted - @DescSynced + @SaveField + @SyncToClient protected long energyStored; @Getter private long energyCapacity, inputVoltage, inputAmperage, outputVoltage, outputAmperage; @@ -97,11 +93,6 @@ public void resetBasicInfo(long maxCapacity, long maxInputVoltage, long maxInput checkOutputSubscription(); } - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - @Override public void onMachineLoad() { super.onMachineLoad(); @@ -147,6 +138,7 @@ public void setEnergyStored(long energyStored) { energyOutputPerSec += this.energyStored - energyStored; } this.energyStored = energyStored; + syncDataHolder.markClientSyncFieldDirty("energyStored"); checkOutputSubscription(); notifyListeners(); } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableFluidTank.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableFluidTank.java index 85905c71f6e..22235f5eb59 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableFluidTank.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableFluidTank.java @@ -9,12 +9,10 @@ import com.gregtechceu.gtceu.api.recipe.ingredient.IntProviderFluidIngredient; import com.gregtechceu.gtceu.api.transfer.fluid.CustomFluidTank; import com.gregtechceu.gtceu.api.transfer.fluid.IFluidHandlerModifiable; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.gregtechceu.gtceu.utils.GTTransferUtils; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; - import net.minecraft.core.Direction; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.FluidType; @@ -28,13 +26,11 @@ public class NotifiableFluidTank extends NotifiableRecipeHandlerTrait implements ICapabilityTrait, IFluidHandlerModifiable { - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(NotifiableFluidTank.class, - NotifiableRecipeHandlerTrait.MANAGED_FIELD_HOLDER); @Getter public final IO handlerIO; @Getter public final IO capabilityIO; - @Persisted + @SaveField @Getter protected final CustomFluidTank[] storages; @Getter @@ -42,8 +38,8 @@ public class NotifiableFluidTank extends NotifiableRecipeHandlerTrait storages, public void onContentsChanged() { isEmpty = null; + syncDataHolder.markClientSyncFieldDirty("storages"); notifyListeners(); } - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - @Override public List handleRecipeInner(IO io, GTRecipe recipe, List left, boolean simulate) { @@ -250,6 +242,7 @@ public void setLocked(boolean locked, FluidStack fluidStack) { this.lockedFluid.setFluid(FluidStack.EMPTY); setFilter(stack -> true); } + syncDataHolder.markClientSyncFieldDirty("lockedFluid"); onContentsChanged(); } diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableItemStackHandler.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableItemStackHandler.java index 7ca88ea0321..fe3433ebd8c 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableItemStackHandler.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableItemStackHandler.java @@ -10,13 +10,11 @@ import com.gregtechceu.gtceu.api.recipe.ingredient.IntProviderIngredient; import com.gregtechceu.gtceu.api.recipe.ingredient.SizedIngredient; import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; import com.gregtechceu.gtceu.utils.GTTransferUtils; import com.gregtechceu.gtceu.utils.GTUtil; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; - import net.minecraft.core.Direction; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Ingredient; @@ -37,14 +35,12 @@ public class NotifiableItemStackHandler extends NotifiableRecipeHandlerTrait implements ICapabilityTrait, IItemHandlerModifiable { - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( - NotifiableItemStackHandler.class, NotifiableRecipeHandlerTrait.MANAGED_FIELD_HOLDER); @Getter public final IO handlerIO; @Getter public final IO capabilityIO; - @Persisted - @DescSynced + @SaveField + @SyncToClient public final CustomItemStackHandler storage; @Accessors(fluent = true) @Getter @@ -76,14 +72,10 @@ public NotifiableItemStackHandler setFilter(Predicate filter) { public void onContentsChanged() { isEmpty = null; + syncDataHolder.markClientSyncFieldDirty("storage"); notifyListeners(); } - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - @Override public List handleRecipeInner(IO io, GTRecipe recipe, List left, boolean simulate) { return handleRecipe(io, recipe, left, simulate, handlerIO, storage); diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableLaserContainer.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableLaserContainer.java index b1e65e8fb10..23229f60c2d 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableLaserContainer.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableLaserContainer.java @@ -5,16 +5,11 @@ import com.gregtechceu.gtceu.api.machine.MetaMachine; import com.gregtechceu.gtceu.utils.GTUtil; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; - import net.minecraft.core.Direction; import net.minecraft.world.level.block.entity.BlockEntity; public class NotifiableLaserContainer extends NotifiableEnergyContainer implements ILaserContainer { - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( - NotifiableEnergyContainer.class, NotifiableRecipeHandlerTrait.MANAGED_FIELD_HOLDER); - public NotifiableLaserContainer(MetaMachine machine, long maxCapacity, long maxInputVoltage, long maxInputAmperage, long maxOutputVoltage, long maxOutputAmperage) { super(machine, maxCapacity, maxInputVoltage, maxInputAmperage, maxOutputVoltage, maxOutputAmperage); diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableRecipeHandlerTrait.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableRecipeHandlerTrait.java index 476a92cccc6..e87257f159e 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableRecipeHandlerTrait.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/NotifiableRecipeHandlerTrait.java @@ -1,37 +1,31 @@ package com.gregtechceu.gtceu.api.machine.trait; import com.gregtechceu.gtceu.api.machine.MetaMachine; - -import com.lowdragmc.lowdraglib.syncdata.ISubscription; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; +import com.gregtechceu.gtceu.syncsystem.annotations.SyncToClient; +import com.gregtechceu.gtceu.utils.ISubscription; import lombok.Getter; -import lombok.Setter; import java.util.ArrayList; import java.util.List; public abstract class NotifiableRecipeHandlerTrait extends MachineTrait implements IRecipeHandlerTrait { - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( - NotifiableRecipeHandlerTrait.class); protected List listeners = new ArrayList<>(); - @Persisted - @DescSynced + @SaveField + @SyncToClient @Getter - @Setter protected boolean isDistinct; public NotifiableRecipeHandlerTrait(MetaMachine machine) { super(machine); } - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; + public void setDistinct(boolean distinct) { + isDistinct = distinct; + syncDataHolder.markClientSyncFieldDirty("isDistinct"); } @Override diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeHandlerList.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeHandlerList.java index 1462cab76cd..d996db0b908 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeHandlerList.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeHandlerList.java @@ -4,8 +4,7 @@ import com.gregtechceu.gtceu.api.capability.recipe.IRecipeHandler; import com.gregtechceu.gtceu.api.capability.recipe.RecipeCapability; import com.gregtechceu.gtceu.api.recipe.GTRecipe; - -import com.lowdragmc.lowdraglib.syncdata.ISubscription; +import com.gregtechceu.gtceu.utils.ISubscription; import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; import lombok.Getter; diff --git a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java index 772eef9c6c0..2a9093f8de3 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java +++ b/src/main/java/com/gregtechceu/gtceu/api/machine/trait/RecipeLogic.java @@ -18,14 +18,11 @@ import com.gregtechceu.gtceu.api.registry.GTRegistries; import com.gregtechceu.gtceu.api.sound.AutoReleasedSound; import com.gregtechceu.gtceu.common.cover.MachineControllerCover; +import com.gregtechceu.gtceu.syncsystem.annotations.*; +import com.gregtechceu.gtceu.syncsystem.annotations.SaveField; import com.gregtechceu.gtceu.utils.GTMath; import com.lowdragmc.lowdraglib.gui.texture.IGuiTexture; -import com.lowdragmc.lowdraglib.syncdata.IEnhancedManaged; -import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; -import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; -import com.lowdragmc.lowdraglib.syncdata.annotation.UpdateListener; -import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; @@ -46,7 +43,7 @@ import java.util.*; -public class RecipeLogic extends MachineTrait implements IEnhancedManaged, IWorkable, IFancyTooltip { +public class RecipeLogic extends MachineTrait implements IWorkable, IFancyTooltip { public enum Status implements StringRepresentable { @@ -64,37 +61,35 @@ public enum Status implements StringRepresentable { } public static final EnumProperty STATUS_PROPERTY = GTMachineModelProperties.RECIPE_LOGIC_STATUS; - public static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(RecipeLogic.class); public final IRecipeLogicMachine machine; public List lastFailedMatches; @Getter - @Persisted - @DescSynced - @UpdateListener(methodName = "onStatusSynced") + @SaveField + @SyncToClient private Status status = Status.IDLE; - @Persisted - @DescSynced - @UpdateListener(methodName = "onActiveSynced") + @SaveField + @SyncToClient + @RerenderOnChanged protected boolean isActive; @Nullable - @Persisted - @DescSynced + @SaveField + @SyncToClient private Component waitingReason = null; /** * unsafe, it may not be found from {@link RecipeManager}. Do not index it. */ @Nullable @Getter - @Persisted - @DescSynced + @SaveField + @SyncToClient protected GTRecipe lastRecipe; @Getter - @Persisted - @DescSynced + @SaveField + @SyncToClient protected int consecutiveRecipes = 0; // Consecutive recipes that have been run /** * safe, it is the origin recipe before {@link IRecipeLogicMachine#fullModifyRecipe(GTRecipe)}' @@ -103,29 +98,30 @@ public enum Status implements StringRepresentable { */ @Nullable @Getter - @Persisted + @SaveField protected GTRecipe lastOriginRecipe; - @Persisted + @SaveField @Getter - @Setter - @DescSynced + @SyncToClient protected int progress; @Getter - @Persisted - @DescSynced + @SyncToClient + @SaveField protected int duration; @Getter(onMethod_ = @VisibleForTesting) protected boolean recipeDirty; - @Persisted + @SaveField @Getter protected long totalContinuousRunningTime; protected int runAttempt = 0; protected int runDelay = 0; - @Persisted - @Setter + @SaveField @Getter + @Setter protected boolean suspendAfterFinish = false; @Getter + @SaveField(nbtKey = "chance_cache") + @CustomDataField protected final Map, Object2IntMap> chanceCaches = makeChanceCaches(); protected TickableSubscription subscription; protected Object workingSound; @@ -136,16 +132,12 @@ public RecipeLogic(IRecipeLogicMachine machine) { } @SuppressWarnings("unused") - protected void onStatusSynced(Status newValue, Status oldValue) { + @ClientFieldChangeListener(fieldName = "status") + protected void onStatusSynced() { scheduleRenderUpdate(); updateSound(); } - @SuppressWarnings("unused") - protected void onActiveSynced(boolean newActive, boolean oldActive) { - scheduleRenderUpdate(); - } - /** * Call it to abort current recipe and reset the first state. */ @@ -162,6 +154,7 @@ public void resetRecipeLogic() { setStatus(Status.IDLE); } updateTickSubscription(); + getSyncDataHolder().resyncAllFields(); } @Override @@ -181,6 +174,11 @@ public void updateTickSubscription() { } } + public void setProgress(int progress) { + this.progress = progress; + syncDataHolder.markClientSyncFieldDirty("progress"); + } + public double getProgressPercent() { return duration == 0 ? 0.0 : progress / (duration * 1.0); } @@ -272,6 +270,7 @@ public void handleRecipeWorking() { } progress++; totalContinuousRunningTime++; + syncDataHolder.markClientSyncFieldDirty("progress"); } else { setWaiting(handleTick.reason()); @@ -312,6 +311,7 @@ public void handleRecipeWorking() { protected void regressRecipe() { if (progress > 0 && machine.regressWhenWaiting()) { this.progress = 1; + syncDataHolder.markClientSyncFieldDirty("progress"); } } @@ -332,6 +332,7 @@ public void findAndHandleRecipe() { lastOriginRecipe = null; handleSearchingRecipes(searchRecipe()); } + syncDataHolder.markClientSyncFieldDirty("lastRecipe"); recipeDirty = false; } @@ -372,6 +373,7 @@ public void setupRecipe(GTRecipe recipe) { progress = 0; duration = 0; isActive = false; + syncDataHolder.resyncAllFields(); return; } var handledIO = handleRecipeIO(recipe, IO.IN); @@ -385,6 +387,7 @@ public void setupRecipe(GTRecipe recipe) { progress = 0; duration = recipe.duration; isActive = true; + syncDataHolder.resyncAllFields(); } } @@ -399,10 +402,12 @@ public void setStatus(Status status) { } machine.notifyStatusChanged(this.status, status); this.status = status; + syncDataHolder.markClientSyncFieldDirty("status"); setRenderState(getRenderState().setValue(GTMachineModelProperties.RECIPE_LOGIC_STATUS, status)); updateTickSubscription(); if (this.status != Status.WAITING) { waitingReason = null; + syncDataHolder.markClientSyncFieldDirty("waitingReason"); } } } @@ -410,6 +415,7 @@ public void setStatus(Status status) { public void setWaiting(@Nullable Component reason) { setStatus(Status.WAITING); waitingReason = reason; + syncDataHolder.markClientSyncFieldDirty("waitingReason"); machine.onWaiting(); } @@ -498,6 +504,7 @@ public void onRecipeFinish() { isActive = false; // Force a recipe recheck. lastRecipe = null; + syncDataHolder.resyncAllFields(); return; } if (machine.alwaysTryModifyRecipe()) { @@ -507,6 +514,7 @@ public void onRecipeFinish() { markLastRecipeDirty(); } else { lastRecipe = modified; + syncDataHolder.markClientSyncFieldDirty("lastRecipe"); } } else { markLastRecipeDirty(); @@ -525,6 +533,7 @@ public void onRecipeFinish() { progress = 0; duration = 0; isActive = false; + syncDataHolder.resyncAllFields(); } } } @@ -546,17 +555,14 @@ public void interruptRecipe() { setStatus(Status.IDLE); progress = 0; duration = 0; + syncDataHolder.markClientSyncFieldDirty("progress"); + syncDataHolder.markClientSyncFieldDirty("duration"); } } // Remains for legacy + for subclasses public void inValid() {} - @Override - public ManagedFieldHolder getFieldHolder() { - return MANAGED_FIELD_HOLDER; - } - ////////////////////////////////////// // ******** MISC *********// ////////////////////////////////////// @@ -613,9 +619,8 @@ protected Map, Object2IntMap> makeChanceCaches() { return map; } - @Override - public void saveCustomPersistedData(@NotNull CompoundTag tag, boolean forDrop) { - super.saveCustomPersistedData(tag, forDrop); + @FieldDataModifier(fieldName = "chanceCaches", target = FieldDataModifier.ModifyTarget.SAVE_NBT) + private Tag saveChanceCacheData(Tag tag, boolean saveClientFields) { CompoundTag chanceCache = new CompoundTag(); this.chanceCaches.forEach((cap, cache) -> { ListTag cacheTag = new ListTag(); @@ -628,39 +633,27 @@ public void saveCustomPersistedData(@NotNull CompoundTag tag, boolean forDrop) { } chanceCache.put(cap.name, cacheTag); }); - tag.put("chance_cache", chanceCache); - } - - @Override - public void loadCustomPersistedData(@NotNull CompoundTag tag) { - super.loadCustomPersistedData(tag); - CompoundTag chanceCache = tag.getCompound("chance_cache"); - for (String key : chanceCache.getAllKeys()) { - RecipeCapability cap = GTRegistries.RECIPE_CAPABILITIES.get(key); - if (cap == null) continue; // Necessary since we removed a RecipeCapability when nuking Create - // noinspection rawtypes - Object2IntMap map = this.chanceCaches.computeIfAbsent(cap, RecipeCapability::makeChanceCache); - - ListTag chanceTag = chanceCache.getList(key, Tag.TAG_COMPOUND); - for (int i = 0; i < chanceTag.size(); ++i) { - CompoundTag chanceKey = chanceTag.getCompound(i); - var entry = cap.serializer.fromNbt(chanceKey.get("entry")); - int value = chanceKey.getInt("cached_chance"); - // noinspection unchecked - map.put(entry, value); + return chanceCache; + } + + @FieldDataModifier(fieldName = "chanceCaches", target = FieldDataModifier.ModifyTarget.LOAD_NBT) + private void loadChanceCacheData(Tag tag, boolean loadClientFields) { + if (tag instanceof CompoundTag chanceCache) { + for (String key : chanceCache.getAllKeys()) { + RecipeCapability cap = GTRegistries.RECIPE_CAPABILITIES.get(key); + if (cap == null) continue; // Necessary since we removed a RecipeCapability when nuking Create + // noinspection rawtypes + Object2IntMap map = this.chanceCaches.computeIfAbsent(cap, RecipeCapability::makeChanceCache); + + ListTag chanceTag = chanceCache.getList(key, Tag.TAG_COMPOUND); + for (int i = 0; i < chanceTag.size(); ++i) { + CompoundTag chanceKey = chanceTag.getCompound(i); + var entry = cap.serializer.fromNbt(chanceKey.get("entry")); + int value = chanceKey.getInt("cached_chance"); + // noinspection unchecked + map.put(entry, value); + } } } - this.chanceCaches.forEach((cap, cache) -> { - ListTag cacheTag = new ListTag(); - for (var entry : cache.object2IntEntrySet()) { - CompoundTag compoundTag = new CompoundTag(); - var obj = cap.contentToNbt(entry.getKey()); - compoundTag.put("entry", obj); - compoundTag.putInt("cached_chance", entry.getIntValue()); - cacheTag.add(compoundTag); - } - chanceCache.put(cap.name, cacheTag); - }); - tag.put("chance_cache", chanceCache); } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/misc/forge/FluidTankHandler.java b/src/main/java/com/gregtechceu/gtceu/api/misc/forge/FluidTankHandler.java new file mode 100644 index 00000000000..7c2e533a3ce --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/misc/forge/FluidTankHandler.java @@ -0,0 +1,62 @@ +package com.gregtechceu.gtceu.api.misc.forge; + +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.IFluidTank; +import net.minecraftforge.fluids.capability.IFluidHandler; + +import org.jetbrains.annotations.NotNull; + +public class FluidTankHandler implements IFluidHandler { + + public static IFluidHandler getTankFluidHandler(IFluidTank tank) { + if (tank instanceof IFluidHandler fluidHandler) { + return fluidHandler; + } + return new FluidTankHandler(tank); + } + + private final IFluidTank fluidTank; + + public FluidTankHandler(IFluidTank tank) { + this.fluidTank = tank; + } + + @Override + public int fill(FluidStack resource, FluidAction action) { + return this.fluidTank.fill(resource, action); + } + + @Override + public @NotNull FluidStack drain(FluidStack resource, FluidAction action) { + FluidStack currentFluid = this.fluidTank.getFluid(); + if (currentFluid.isEmpty() || !currentFluid.isFluidEqual(resource)) { + return FluidStack.EMPTY; + } + return this.fluidTank.drain(resource, action); + } + + @Override + public @NotNull FluidStack drain(int maxDrain, FluidAction action) { + return this.fluidTank.drain(maxDrain, action); + } + + @Override + public int getTanks() { + return 1; + } + + @Override + public @NotNull FluidStack getFluidInTank(int tank) { + return this.fluidTank.getFluid(); + } + + @Override + public int getTankCapacity(int tank) { + return this.fluidTank.getCapacity(); + } + + @Override + public boolean isFluidValid(int tank, @NotNull FluidStack stack) { + return this.fluidTank.isFluidValid(stack); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/misc/virtualregistry/VirtualEntry.java b/src/main/java/com/gregtechceu/gtceu/api/misc/virtualregistry/VirtualEntry.java index 1c4d24e118f..b4f8c322012 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/misc/virtualregistry/VirtualEntry.java +++ b/src/main/java/com/gregtechceu/gtceu/api/misc/virtualregistry/VirtualEntry.java @@ -1,7 +1,5 @@ package com.gregtechceu.gtceu.api.misc.virtualregistry; -import com.lowdragmc.lowdraglib.syncdata.ITagSerializable; - import net.minecraft.nbt.CompoundTag; import net.minecraftforge.common.util.INBTSerializable; @@ -14,7 +12,7 @@ @Getter @Accessors(chain = true) -public abstract class VirtualEntry implements INBTSerializable, ITagSerializable { +public abstract class VirtualEntry implements INBTSerializable { public static final String DEFAULT_COLOR = "FFFFFFFF"; protected static final String COLOR_KEY = "color"; diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/GuiError.java b/src/main/java/com/gregtechceu/gtceu/api/mui/GuiError.java new file mode 100644 index 00000000000..130c8fbb948 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/GuiError.java @@ -0,0 +1,51 @@ +package com.gregtechceu.gtceu.api.mui; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; + +import lombok.Getter; +import org.apache.logging.log4j.Level; + +import java.util.Objects; + +public class GuiError { + + public static void throwNew(IGuiElement guiElement, Type type, String msg) { + if (GTCEu.isClientSide()) { + GuiErrorHandler.INSTANCE.pushError(guiElement, type, msg); + } + } + + @Getter + private final Level level = Level.ERROR; + @Getter + private final String msg; + @Getter + private final IGuiElement reference; + @Getter + private final Type type; + + protected GuiError(String msg, IGuiElement reference, Type type) { + this.msg = msg; + this.reference = reference; + this.type = type; + } + + @Override + public String toString() { + return "MUI [" + this.type.toString() + "][" + this.reference.toString() + "]: " + this.msg; + } + + @Override + public int hashCode() { + return Objects.hash(this.level, this.reference, this.type); + } + + public enum Type { + DRAW, + SIZING, + WIDGET_TREE, + INTERACTION, + SYNC + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/GuiErrorHandler.java b/src/main/java/com/gregtechceu/gtceu/api/mui/GuiErrorHandler.java new file mode 100644 index 00000000000..185344fc6c1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/GuiErrorHandler.java @@ -0,0 +1,40 @@ +package com.gregtechceu.gtceu.api.mui; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@OnlyIn(Dist.CLIENT) +public class GuiErrorHandler { + + public static final GuiErrorHandler INSTANCE = new GuiErrorHandler(); + + private final Set errorSet = new ObjectOpenHashSet<>(); + @Getter + private final List errors = new ArrayList<>(); + + private GuiErrorHandler() {} + + public void clear() { + this.errors.clear(); + } + + void pushError(IGuiElement reference, GuiError.Type type, String msg) { + GuiError error = new GuiError(msg, reference, type); + if (this.errorSet.add(error)) { + GTCEu.LOGGER.log(error.getLevel(), error); + this.errors.add(error); + } + } + + public void drawErrors(int x, int y) {} +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/animation/Animator.java b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/Animator.java new file mode 100644 index 00000000000..5aab34b489e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/Animator.java @@ -0,0 +1,199 @@ +package com.gregtechceu.gtceu.api.mui.animation; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IInterpolation; +import com.gregtechceu.gtceu.api.mui.utils.Interpolation; + +import lombok.Getter; + +import java.util.function.DoubleConsumer; +import java.util.function.DoublePredicate; + +public class Animator extends BaseAnimator implements IAnimator { + + @Getter + private float min = 0.0f; + @Getter + private float max = 1.0f; + @Getter + private int duration = 250; + @Getter + private IInterpolation curve = Interpolation.LINEAR; + private DoublePredicate onUpdate; + private Runnable onFinish; + + private int progress = 0; + + @Override + public void reset(boolean atEnd) { + super.reset(atEnd); + this.progress = atEnd ? this.duration : 0; + } + + public Animator copy(boolean reversed) { + Animator animator = new Animator() + .curve(this.curve) + .reverseOnFinish(this.reverseOnFinish) + .repeatsOnFinish(this.repeats) + .onUpdate(this.onUpdate) + .duration(this.duration) + .onFinish(this.onFinish); + if (reversed) { + animator.bounds(this.max, this.min); + } else { + animator.bounds(this.min, this.max); + } + return animator; + } + + @Override + public int advance(int elapsedTime) { + if (!isAnimating()) return elapsedTime; + while (elapsedTime > 0) { + int max = isAnimatingForward() ? this.duration - this.progress : this.progress; + int prog = Math.min(max, elapsedTime); + this.progress += prog * getDirection(); + elapsedTime -= prog; + if (onUpdate()) { + stop(true); + break; + } + if ((isAnimatingForward() && this.progress >= this.duration) || + (isAnimatingReverse() && this.progress <= 0)) { + stop(false); + if (!isAnimating()) { + onAnimationFinished(true, true); + break; + } + } + } + return elapsedTime; + } + + protected boolean onUpdate() { + return this.onUpdate != null && this.onUpdate.test(getRawValue()); + } + + protected void onAnimationFinished(boolean finishedOneCycle, boolean finishedAllRepeats) { + if (this.onFinish != null) { + this.onFinish.run(); + } + } + + public boolean isAtEnd() { + return this.progress >= this.duration; + } + + public boolean isAtStart() { + return this.progress <= 0; + } + + protected float getRawValue() { + return this.curve.interpolate(this.min, this.max, (float) this.progress / this.duration); + } + + public float getValue() { + // advance(); + return getRawValue(); + } + + @Override + public boolean hasProgressed() { + if (!isAnimating()) return false; + return isAnimatingForward() ? this.progress > 0 : this.progress < this.duration; + } + + /** + * Sets the min bound of the value that will be interpolated. + * + * @param min min value + * @return this + */ + public Animator min(float min) { + this.min = min; + return this; + } + + /** + * Sets the max bound of the value that will be interpolated. + * + * @param max max value + * @return this + */ + public Animator max(float max) { + this.max = max; + return this; + } + + /** + * Sets the bounds of the value that will be interpolated. + * + * @param min min value + * @param max max value + * @return this + */ + public Animator bounds(float min, float max) { + this.min = min; + this.max = max; + return this; + } + + /** + * The duration of this animation in milliseconds. Note this is not 100% accurate. + * Usually it's plus minus 2ms, but can rarely be more. + * + * @param duration duration in milliseconds + * @return this + */ + public Animator duration(int duration) { + this.duration = duration; + return this; + } + + /** + * Sets the interpolation curve, which is used to interpolate between the bounds. + * + * @param curve curve to interpolate on + * @return this + */ + public Animator curve(IInterpolation curve) { + this.curve = curve; + return this; + } + + /** + * Sets a function which is executed everytime the progress updates, that is on every frame. + * The argument of the function is the interpolated value. + * + * @param onUpdate update function + * @return this + */ + public Animator onUpdate(DoublePredicate onUpdate) { + this.onUpdate = onUpdate; + return this; + } + + /** + * Sets a function which is executed everytime the progress updates, that is on every frame. + * The argument of the function is the interpolated value. + * + * @param onUpdate update function + * @return this + */ + public Animator onUpdate(DoubleConsumer onUpdate) { + return onUpdate(val -> { + onUpdate.accept(val); + return false; + }); + } + + /** + * Sets a function which is executed everytime, on animation, cycle or all repeats is finished. + * + * @param onFinish finish function + * @return this + */ + public Animator onFinish(Runnable onFinish) { + this.onFinish = onFinish; + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/animation/AnimatorManager.java b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/AnimatorManager.java new file mode 100644 index 00000000000..86f8202afcf --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/AnimatorManager.java @@ -0,0 +1,64 @@ +package com.gregtechceu.gtceu.api.mui.animation; + +import net.minecraft.Util; +import net.minecraftforge.client.event.ScreenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +import java.util.ArrayList; +import java.util.List; + +public class AnimatorManager { + + private static final List animators = new ArrayList<>(16); + private static final List queuedAnimators = new ArrayList<>(8); + private static long lastTime = 0; + private static boolean waitClearAnimators = false; + + static void startAnimation(IAnimator animator) { + if (!animators.contains(animator) && !queuedAnimators.contains(animator)) { + queuedAnimators.add(animator); + } + } + + private AnimatorManager() {} + + public static void init() { + MinecraftForge.EVENT_BUS.register(new AnimatorManager()); + } + + @SubscribeEvent(priority = EventPriority.HIGHEST) + public void onDraw(ScreenEvent.Render.Pre event) { + long time = Util.getMillis(); + int elapsedTime = IAnimator.getTimeDiff(lastTime, time); + checkClearAnimators(); + if (lastTime > 0 && !animators.isEmpty()) { + animators.removeIf(animator -> { + if (animator == null) return true; + if (animator.isPaused()) return false; + animator.advance(elapsedTime); + return !animator.isAnimating(); + }); + } + lastTime = time; + animators.addAll(queuedAnimators); + queuedAnimators.clear(); + checkClearAnimators(); + } + + private static void checkClearAnimators() { + if (waitClearAnimators) { + waitClearAnimators = false; + animators.forEach(iAnimator -> iAnimator.stop(false)); + animators.clear(); + } + } + + @SubscribeEvent(priority = EventPriority.LOWEST) + public void onClose(ScreenEvent.Closing event) { + // stop and yeet all animators on gui close + // we can't clear now otherwise we might get a CME because of multithreading + waitClearAnimators = true; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/animation/BaseAnimator.java b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/BaseAnimator.java new file mode 100644 index 00000000000..ff452cf1d0a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/BaseAnimator.java @@ -0,0 +1,128 @@ +package com.gregtechceu.gtceu.api.mui.animation; + +import org.jetbrains.annotations.Nullable; + +public abstract class BaseAnimator> implements IAnimator { + + private IAnimator parent; + protected boolean reverseOnFinish = false; + protected int repeats = 0; + + private byte direction = 0; + private boolean paused = false; + private boolean startedReverse = false; + private int repeated = 0; + + void setParent(IAnimator parent) { + this.parent = parent; + } + + @SuppressWarnings("unchecked") + public A getThis() { + return (A) this; + } + + @Nullable + public final IAnimator getParent() { + return parent; + } + + @Override + public void reset(boolean atEnd) { + this.startedReverse = atEnd; + this.repeated = 0; + } + + @Override + public boolean stop(boolean force) { + if (isAnimating() && !force) { + if (this.reverseOnFinish && this.startedReverse == isAnimatingReverse()) { + onAnimationFinished(false, false); + // started reverse -> bounce back and animate forward + animate(isAnimatingForward()); + return false; + } + if (repeats != 0 && (repeated < repeats || repeats < 0)) { + onAnimationFinished(true, false); + // started forward -> full cycle finished -> try repeating + boolean reverse = !this.reverseOnFinish == isAnimatingReverse(); + animate(reverse); + repeated++; + return false; + } + } + this.direction = 0; + return true; + } + + protected void onAnimationFinished(boolean finishedOneCycle, boolean finishedAllRepeats) {} + + @Override + public void pause() { + this.paused = true; + } + + @Override + public void resume(boolean reverse) { + this.paused = false; + this.direction = (byte) (reverse ? -1 : 1); + if (this.parent == null) AnimatorManager.startAnimation(this); + } + + @Override + public boolean isPaused() { + return paused; + } + + @Override + public boolean isAnimating() { + return this.direction != 0; + } + + @Override + public boolean isAnimatingReverse() { + return this.direction < 0; + } + + @Override + public boolean isAnimatingForward() { + return this.direction > 0; + } + + public final byte getDirection() { + return direction; + } + + /** + * Sets if the animation should reverse animate once after it finished. + * If the animation started in reverse it will animate forward on finish. + * + * @param reverseOnFinish if animation should bounce back on finish + * @return this + */ + public A reverseOnFinish(boolean reverseOnFinish) { + this.reverseOnFinish = reverseOnFinish; + return getThis(); + } + + /** + * Sets how often the animation should repeat. If {@link #reverseOnFinish(boolean)} is set to true, it will repeat + * the whole cycle. + * If the number of repeats is negative, it will repeat infinitely. + * + * @param repeats how often the animation should repeat. + * @return this + */ + public A repeatsOnFinish(int repeats) { + this.repeats = repeats; + return getThis(); + } + + public SequentialAnimator followedBy(IAnimator animator) { + return new SequentialAnimator(this, animator); + } + + public ParallelAnimator inParallelWith(IAnimator animator) { + return new ParallelAnimator(this, animator); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/animation/IAnimatable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/IAnimatable.java new file mode 100644 index 00000000000..38bf2801288 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/IAnimatable.java @@ -0,0 +1,63 @@ +package com.gregtechceu.gtceu.api.mui.animation; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IInterpolation; + +public interface IAnimatable> { + + T interpolate(T start, T end, float t); + + T copyOrImmutable(); + + default boolean shouldAnimate(T target) { + return !equals(target); + } + + default MutableObjectAnimator animator(T target) { + T self = (T) this; + return new MutableObjectAnimator<>(self, self.copyOrImmutable(), target); + } + + default void animate(T target) { + animate(target, false); + } + + default void animate(T target, boolean reverse) { + if (shouldAnimate(target)) { + animator(target).animate(reverse); + } + } + + default void animate(T target, boolean reverse, boolean reverseOnFinish, int repeatsOnFinish) { + if (shouldAnimate(target)) { + animator(target).reverseOnFinish(reverseOnFinish).repeatsOnFinish(repeatsOnFinish).animate(reverse); + } + } + + default void animate(T target, int durationMs, boolean reverse) { + if (shouldAnimate(target)) { + animator(target).duration(durationMs).animate(reverse); + } + } + + default void animate(T target, IInterpolation curve, boolean reverse) { + if (shouldAnimate(target)) { + animator(target).curve(curve).animate(reverse); + } + } + + default void animate(T target, IInterpolation curve, int durationMs, boolean reverse) { + animate(target, curve, durationMs, reverse, false, 0); + } + + default void animate(T target, IInterpolation curve, int durationMs, boolean reverse, boolean reverseOnFinish, + int repeatsOnFinish) { + if (shouldAnimate(target)) { + animator(target) + .curve(curve) + .duration(durationMs) + .reverseOnFinish(reverseOnFinish) + .repeatsOnFinish(repeatsOnFinish) + .animate(reverse); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/animation/IAnimator.java b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/IAnimator.java new file mode 100644 index 00000000000..adeac87c6ad --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/IAnimator.java @@ -0,0 +1,63 @@ +package com.gregtechceu.gtceu.api.mui.animation; + +import net.minecraft.Util; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +public interface IAnimator { + + @Nullable + IAnimator getParent(); + + default void animate(boolean reverse) { + reset(reverse); + resume(reverse); + } + + default void animate() { + animate(false); + } + + boolean stop(boolean force); + + void pause(); + + void resume(boolean reverse); + + void reset(boolean atEnd); + + default void reset() { + reset(false); + } + + /** + * Advances the animation by a given duration. + * + * @param elapsedTime elapsed time in ms + * @return remaining time (elapsed time - consumed time) + */ + @ApiStatus.OverrideOnly + int advance(int elapsedTime); + + boolean isPaused(); + + boolean isAnimating(); + + boolean isAnimatingReverse(); + + boolean hasProgressed(); + + default boolean isAnimatingForward() { + return isAnimating() && !isAnimatingReverse(); + } + + static int getTimeDiff(long startTime) { + return getTimeDiff(startTime, Util.getMillis()); + } + + static int getTimeDiff(long startTime, long currentTime) { + long elapsedTime = Math.abs(currentTime - startTime); + return (int) Math.min(Integer.MAX_VALUE, elapsedTime); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/animation/MutableObjectAnimator.java b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/MutableObjectAnimator.java new file mode 100644 index 00000000000..2e3521ebbb8 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/MutableObjectAnimator.java @@ -0,0 +1,38 @@ +package com.gregtechceu.gtceu.api.mui.animation; + +import java.util.function.Consumer; + +public class MutableObjectAnimator> extends Animator { + + private final T from; + private final T to; + private final T animatable; + private Consumer intermediateConsumer; + + public MutableObjectAnimator(T animatable, T from, T to) { + this.from = from; + this.to = to; + this.animatable = animatable; + bounds(0f, 1f); + } + + @Override + public void resume(boolean reverse) { + super.resume(reverse); + this.animatable.interpolate(this.from, this.to, getRawValue()); + } + + @Override + protected boolean onUpdate() { + T intermediate = this.animatable.interpolate(this.from, this.to, getRawValue()); + if (this.intermediateConsumer != null) { + this.intermediateConsumer.accept(intermediate); + } + return super.onUpdate(); + } + + public MutableObjectAnimator intermediateConsumer(Consumer consumer) { + this.intermediateConsumer = consumer; + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/animation/ParallelAnimator.java b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/ParallelAnimator.java new file mode 100644 index 00000000000..afe6bcd6e2e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/ParallelAnimator.java @@ -0,0 +1,121 @@ +package com.gregtechceu.gtceu.api.mui.animation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ParallelAnimator extends BaseAnimator implements IAnimator { + + private final List animators; + private int waitTimeBetweenAnimators; + + private int startedAnimating = 0; + private int finishedAnimating = 0; + private int waitTime = 0; + + public ParallelAnimator(List animators) { + this.animators = new ArrayList<>(animators); + this.animators.forEach(animator -> { + if (animator instanceof BaseAnimator baseAnimator) { + baseAnimator.setParent(this); + } + }); + } + + public ParallelAnimator(IAnimator... animators) { + this.animators = new ArrayList<>(); + Collections.addAll(this.animators, animators); + this.animators.forEach(animator -> { + if (animator instanceof BaseAnimator baseAnimator) { + baseAnimator.setParent(this); + } + }); + } + + @Override + public void animate(boolean reverse) { + super.animate(reverse); + if (this.waitTimeBetweenAnimators <= 0) { + for (IAnimator animator : animators) { + animator.animate(reverse); + } + this.startedAnimating = this.animators.size(); + } else { + this.animators.get(this.startedAnimating).animate(reverse); + } + } + + @Override + + public boolean stop(boolean force) { + if (super.stop(force)) { + for (IAnimator animator : animators) { + animator.stop(force); + } + return true; + } + return false; + } + + @Override + public void reset(boolean atEnd) { + super.reset(atEnd); + this.startedAnimating = 0; + this.finishedAnimating = 0; + for (IAnimator animator : animators) { + animator.reset(atEnd); + } + } + + @Override + public int advance(int elapsedTime) { + int remainingTime = 0; + for (int i = 0; i < this.startedAnimating; i++) { + IAnimator animator = this.animators.get(i); + if (!animator.isAnimating()) continue; + remainingTime = Math.max(remainingTime, animator.advance(elapsedTime)); + if (!animator.isAnimating()) { + this.finishedAnimating++; + if (isFinished()) { + stop(false); + return remainingTime; + } + } + } + while (elapsedTime > 0 && this.startedAnimating < this.animators.size()) { + int prog = Math.min(elapsedTime, this.waitTimeBetweenAnimators - this.waitTime); + this.waitTime += prog; + elapsedTime -= prog; + if (this.waitTime >= this.waitTimeBetweenAnimators) { + this.animators.get(this.startedAnimating).animate(isAnimatingReverse()); + this.waitTime -= this.waitTimeBetweenAnimators; + this.startedAnimating++; + } + } + return Math.min(elapsedTime, remainingTime); + } + + public boolean isFinished() { + return this.finishedAnimating == this.animators.size(); + } + + @Override + public boolean hasProgressed() { + return isAnimating() && this.startedAnimating > 0; + } + + public ParallelAnimator waitTimeBetweenAnimators(int waitTime) { + this.waitTimeBetweenAnimators = waitTime; + return this; + } + + @Override + public ParallelAnimator inParallelWith(IAnimator animator) { + if (isAnimating()) { + throw new IllegalStateException("Can't add animators while animating"); + } + reset(); + this.animators.add(animator); + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/animation/SequentialAnimator.java b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/SequentialAnimator.java new file mode 100644 index 00000000000..00c7ba144ed --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/SequentialAnimator.java @@ -0,0 +1,88 @@ +package com.gregtechceu.gtceu.api.mui.animation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SequentialAnimator extends BaseAnimator implements IAnimator { + + private final List animators; + private int currentIndex = 0; + + public SequentialAnimator(List animators) { + this.animators = new ArrayList<>(animators); + this.animators.forEach(animator -> { + if (animator instanceof BaseAnimator baseAnimator) { + baseAnimator.setParent(this); + } + }); + } + + public SequentialAnimator(IAnimator... animators) { + this.animators = new ArrayList<>(); + Collections.addAll(this.animators, animators); + this.animators.forEach(animator -> { + if (animator instanceof BaseAnimator baseAnimator) { + baseAnimator.setParent(this); + } + }); + } + + @Override + public void animate(boolean reverse) { + if (this.animators.isEmpty()) return; + super.animate(reverse); + // start first animation + this.animators.get(this.currentIndex).animate(reverse); + } + + @Override + public void reset(boolean atEnd) { + super.reset(atEnd); + this.currentIndex = atEnd ? this.animators.size() - 1 : 0; + this.animators.forEach(animator -> animator.reset(atEnd)); + } + + @Override + public void resume(boolean reverse) { + super.resume(reverse); + this.animators.get(this.currentIndex).resume(reverse); + } + + @Override + public int advance(int elapsedTime) { + while (isAnimating() && elapsedTime > 0) { + IAnimator animator = this.animators.get(currentIndex); + elapsedTime = animator.advance(elapsedTime); + if (!animator.isAnimating()) { + // animator has finished + this.currentIndex += getDirection(); + // GTCEu.LOGGER.info("Finished {}th animator", this.currentIndex); + if (this.currentIndex >= this.animators.size() || this.currentIndex < 0) { + // whole sequence has finished + stop(false); + } else { + // start next animation + animator = this.animators.get(this.currentIndex); + animator.animate(isAnimatingReverse()); + } + } + } + return elapsedTime; + } + + @Override + public boolean hasProgressed() { + return !this.animators.isEmpty() && this.animators.get(0).hasProgressed(); + } + + @Override + public SequentialAnimator followedBy(IAnimator animator) { + if (isAnimating()) { + throw new IllegalStateException("Can't add animators while animating"); + } + reset(); + this.animators.add(animator); + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/animation/Wait.java b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/Wait.java new file mode 100644 index 00000000000..787e863c29a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/animation/Wait.java @@ -0,0 +1,41 @@ +package com.gregtechceu.gtceu.api.mui.animation; + +import lombok.Setter; +import lombok.experimental.Accessors; + +@Accessors(fluent = true, chain = true) +public class Wait extends BaseAnimator { + + @Setter + private int duration; + private int progress = 0; + + public Wait() { + this(250); + } + + public Wait(int duration) { + this.duration = duration; + } + + @Override + public void reset(boolean atEnd) { + this.progress = 0; + } + + @Override + public int advance(int elapsedTime) { + int max = this.duration - this.progress; + int prog = Math.min(max, elapsedTime); + this.progress += prog; + if (this.progress >= this.duration) { + stop(false); + } + return elapsedTime - prog; + } + + @Override + public boolean hasProgressed() { + return progress > 0 && isAnimating(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/GuiAxis.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/GuiAxis.java new file mode 100644 index 00000000000..df57956d26e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/GuiAxis.java @@ -0,0 +1,19 @@ +package com.gregtechceu.gtceu.api.mui.base; + +public enum GuiAxis { + + X, + Y; + + public boolean isHorizontal() { + return this == X; + } + + public boolean isVertical() { + return this == Y; + } + + public GuiAxis getOther() { + return this == X ? Y : X; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IItemUIHolder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IItemUIHolder.java new file mode 100644 index 00000000000..882d174f09c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IItemUIHolder.java @@ -0,0 +1,39 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.item.component.IInteractionItem; +import com.gregtechceu.gtceu.api.mui.factory.PlayerInventoryGuiData; +import com.gregtechceu.gtceu.api.mui.factory.PlayerInventoryUIFactory; + +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; + +public interface IItemUIHolder extends IUIHolder>, IInteractionItem { + + default boolean shouldOpenUI() { + return true; + } + + @Override + default InteractionResultHolder use(Item item, Level level, Player player, InteractionHand usedHand) { + if (!shouldOpenUI()) + return IInteractionItem.super.use(item, level, player, usedHand); + if (level.isClientSide) + PlayerInventoryUIFactory.INSTANCE.openFromHandClient(usedHand); + return InteractionResultHolder.sidedSuccess(player.getItemInHand(usedHand), level.isClientSide); + } + + @Override + default InteractionResult useOn(UseOnContext context) { + if (!shouldOpenUI()) + return IInteractionItem.super.useOn(context); + if (context.getLevel().isClientSide) + PlayerInventoryUIFactory.INSTANCE.openFromHandClient(context.getHand()); + return InteractionResult.SUCCESS; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IJsonSerializable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IJsonSerializable.java new file mode 100644 index 00000000000..2782f238e76 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IJsonSerializable.java @@ -0,0 +1,48 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.google.gson.JsonObject; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.JsonOps; +import org.jetbrains.annotations.ApiStatus; + +public interface IJsonSerializable> { + + /** + * Override this + * + * @return the codec to serialize this object with + */ + // TODO actually implement on subclasses + @ApiStatus.OverrideOnly + default Codec getCodec() { + return Codec.PASSTHROUGH.flatComapMap(dynamic -> { + loadFromJson(dynamic.cast(JsonOps.INSTANCE).getAsJsonObject()); + return (T) this; + }, object -> { + JsonObject jsonObject = new JsonObject(); + if (saveToJson(jsonObject)) { + return DataResult.success(new Dynamic<>(JsonOps.INSTANCE, jsonObject)); + } + return DataResult.error(() -> "Failed to serialize drawable %s".formatted(object)); + }); + } + + /** + * Reads extra json data after this drawable is created. + * + * @param json json to read from + */ + default void loadFromJson(JsonObject json) {} + + /** + * Writes all json data necessary so that deserializing it results in the same drawable. + * + * @param json json to write to + * @return if the drawable was serialized + */ + default boolean saveToJson(JsonObject json) { + return false; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMathValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMathValue.java new file mode 100644 index 00000000000..600f53a5dc1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMathValue.java @@ -0,0 +1,36 @@ +package com.gregtechceu.gtceu.api.mui.base; + +/** + * Math value interface + *

+ * This interface provides only one method which is used by all + * mathematical related classes. The point of this interface is to + * provide generalized abstract method for computing/fetching some value + * from different mathematical classes. + */ +public interface IMathValue { + + /** + * Get computed or stored value + */ + IMathValue get(); + + boolean isNumber(); + + void set(double value); + + void set(String value); + + double doubleValue(); + + boolean booleanValue(); + + String stringValue(); + + class EvaluateException extends RuntimeException { + + public EvaluateException(String message) { + super(message); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java new file mode 100644 index 00000000000..b9b7f8eb015 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java @@ -0,0 +1,105 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.utils.Rectangle; +import com.gregtechceu.gtceu.client.mui.screen.ClientScreenHandler; +import com.gregtechceu.gtceu.client.mui.screen.ContainerScreenWrapper; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.client.mui.screen.ScreenWrapper; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.gregtechceu.gtceu.core.mixins.client.AbstractContainerScreenAccessor; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.Slot; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +/** + * Implement this interface on a {@link Screen} to be able to use it as a custom wrapper. + * The Screen should have final {@link ModularScreen} field, which is set from the constructor. + * Additionally, the Screen MUST call {@link ModularScreen#construct(IMuiScreen)} in its constructor. + * See {@link ScreenWrapper ScreenWrapper} and {@link ContainerScreenWrapper GuiContainerWrapper} + * for default implementations. + */ +@OnlyIn(Dist.CLIENT) +public interface IMuiScreen { + + /** + * Returns the {@link ModularScreen} that is being wrapped. This should return a final instance field. + * + * @return the wrapped modular screen + */ + @NotNull + ModularScreen getScreen(); + + /** + * This method decides how the gui background is drawn. + * The intended usage is to override {@link Screen#renderBackground(GuiGraphics)} and call this method + * with the super method reference as the second parameter. + * + * @param guiGraphics this screen's {@link GuiGraphics} instance + * @param drawFunction a method reference to draw the world background normally with the + * {@code guiGraphics} as the parameter + */ + @ApiStatus.NonExtendable + default void handleDrawBackground(GuiGraphics guiGraphics, Consumer drawFunction) { + if (ClientScreenHandler.shouldDrawWorldBackground()) { + drawFunction.accept(guiGraphics); + } + ClientScreenHandler.drawDarkBackground(getWrappedScreen(), guiGraphics); + } + + /** + * This method is called every time the {@link ModularScreen} resizes. + * This usually only affects {@link AbstractContainerScreen AbstractContainerScreens}. + * + * @param area area of the main panel + */ + default void updateGuiArea(Rectangle area) { + if (getWrappedScreen() instanceof AbstractContainerScreenAccessor acc) { + acc.setLeftPos(area.x); + acc.setTopPos(area.y); + acc.setImageWidth(area.width); + acc.setImageHeight(area.height); + } + } + + /** + * @return if this wrapper is a {@link AbstractContainerScreen} + */ + @ApiStatus.NonExtendable + default boolean isGuiContainer() { + return getWrappedScreen() instanceof AbstractContainerScreen; + } + + /** + * Hovering widget is handled by {@link ModularGuiContext}. + * If it detects a slot, this method is called. Only affects {@link AbstractContainerScreen + * AbstractContainerScreens}. + * + * @param slot hovered slot + */ + @ApiStatus.NonExtendable + default void setHoveredSlot(Slot slot) { + if (getWrappedScreen() instanceof AbstractContainerScreenAccessor acc) { + acc.setHoveredSlot(slot); + } + } + + /** + * Returns the {@link Screen} that wraps the {@link ModularScreen}. + * In most cases this does not need to be overridden as this interfaces should be implemented on {@link Screen + * Screens}. + * + * @return the wrapping gui screen + */ + default Screen getWrappedScreen() { + return (Screen) this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPacketWriter.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPacketWriter.java new file mode 100644 index 00000000000..d1e92c68f13 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPacketWriter.java @@ -0,0 +1,24 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import net.minecraft.network.FriendlyByteBuf; + +import io.netty.buffer.Unpooled; + +/** + * A function that can write any data to an {@link FriendlyByteBuf}. + */ +public interface IPacketWriter { + + /** + * Writes any data to a packet buffer + * + * @param buffer buffer to write to + */ + void write(FriendlyByteBuf buffer); + + default FriendlyByteBuf toPacket() { + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + write(buffer); + return buffer; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java new file mode 100644 index 00000000000..fa650b94a0a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java @@ -0,0 +1,103 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.value.sync.ItemSlotSyncHandler; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncHandler; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.SecondaryPanel; + +import org.jetbrains.annotations.ApiStatus; + +/** + * This class can handle opening and closing of a {@link ModularPanel}. It makes sure, that the same panel is not + * created multiple + * times and instead reused. + *

+ * Using {@link #openPanel()} is the only way to open multiple panels. + *

+ *

+ * Panels can be closed with {@link #closePanel()}, but also with {@link ModularPanel#closeIfOpen()}. + * With the difference, that the method from this interface also works on server + * side. + *

+ * Synced panels must be created with {@link PanelSyncManager#panel(String, PanelSyncHandler.IPanelBuilder, boolean)}. + * If the panel does not contain any synced widgets, a simple panel handler using + * {@link #simple(ModularPanel, SecondaryPanel.IPanelBuilder, boolean)} + * is likely what you need. + */ +@ApiStatus.NonExtendable +public interface IPanelHandler { + + /** + * Creates a non synced panel handler. Trying to use synced values anyway will result in a crash. + * It only works on client side. Doing anything with it on server side might result in a crash. + * + * @param parent an existing parent panel of the gui + * @param provider the panel builder, that will create the new panel. It must not return null or the main panel. + * @param subPanel true if this panel should close when its parent closes (the parent is defined by the first + * parameter) + * @return a simple panel handler. + * @throws NullPointerException if the build panel of the builder is null + * @throws IllegalArgumentException if the build panel of the builder is the main panel or there are synced values + * in the panel + */ + static IPanelHandler simple(ModularPanel parent, SecondaryPanel.IPanelBuilder provider, boolean subPanel) { + return new SecondaryPanel(parent, provider, subPanel); + } + + boolean isPanelOpen(); + + /** + * Opens the panel. If there is no cached panel, one will be created. + * Can be called on both sides if this handler is synced. + */ + void openPanel(); + + /** + * Initiates the closing animation if the panel is open. + * Can be called on both sides if this handler is synced. + */ + void closePanel(); + + /** + * Initiates the closing animation of all sub panels. + * Usually for internal use. + */ + void closeSubPanels(); + + /** + * Called internally after the panel is closed. + */ + @ApiStatus.OverrideOnly + void closePanelInternal(); + + /** + * Toggles this panel open or closed. Delegates to {@link #openPanel()} and {@link #closePanel()}. + * + * @return {@code true} if the panel was opened, {@code false} if it was closed + */ + default boolean togglePanel() { + if (isPanelOpen()) { + closePanel(); + return false; + } else { + openPanel(); + return true; + } + } + + /** + * Deletes the current cached panel. Should not be used frequently. + * This only works on panels which don't have {@link ItemSlotSyncHandler} sync handlers. + * + * @throws UnsupportedOperationException if this handler has ItemSlot sync handlers + */ + void deleteCachedPanel(); + + /** + * If this is a sub panel of another panel. A sub panel will be closed when its parent is closed. + * + * @return true if this is a sub panel + */ + boolean isSubPanel(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/ISyncedAction.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/ISyncedAction.java new file mode 100644 index 00000000000..2ded1a87fb7 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/ISyncedAction.java @@ -0,0 +1,13 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import net.minecraft.network.FriendlyByteBuf; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@FunctionalInterface +public interface ISyncedAction { + + @ApiStatus.OverrideOnly + void invoke(@NotNull FriendlyByteBuf packet); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/ITheme.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/ITheme.java new file mode 100644 index 00000000000..cf2b4c135a7 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/ITheme.java @@ -0,0 +1,59 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.theme.*; + +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collection; + +/** + * A theme is parsed from json and contains style information like color or background texture. + */ +public interface ITheme { + + /** + * @return the master default theme. + */ + static ITheme getDefault() { + return IThemeApi.get().getDefaultTheme(); + } + + /** + * @param id theme id + * @return theme with given id + */ + static ITheme get(String id) { + return IThemeApi.get().getTheme(id); + } + + /** + * @return theme id + */ + String getId(); + + /** + * @return parent theme + */ + ITheme getParentTheme(); + + @UnmodifiableView + Collection> getWidgetThemes(); + + WidgetThemeEntry getFallback(); + + WidgetThemeEntry getPanelTheme(); + + WidgetThemeEntry getButtonTheme(); + + WidgetThemeEntry getScrollbarTheme(); + + WidgetThemeEntry getItemSlotTheme(); + + WidgetThemeEntry getFluidSlotTheme(); + + WidgetThemeEntry getTextFieldTheme(); + + WidgetThemeEntry getToggleButtonTheme(); + + WidgetThemeEntry getWidgetTheme(WidgetThemeKey key); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java new file mode 100644 index 00000000000..039e3c3fd89 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java @@ -0,0 +1,203 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.drawable.Scrollbar; +import com.gregtechceu.gtceu.api.mui.theme.*; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.common.mui.GTGuiTextures; +import com.gregtechceu.gtceu.utils.serialization.json.JsonBuilder; + +import org.jetbrains.annotations.*; + +import java.util.List; + +/** + * An API interface for Themes. + */ +@ApiStatus.NonExtendable +public interface IThemeApi { + + // widget themes + WidgetThemeKey FALLBACK = get().widgetThemeKeyBuilder("default", WidgetTheme.class) + .defaultTheme(WidgetTheme.darkTextNoShadow(18, 18, null)) + .register(); + + WidgetThemeKey PANEL = get().widgetThemeKeyBuilder("panel", WidgetTheme.class) + .defaultTheme(WidgetTheme.darkTextNoShadow(176, 166, GTGuiTextures.BACKGROUND)) + .register(); + + WidgetThemeKey BUTTON = get().widgetThemeKeyBuilder("button", WidgetTheme.class) + .defaultTheme(WidgetTheme.whiteTextShadow(18, 18, GTGuiTextures.MC_BUTTON)) + .defaultHoverTheme(WidgetTheme.whiteTextShadow(18, 18, GTGuiTextures.MC_BUTTON_HOVERED)) + .register(); + + WidgetThemeKey CLOSE_BUTTON = get().widgetThemeKeyBuilder("closeButton", WidgetTheme.class) + .defaultTheme(WidgetTheme.whiteTextShadow(10, 10, GTGuiTextures.MC_BUTTON)) + .defaultHoverTheme(WidgetTheme.whiteTextShadow(10, 10, GTGuiTextures.MC_BUTTON_HOVERED)) + .register(); + + WidgetThemeKey SCROLLBAR = get().widgetThemeKeyBuilder("scrollbar", WidgetTheme.class) + .defaultTheme(WidgetTheme.darkTextNoShadow(4, 4, Scrollbar.VANILLA)) + .register(); + + WidgetThemeKey ITEM_SLOT = get().widgetThemeKeyBuilder("itemSlot", SlotTheme.class) + .defaultTheme(new SlotTheme(GTGuiTextures.SLOT)) + .register(); + + WidgetThemeKey FLUID_SLOT = get().widgetThemeKeyBuilder("fluidSlot", SlotTheme.class) + .defaultTheme(new SlotTheme(GTGuiTextures.FLUID_SLOT)) + .register(); + + WidgetThemeKey TEXT_FIELD = get().widgetThemeKeyBuilder("textField", TextFieldTheme.class) + .defaultTheme(new TextFieldTheme(0xFF2F72A8, 0xFF5F5F5F)) + .register(); + + WidgetThemeKey TOGGLE_BUTTON = get().widgetThemeKeyBuilder("toggleButton", SelectableTheme.class) + .defaultTheme( + SelectableTheme.whiteTextShadow(18, 18, GTGuiTextures.MC_BUTTON, GTGuiTextures.MC_BUTTON_DISABLED)) + .defaultHoverTheme(SelectableTheme.whiteTextShadow(18, 18, GTGuiTextures.MC_BUTTON_HOVERED, + IDrawable.NONE)) + .register(); + + // subwidget themes + WidgetThemeKey ITEM_SLOT_PLAYER = ITEM_SLOT.createSubKey("player"); + WidgetThemeKey ITEM_SLOT_PLAYER_HOTBAR = ITEM_SLOT_PLAYER.createSubKey("playerHotbar"); + WidgetThemeKey ITEM_SLOT_PLAYER_MAIN_INV = ITEM_SLOT_PLAYER.createSubKey("playerMainInventory"); + WidgetThemeKey ITEM_SLOT_PLAYER_OFFHAND = ITEM_SLOT_PLAYER.createSubKey("playerOffhand"); + WidgetThemeKey ITEM_SLOT_PLAYER_ARMOR = ITEM_SLOT_PLAYER.createSubKey("playerArmor"); + + String HOVER_SUFFIX = ":hover"; + + // properties + String PARENT = "parent"; + String DEFAULT_WIDTH = "defaultWidth"; + String DEFAULT_HEIGHT = "defaultHeight"; + String BACKGROUND = "background"; + String HOVER_BACKGROUND = "hoverBackground"; + String COLOR = "color"; + String TEXT_COLOR = "textColor"; + String TEXT_SHADOW = "textShadow"; + String ICON_COLOR = "iconColor"; + String SLOT_HOVER_COLOR = "slotHoverColor"; + String MARKED_COLOR = "markedColor"; + String HINT_COLOR = "hintColor"; + String SELECTED_BACKGROUND = "selectedBackground"; + String SELECTED_COLOR = "selectedColor"; + String SELECTED_TEXT_COLOR = "selectedTextColor"; + String SELECTED_TEXT_SHADOW = "selectedTextShadow"; + String SELECTED_ICON_COLOR = "selectedIconColor"; + + /** + * @return the default api implementation + */ + @Contract(pure = true) + static IThemeApi get() { + return ThemeAPI.INSTANCE; + } + + /** + * @return the absolute fallback theme + */ + ITheme getDefaultTheme(); + + /** + * Finds a theme for an id + * + * @param id id of the theme + * @return the found theme or {@link #getDefaultTheme()} if no theme was found + */ + @NotNull + ITheme getTheme(String id); + + /** + * @param id id of the theme + * @return if a theme with the id is registered + */ + boolean hasTheme(String id); + + /** + * Registers a theme json object. Themes from resource packs always have greater priority. + * Json builders are used here as they are much easier to merge as opposed to normal java objects. + * + * @param id id of the theme + * @param json theme data + */ + void registerTheme(String id, JsonBuilder json); + + /** + * Registers a theme json object. Themes from resource packs always have greater priority. + * + * @param themeBuilder theme data + */ + default void registerTheme(ThemeBuilder themeBuilder) { + registerTheme(themeBuilder.getId(), themeBuilder); + } + + /** + * Gets all currently from java side registered theme json's for a theme. + * + * @param id id of the theme + * @return all theme json's for a theme. + */ + List getJavaDefaultThemes(String id); + + /** + * Gets the appropriate theme for a screen. + * + * @param owner owner of the screen + * @param name name of the screen + * @param defaultTheme default theme if no theme was found + * @return the registered theme for the given screen or the given default theme or {@link #getDefaultTheme()} + */ + ITheme getThemeForScreen(String owner, String name, @Nullable String defaultTheme); + + /** + * Gets the appropriate theme for a screen. + * + * @param screen screen + * @param defaultTheme default theme if no theme was found + * @return the registered theme for the given screen or the given default theme or {@link #getDefaultTheme()} + */ + default ITheme getThemeForScreen(ModularScreen screen, @Nullable String defaultTheme) { + return getThemeForScreen(screen.getOwner(), screen.getName(), defaultTheme); + } + + /** + * Registers a theme for a screen. Themes from resource packs always have greater priority. + * + * @param owner owner of the screen + * @param name name of the screen + * @param theme theme to register + */ + default void registerThemeForScreen(String owner, String name, String theme) { + registerThemeForScreen(owner + ":" + name, theme); + } + + /** + * Registers a theme for a screen. Themes from resource packs always have greater priority. + * + * @param screen full screen id + * @param theme theme to register + */ + void registerThemeForScreen(String screen, String theme); + + /** + * Registers a widget theme. It is recommended to store the resulting key in a static variable and make it + * accessible by public methods. + * + * @param id id of the widget theme + * @param defaultTheme the fallback widget theme + * @param defaultHoverTheme the fallback hover widget theme + * @param parser the widget theme json parser function. This is usually another constructor. + * @return key to access the widget theme + */ + WidgetThemeKey registerWidgetTheme(String id, T defaultTheme, T defaultHoverTheme, + WidgetThemeParser parser); + + default WidgetThemeKeyBuilder widgetThemeKeyBuilder(String id, Class type) { + return new WidgetThemeKeyBuilder<>(id); + } + + @UnmodifiableView + List> getWidgetThemeKeys(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java new file mode 100644 index 00000000000..58a9ca3923e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java @@ -0,0 +1,43 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.factory.GuiData; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.client.mui.screen.UISettings; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +/** + * An interface to implement on {@link net.minecraft.world.level.block.entity.BlockEntity} or + * {@link net.minecraft.world.item.Item}. + */ +@FunctionalInterface +public interface IUIHolder { + + /** + * Only called on client side. + * + * @param data information about the creation context + * @param mainPanel the panel created in {@link #buildUI(GuiData, PanelSyncManager, UISettings)} + * @return a modular screen instance with the given panel + */ + @OnlyIn(Dist.CLIENT) + default ModularScreen createScreen(T data, ModularPanel mainPanel) { + return new ModularScreen(mainPanel); + } + + /** + * Called on server and client. Create only the main panel here. Only here you can add sync handlers to widgets + * directly. + * If the widget to be synced is not in this panel yet (f.e. in another panel) the sync handler must be registered + * here + * with {@link PanelSyncManager}. + * + * @param data information about the creation context + * @param syncManager sync handler where widget sync handlers should be registered + * @param settings settings which apply to the whole ui and not just this panel + */ + ModularPanel buildUI(T data, PanelSyncManager syncManager, UISettings settings); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/MCHelper.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/MCHelper.java new file mode 100644 index 00000000000..e6ef11fb86e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/MCHelper.java @@ -0,0 +1,74 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.common.network.ModularNetwork; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import java.util.List; + +public class MCHelper { + + @SideOnly(Side.CLIENT) + public static Minecraft getMc() { + return Minecraft.getInstance(); + } + + @SideOnly(Side.CLIENT) + public static Player getPlayer() { + return getMc().player; + } + + @SideOnly(Side.CLIENT) + public static boolean closeScreen() { + getMc().popGuiLayer(); + return false; + } + + @SideOnly(Side.CLIENT) + public static void popScreen(boolean openParentOnClose, Screen parent) { + Player player = MCHelper.getPlayer(); + if (player != null) { + // container should not just be closed here + // instead they are kept in a stack until all screens are closed + // prepareCloseContainer(player); + if (openParentOnClose) { + Minecraft.getInstance().setScreen(parent); + ModularNetwork.CLIENT.reopenSyncerOf(parent); + } else { + Minecraft.getInstance().setScreen(null); + } + } else { + // we are currently not in a world and want to display the previous screen + Minecraft.getInstance().setScreen(parent); + } + } + + public static void setScreen(Screen screen) { + if (screen == null) { + closeScreen(); + } else { + getMc().setScreen(screen); + } + } + + @SideOnly(Side.CLIENT) + public static Screen getCurrentScreen() { + return getMc().screen; + } + + @SideOnly(Side.CLIENT) + public static Font getFont() { + return getMc().font; + } + + public static List getItemToolTip(ItemStack item) { + return Screen.getTooltipFromItem(getMc(), item); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java new file mode 100644 index 00000000000..0d58fc06be3 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java @@ -0,0 +1,114 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.factory.GuiData; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.client.mui.screen.*; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * An interface for UI factories. They are responsible for opening synced GUIs and syncing necessary data. + * + * @param gui data type + */ +public interface UIFactory { + + /** + * The name of this factory. Must be constant. + * + * @return the factory name + */ + @NotNull + ResourceLocation getFactoryName(); + + /** + * Creates the main panel for the GUI. Is called on client and server side. + * + * @param guiData gui data + * @param syncManager sync manager + * @param settings ui settings + * @return new main panel + */ + @ApiStatus.OverrideOnly + ModularPanel createPanel(D guiData, PanelSyncManager syncManager, UISettings settings); + + /** + * Creates the screen for the GUI. Is only called on client side. + * + * @param guiData gui data + * @param mainPanel main panel created in {@link #createPanel(GuiData, PanelSyncManager, UISettings)} + * @return new main panel + */ + @OnlyIn(Dist.CLIENT) + @ApiStatus.OverrideOnly + ModularScreen createScreen(D guiData, ModularPanel mainPanel); + + /** + * Creates the screen wrapper for the GUI. Is only called on client side. + * + * @param container container for the gui + * @param screen the screen which was created in {@link #createScreen(GuiData, ModularPanel)} + * @return new screen wrapper + * @throws IllegalStateException if the wrapping screen is not a + * {@link AbstractContainerMenu AbstractContainerMenu} + * or if the container inside is not the same as the one passed to this method. + * This method is not the thrower, but the caller of this method. + */ + @OnlyIn(Dist.CLIENT) + @ApiStatus.OverrideOnly + default IMuiScreen createScreenWrapper(ModularContainerMenu container, ModularScreen screen) { + return new ContainerScreenWrapper(container, screen); + } + + /** + * The default container supplier. This is called when no custom container in {@link UISettings} is set. + * + * @return new container instance + */ + default ModularContainerMenu createContainer(int containerId) { + return new ModularContainerMenu(containerId); + } + + /** + * A default function to check if the current interacting player can interact with the ui. If not overridden on + * {@link UISettings}, + * then this is called every tick while a UI opened by this factory is open. Once this function returns false, the + * UI is immediately + * closed. + * + * @param player current interacting player + * @param guiData gui data of the current ui + * @return if the player can interact with the player. + */ + default boolean canInteractWith(Player player, D guiData) { + return player == guiData.getPlayer(); + } + + /** + * Writes the gui data to a buffer. + * + * @param guiData gui data + * @param buffer buffer + */ + @ApiStatus.OverrideOnly + void writeGuiData(D guiData, FriendlyByteBuf buffer); + + /** + * Reads and creates the gui data from the buffer. + * + * @param player player + * @param buffer buffer + * @return new gui data + */ + @NotNull + @ApiStatus.OverrideOnly + D readGuiData(Player player, FriendlyByteBuf buffer); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java new file mode 100644 index 00000000000..cc9c4d19c12 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java @@ -0,0 +1,122 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.utils.Rectangle; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.integration.xei.handlers.GhostIngredientSlot; + +import org.jetbrains.annotations.ApiStatus; + +/** + * Keeps track of everything related to JEI in a Modular GUI. + * By default, JEI is disabled in client only GUIs. + * This class can be safely interacted with even when JEI/HEI is not installed. + */ +@ApiStatus.NonExtendable +public interface XeiSettings { + + /** + * Force XEI to be enabled + */ + void forceEnabled(); + + /** + * Force XEI to be disabled + */ + void forceDisabled(); + + /** + * Only enabled XEI in synced GUIs + */ + void defaultXei(); + + /** + * Checks if XEI is enabled for a given screen + * + * @param screen modular screen + * @return true if xei is enabled + */ + boolean isEnabled(ModularScreen screen); + + /** + * Adds an exclusion zone. XEI will always try to avoid exclusion zones.
+ * If a widgets wishes to have an exclusion zone it should use {@link #addExclusionArea(IWidget)}! + * + * @param area exclusion area + */ + void addExclusionArea(Rectangle area); + + /** + * Removes an exclusion zone. + * + * @param area exclusion area to remove (must be the same instance) + */ + void removeExclusionArea(Rectangle area); + + /** + * Adds an exclusion zone of a widget. XEI will always try to avoid exclusion zones.
+ * Useful when a widget is outside its panel. + * + * @param area widget + */ + void addExclusionArea(IWidget area); + + /** + * Removes a widget exclusion area. + * + * @param area widget + */ + void removeExclusionArea(IWidget area); + + /** + * Adds a XEI ghost slot. Ghost slots can display an ingredient, but the ingredient does not really exist. + * By calling this method users will be able to drag ingredients from JEI into the slot. + * + * @param slot slot widget + * @param slot widget type + */ + > void addGhostIngredientSlot(W slot); + + /** + * Removes a XEI ghost slot. + * + * @param slot slot widget + * @param slot widget type + */ + > void removeGhostIngredientSlot(W slot); + + XeiSettings DUMMY = new XeiSettings() { + + @Override + public void forceEnabled() {} + + @Override + public void forceDisabled() {} + + @Override + public void defaultXei() {} + + @Override + public boolean isEnabled(ModularScreen screen) { + return false; + } + + @Override + public void addExclusionArea(Rectangle area) {} + + @Override + public void removeExclusionArea(Rectangle area) {} + + @Override + public void addExclusionArea(IWidget area) {} + + @Override + public void removeExclusionArea(IWidget area) {} + + @Override + public > void addGhostIngredientSlot(W slot) {} + + @Override + public > void removeGhostIngredientSlot(W slot) {} + }; +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java new file mode 100644 index 00000000000..772e0e6141f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java @@ -0,0 +1,197 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.api.mui.drawable.DrawableStack; +import com.gregtechceu.gtceu.api.mui.drawable.Icon; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.theme.WidgetThemeEntry; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import org.jetbrains.annotations.Nullable; + +/** + * An object which can be drawn at any size. This is mainly used for backgrounds and overlays in + * {@link com.gregtechceu.gtceu.api.mui.base.widget.IWidget}. + * To draw at a fixed size, use {@link IIcon} (see {@link #asIcon()}). + */ +public interface IDrawable { + + static IDrawable of(IDrawable... drawables) { + if (drawables == null || drawables.length == 0) { + return null; + } else if (drawables.length == 1) { + return drawables[0]; + } else { + return new DrawableStack(drawables); + } + } + + /** + * Draws this drawable at the given position with the given size. It's the implementors responsibility to properly + * apply the widget theme by calling {@link #applyColor(int)} before drawing. + * + * @param context current context to draw with + * @param x x position + * @param y y position + * @param width draw width + * @param height draw height + * @param widgetTheme current theme + */ + @OnlyIn(Dist.CLIENT) + void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme); + + /** + * Draws this drawable at the current (0|0) with the given size. This is useful inside widgets since GL is + * transformed to their position when they are drawing. + * + * @param context gui context + * @param width draw width + * @param height draw height + * @param widgetTheme current theme + */ + @OnlyIn(Dist.CLIENT) + default void drawAtZero(GuiContext context, int width, int height, WidgetTheme widgetTheme) { + draw(context, 0, 0, width, height, widgetTheme); + } + + /** + * Draws this drawable in a given area. The padding of the area is not applied here. + * + * @param context current context to draw with + * @param area draw area + * @param widgetTheme current theme + */ + @OnlyIn(Dist.CLIENT) + default void draw(GuiContext context, Area area, WidgetTheme widgetTheme) { + draw(context, area.x, area.y, area.width, + area.height, widgetTheme); + } + + /** + * Draws this drawable in a given area with its padding applied. + * + * @param context current context to draw with + * @param area draw area + * @param widgetTheme current theme + */ + @OnlyIn(Dist.CLIENT) + default void drawPadded(GuiContext context, Area area, WidgetTheme widgetTheme) { + draw(context, area.x + area.getPadding().left(), area.y + area.getPadding().top(), + area.paddedWidth(), area.paddedHeight(), widgetTheme); + } + + /** + * Draws this drawable at the current (0|0) with the given area's size. This is useful inside widgets since GL is + * transformed to their position when they are drawing. The padding of the area is not applied here. + * + * @param context gui context + * @param area draw area + * @param widgetTheme current theme + */ + @OnlyIn(Dist.CLIENT) + default void drawAtZero(GuiContext context, Area area, WidgetTheme widgetTheme) { + draw(context, 0, 0, area.width, area.height, widgetTheme); + } + + /** + * Draws this drawable at the current (0|0) with the given area's size and its padding applied + * (this means its technically not at 0|0). This is useful inside widgets since GL is transformed to their position + * when they are drawing. + * + * @param context gui context + * @param area draw area + * @param widgetTheme current theme + */ + @OnlyIn(Dist.CLIENT) + default void drawAtZeroPadded(GuiContext context, Area area, WidgetTheme widgetTheme) { + draw(context, area.getPadding().left(), area.getPadding().top(), area.paddedWidth(), area.paddedHeight(), + widgetTheme); + } + + /** + * @return if theme color can be applied on this drawable + */ + default boolean canApplyTheme() { + return false; + } + + /** + * Applies the theme color to OpenGL if this drawable can have theme colors applied. This is determined by + * {@link #canApplyTheme()}. + * If this drawable does not allow theme colors, it will reset the current color (to white). + * This method should be called before drawing. + * + * @param themeColor theme color to apply (usually {@link WidgetTheme#getColor()}) + */ + default void applyColor(int themeColor) { + if (canApplyTheme()) { + Color.setGlColor(themeColor); + } else { + Color.setGlColorOpaque(Color.WHITE.main); + } + } + + default int getDefaultWidth() { + return 0; + } + + default int getDefaultHeight() { + return 0; + } + + /** + * @return a widget with this drawable as a background + */ + default Widget asWidget() { + return new DrawableWidget(this); + } + + /** + * @return this drawable as an icon + */ + default Icon asIcon() { + return new Icon(this).size(getDefaultWidth(), getDefaultHeight()); + } + + /** + * An empty drawable. Does nothing. + */ + IDrawable EMPTY = (context, x, y, width, height, widgetTheme) -> {}; + + /** + * An empty drawable used to mark hover textures as "should not be used"! + */ + IDrawable NONE = (context, x, y, width, height, widgetTheme) -> {}; + + static boolean isVisible(@Nullable IDrawable drawable) { + if (drawable == null || drawable == EMPTY || drawable == NONE) return false; + if (drawable instanceof DrawableStack array) { + return array.getDrawables().length > 0; + } + return true; + } + + /** + * A widget wrapping a drawable. The drawable is drawn between the background and the overlay. + */ + class DrawableWidget extends Widget { + + private final IDrawable drawable; + + public DrawableWidget(IDrawable drawable) { + this.drawable = drawable; + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme) { + this.drawable.drawAtZero(context, getArea(), getActiveWidgetTheme(widgetTheme, isHovering())); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java new file mode 100644 index 00000000000..fdca45a2433 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java @@ -0,0 +1,38 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.api.mui.drawable.text.RichText; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +/** + * This marks an {@link IDrawable} as hoverable in a {@link RichText RichText}. This should not be + * extended in most cases instead obtain an instance by calling {@link IIcon#asHoverable()}. + */ +@ApiStatus.NonExtendable +public interface IHoverable extends IIcon { + + /** + * Called every frame this hoverable is hovered inside a + * {@link com.gregtechceu.gtceu.api.mui.drawable.text.RichText RichText}. + */ + default void onHover() {} + + @Nullable + default RichTooltip getTooltip() { + return null; + } + + /** + * An internal function to set the current rendered position. This is used to detect if this element is under the + * mouse. + */ + void setRenderedAt(int x, int y); + + /** + * @return the last area this drawable was drawn at. + */ + Area getRenderedArea(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IIcon.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IIcon.java new file mode 100644 index 00000000000..23035e1750c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IIcon.java @@ -0,0 +1,78 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.drawable.HoverableIcon; +import com.gregtechceu.gtceu.api.mui.drawable.InteractableIcon; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Box; + +import org.jetbrains.annotations.Nullable; + +/** + * A {@link IDrawable} with a fixed size. + */ +public interface IIcon extends IDrawable { + + /** + * @return the drawable this icon wraps or null if it doesn't wrap anything + */ + @Nullable + IDrawable getWrappedDrawable(); + + /** + * @return width of this icon or 0 if the width should be dynamic + */ + int getWidth(); + + /** + * @return height of this icon or 0 of the height should be dynamic + */ + int getHeight(); + + default int getSize(GuiAxis axis) { + return axis.isHorizontal() ? getWidth() : getHeight(); + } + + @Override + default int getDefaultWidth() { + return getWrappedDrawable() != null ? getWrappedDrawable().getDefaultWidth() : 0; + } + + @Override + default int getDefaultHeight() { + return getWrappedDrawable() != null ? getWrappedDrawable().getDefaultHeight() : 0; + } + + /** + * @return the margin of this icon. Only used if width or height is 0 + */ + Box getMargin(); + + default IDrawable getRootDrawable() { + IDrawable drawable = this; + while (drawable instanceof IIcon icon) { + drawable = icon.getWrappedDrawable(); + if (drawable == null) return icon; + } + return drawable; + } + + /** + * This returns a hoverable wrapper of this icon. This is only used in + * {@link com.gregtechceu.gtceu.api.mui.drawable.text.RichText RichText}. + * This allows this icon to have its own tooltip. + */ + default HoverableIcon asHoverable() { + return new HoverableIcon(this); + } + + /** + * This returns an interactable wrapper of this icon. This is only used in + * {@link com.gregtechceu.gtceu.api.mui.drawable.text.RichText RichText}. + * This allows this icon to be able to listen to clicks and other inputs. + */ + default InteractableIcon asInteractable() { + return new InteractableIcon(this); + } + + IIcon EMPTY_2PX = EMPTY.asIcon().height(2); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IInterpolation.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IInterpolation.java new file mode 100644 index 00000000000..d008d1ccc81 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IInterpolation.java @@ -0,0 +1,17 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +/** + * A function which interpolates between two values. + */ +public interface IInterpolation { + + /** + * Calculates a new value between a and b based on a curve. + * + * @param a start value + * @param b end value + * @param x progress (between 0.0 and 1.0) + * @return new value + */ + float interpolate(float a, float b, float x); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java new file mode 100644 index 00000000000..f30597cbc51 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java @@ -0,0 +1,329 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.drawable.text.*; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widgets.TextWidget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +/** + * This represents a piece of text in a GUI. + */ +public interface IKey extends IDrawable, IJsonSerializable { + + int TEXT_COLOR = 0xFF404040; + + TextRenderer renderer = new TextRenderer(); + + IKey EMPTY = str(""); + IKey LINE_FEED = str("\n"); + IKey SPACE = str(" "); + + // Formatting for convenience + ChatFormatting BLACK = ChatFormatting.BLACK; + ChatFormatting DARK_BLUE = ChatFormatting.DARK_BLUE; + ChatFormatting DARK_GREEN = ChatFormatting.DARK_GREEN; + ChatFormatting DARK_AQUA = ChatFormatting.DARK_AQUA; + ChatFormatting DARK_RED = ChatFormatting.DARK_RED; + ChatFormatting DARK_PURPLE = ChatFormatting.DARK_PURPLE; + ChatFormatting GOLD = ChatFormatting.GOLD; + ChatFormatting GRAY = ChatFormatting.GRAY; + ChatFormatting DARK_GRAY = ChatFormatting.DARK_GRAY; + ChatFormatting BLUE = ChatFormatting.BLUE; + ChatFormatting GREEN = ChatFormatting.GREEN; + ChatFormatting AQUA = ChatFormatting.AQUA; + ChatFormatting RED = ChatFormatting.RED; + ChatFormatting LIGHT_PURPLE = ChatFormatting.LIGHT_PURPLE; + ChatFormatting YELLOW = ChatFormatting.YELLOW; + ChatFormatting WHITE = ChatFormatting.WHITE; + ChatFormatting OBFUSCATED = ChatFormatting.OBFUSCATED; + ChatFormatting BOLD = ChatFormatting.BOLD; + ChatFormatting STRIKETHROUGH = ChatFormatting.STRIKETHROUGH; + ChatFormatting UNDERLINE = ChatFormatting.UNDERLINE; + ChatFormatting ITALIC = ChatFormatting.ITALIC; + ChatFormatting RESET = ChatFormatting.RESET; + + /** + * Creates a translated text. + * + * @param key translation key + * @return text key + */ + static IKey lang(@NotNull String key) { + return new LangKey(key); + } + + /** + * Creates a translated text. + * + * @param component translation component + * @return text key + */ + static IKey lang(@NotNull Component component) { + return new LangKey(component); + } + + /** + * Creates a translated text with arguments. The arguments can change. + * + * @param key translation key + * @param args translation arguments + * @return text key + */ + static IKey lang(@NotNull String key, @Nullable Object... args) { + return new LangKey(key, args); + } + + /** + * Creates a translated text with arguments supplier. + * + * @param key translation key + * @param argsSupplier translation arguments supplier + * @return text key + */ + static IKey lang(@NotNull String key, @NotNull Supplier argsSupplier) { + return new LangKey(key, argsSupplier); + } + + /** + * Creates a translated text. + * + * @param keySupplier translation key supplier + * @return text key + */ + static IKey lang(@NotNull Supplier keySupplier) { + return new LangKey(keySupplier); + } + + /** + * Creates a translated text with arguments supplier. + * + * @param keySupplier translation key supplier + * @param argsSupplier translation arguments supplier + * @return text key + */ + static IKey lang(@NotNull Supplier keySupplier, @NotNull Supplier argsSupplier) { + return new LangKey(keySupplier, argsSupplier); + } + + /** + * Creates a string literal text. + * + * @param key string + * @return text key + */ + static IKey str(@NotNull String key) { + return new StringKey(key); + } + + /** + * Creates a formatted string literal text with arguments. The arguments can be dynamic. + * The string is formatted using {@link String#format(String, Object...)}. + * + * @param key string + * @param args arguments + * @return text key + */ + static IKey str(@NotNull String key, @Nullable Object... args) { + return new StringKey(key, args); + } + + /** + * Creates a composed text key. + * + * @param keys text keys + * @return composed text key. + */ + static IKey comp(@NotNull IKey... keys) { + return new CompoundKey(keys); + } + + /** + * Creates a dynamic text key. + * + * @param supp string supplier + * @return dynamic text key + */ + static IKey dynamic(@NotNull Supplier<@NotNull Component> supp) { + // DO NOT PULL OUT INTO A LOCAL VAR IT WILL BREAK THE SUPPLIER + if (supp.get() instanceof MutableComponent) { + return dynamicKey(() -> IKey.lang(supp.get())); + } else { + return dynamicKey(() -> IKey.lang(supp.get().copy())); + } + } + + /** + * Creates a dynamic text key. + * + * @param supp key supplier + * @return dynamic text key + */ + static IKey dynamicKey(@NotNull Supplier<@NotNull IKey> supp) { + return new DynamicKey(supp); + } + + /** + * @return the current unformatted string + */ + MutableComponent get(); + + /** + * @param parentFormatting formatting of the parent in case of composite keys + * @return the current formatted string + */ + default MutableComponent getFormatted(@Nullable FormattingState parentFormatting) { + return get(); + } + + /** + * @return the current formatted string + */ + default MutableComponent getFormatted() { + return getFormatted(null); + } + + @OnlyIn(Dist.CLIENT) + @Override + default void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + drawAligned(context, x, y, width, height, widgetTheme, Alignment.CENTER); + } + + @OnlyIn(Dist.CLIENT) + default void drawAligned(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme, + Alignment alignment) { + renderer.setColor(widgetTheme.getTextColor()); + renderer.setShadow(widgetTheme.isTextShadow()); + renderer.setAlignment(alignment, width, height); + renderer.setScale(getScale()); + renderer.setPos(x, y); + renderer.draw(context.getGraphics(), getFormatted()); + } + + @Override + default boolean canApplyTheme() { + return true; + } + + @Override + default int getDefaultWidth() { + renderer.setAlignment(Alignment.TopLeft, -1, -1); + renderer.setScale(getScale()); + renderer.setPos(0, 0); + renderer.setSimulate(true); + renderer.draw(null, getFormatted()); + renderer.setSimulate(false); + return (int) renderer.getLastWidth(); + } + + @Override + default int getDefaultHeight() { + renderer.setAlignment(Alignment.TopLeft, -1, -1); + renderer.setScale(getScale()); + renderer.setPos(0, 0); + renderer.setSimulate(true); + renderer.draw(null, getFormatted()); + renderer.setSimulate(false); + return (int) renderer.getLastWidth(); + } + + default float getScale() { + return 1f; + } + + @Override + default TextWidget asWidget() { + return new TextWidget<>(this); + } + + default StyledText withStyle() { + return new StyledText(this); + } + + default AnimatedText withAnimation() { + return new AnimatedText(this); + } + + /** + * @return a formatting state of this key + */ + default @Nullable FormattingState getFormatting() { + return null; + } + + /** + * Set text formatting to this key. If {@link IKey#RESET} is used, then that's applied first and then all other + * formatting of this key. + * With {@code null}, you can remove a color formatting. No matter the parents color, the default color will be + * used. + * + * @param formatting a formatting rule + * @return this + */ + IKey style(@Nullable ChatFormatting formatting); + + default IKey style(ChatFormatting... formatting) { + for (ChatFormatting cf : formatting) style(cf); + return this; + } + + default IKey removeFormatColor() { + return style((ChatFormatting) null); + } + + IKey removeStyle(); + + default StyledText alignment(Alignment alignment) { + return withStyle().alignment(alignment); + } + + default StyledText color(int color) { + return color(() -> color); + } + + default StyledText color(@Nullable IntSupplier color) { + return withStyle().color(color); + } + + default StyledText scale(float scale) { + return withStyle().scale(scale); + } + + default StyledText shadow(@Nullable Boolean shadow) { + return withStyle().shadow(shadow); + } + + default KeyIcon asTextIcon() { + return new KeyIcon(this); + } + + @Override + default void loadFromJson(JsonObject json) { + if (json.has("color") || json.has("shadow") || json.has("align") || json.has("alignment") || + json.has("scale")) { + StyledText styledText = this instanceof StyledText styledText1 ? styledText1 : withStyle(); + if (json.has("color")) { + styledText.color(JsonHelper.getInt(json, 0, "color")); + } + styledText.shadow(JsonHelper.getBoolean(json, false, "shadow")); + styledText.alignment( + JsonHelper.deserialize(json, Alignment.class, styledText.getAlignment(), "align", "alignment")); + styledText.scale(JsonHelper.getFloat(json, 1, "scale")); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java new file mode 100644 index 00000000000..296ec7eb037 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java @@ -0,0 +1,390 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.api.mui.drawable.text.Spacer; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.data.lang.LangHandler; + +import net.minecraft.network.chat.FormattedText; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.world.inventory.tooltip.TooltipComponent; + +import java.util.function.UnaryOperator; +import java.util.regex.Pattern; + +public interface IRichTextBuilder> { + + T getThis(); + + IRichTextBuilder getRichText(); + + /** + * Adds a component to the current line + * + * @param c component to add + * @return this + */ + default T add(FormattedText c) { + getRichText().add(c); + return getThis(); + } + + /** + * Adds a string to the current line + * + * @param s string to add + * @return this + */ + default T add(String s) { + getRichText().add(s); + return getThis(); + } + + /** + * Adds a drawable to the current line. If the drawable is not a {@link IIcon} it will convert to one with + * {@link IDrawable#asIcon()}. + * If that icon then has no default height (<=0) then it is set to the default text height (9 pixel). If the width + * of the icon is not set, then the width of the widest tooltip line is used. + * + * @param drawable drawable to add. + * @return this + */ + default T add(IDrawable drawable) { + getRichText().add(drawable); + return getThis(); + } + + /** + * Adds a vanilla {@link TooltipComponent} to the current line. + * The tooltip component will always be converted into a {@link IIcon} regardless of what it is and drawn inline + * with the other components. + * It's recommended to use {@link #addLine(TooltipComponent)} instead if you want to preserve how vanilla handles + * this. + * + * @param tooltipComponent tooltip component to add. + * @return this + * @see #addLine(TooltipComponent) + */ + default T add(TooltipComponent tooltipComponent) { + getRichText().add(tooltipComponent); + return getThis(); + } + + default T addLine(FormattedText formattedText) { + getRichText().add(formattedText).newLine(); + return getThis(); + } + + default T addLine(String s) { + getRichText().add(s).newLine(); + return getThis(); + } + + default T addLine(ITextLine line) { + getRichText().addLine(line); + return getThis(); + } + + default T addLine(TooltipComponent tooltipComponent) { + getRichText().add(tooltipComponent).newLine(); + return getThis(); + } + + /** + * Adds a drawable to the current line and creates a new line. + * Refer to {@link #add(IDrawable)} for additional information. + * + * @param line drawable to add. + * @return this + * @see #add(IDrawable) + */ + default T addLine(IDrawable line) { + getRichText().add(line).newLine(); + return getThis(); + } + + /** + * Adds all translated components of a multilang's subkeys and creates a new line for each. + * + * @param lang lang key of the multilang to add. + * @return this + */ + default T addMultiLine(String lang) { + for (MutableComponent text : LangHandler.getMultiLang(lang)) { + getRichText().addLine(text); + } + return getThis(); + } + + /** + * Starts a new line. This is always preferred over {@code "\n"} or {@code IKey.str("\n")}, it reduces computation a + * lot and maybe saves a tiny bit of memory. + * + * @return this + */ + default T newLine() { + return add(IKey.LINE_FEED); + } + + /** + * Adds a space character to the current line. This is rarely useful. + * + * @return this + */ + default T space() { + return add(IKey.SPACE); + } + + /** + * Adds a line with a given thickness in pixels. This will result in larger text line gap. + * + * @param pixelSpace thickness in pixel + * @return this + */ + default T spaceLine(int pixelSpace) { + return addLine(Spacer.of(pixelSpace)); + } + + /** + * Adds a two pixel thick empty line. This will result in larger text line gap. + * This is useful for titles. + * + * @return this + */ + default T spaceLine() { + return addLine(Spacer.SPACER_2PX); + } + + /** + * Adds an empty line which is as tall as a normal text line. + * + * @return this + */ + default T emptyLine() { + return addLine(Spacer.LINE_SPACER); + } + + /** + * Adds multiple drawables to the current line. + * Refer to {@link #add(IDrawable)} for additional information. + * + * @param drawables drawables to add. + * @return this + * @see #add(IDrawable) + */ + default T addElements(Iterable drawables) { + for (IDrawable drawable : drawables) { + getRichText().add(drawable); + } + return getThis(); + } + + /** + * Adds each drawable and creates a new line after each. + * Refer to {@link #add(IDrawable)} for additional information. + * + * @param drawables drawables to add. + * @return this + * @see #add(IDrawable) + */ + default T addDrawableLines(Iterable drawables) { + for (IDrawable drawable : drawables) { + getRichText().add(drawable).newLine(); + } + return getThis(); + } + + /** + * Adds each string and creates a new line after each. + * Refer to {@link #add(String)} for additional information. + * + * @param strings strings to add. + * @return this + * @see #add(IDrawable) + */ + default T addStringLines(Iterable strings) { + for (String string : strings) { + getRichText().add(string).newLine(); + } + return getThis(); + } + + /** + * Finds the next element which contains the matching regex and put the cursor after it. + * If none was found the cursor is at the end. + * + * @param regex regex to match strings for + * @return this + */ + default T moveCursorAfterElement(String regex) { + return moveCursorAfterElement(Pattern.compile(regex)); + } + + /** + * Finds the next element which contains the matching regex and put the cursor after it. + * If none was found the cursor is at the end. + * + * @param regex regex to match strings for + * @return this + */ + default T moveCursorAfterElement(Pattern regex) { + getRichText().moveCursorAfterElement(regex); + return getThis(); + } + + /** + * Finds the next element which contains the matching regex and replaces the whole element with the result of the + * function. + * The cursor is then placed after the new element. If the function returns {@code null}, then the element is + * removed. + * If no element is found nothing happens and the cursor stays in place. + * + * @param regex regex to match strings for + * @param function function to modify the found element + * @return this + */ + default T replace(String regex, UnaryOperator function) { + return replace(Pattern.compile(regex), function); + } + + /** + * Finds the next element which contains the matching regex and replaces the whole element with the result of the + * function. + * The cursor is then placed after the new element. If the function returns {@code null}, then the element is + * removed. + * If no element is found nothing happens and the cursor stays in place. + * + * @param regex regex to match strings for + * @param function function to modify the found element + * @return this + */ + default T replace(Pattern regex, UnaryOperator function) { + getRichText().replace(regex, function); + return getThis(); + } + + /** + * Moves the cursor to the very start. + * + * @return this + */ + default T moveCursorToStart() { + getRichText().moveCursorToStart(); + return getThis(); + } + + /** + * Moves the cursor to the very end (default). + * + * @return this + */ + default T moveCursorToEnd() { + getRichText().moveCursorToEnd(); + return getThis(); + } + + /** + * Moves the cursor a given number of elements forward. The cursor will be clamped at the end. + * + * @param by amount to move cursor by + * @return this + */ + default T moveCursorForward(int by) { + getRichText().moveCursorForward(by); + return getThis(); + } + + /** + * Moves the cursor one element forward. The cursor will be clamped at the end. + * + * @return this + */ + default T moveCursorForward() { + return moveCursorForward(1); + } + + /** + * Moves the cursor a given number of elements backward. The cursor will be clamped at the start. + * + * @param by amount to move cursor by + * @return this + */ + default T moveCursorBackward(int by) { + getRichText().moveCursorBackward(by); + return getThis(); + } + + /** + * Moves the cursor one element backward. The cursor will be clamped at the start. + * + * @return this + */ + default T moveCursorBackward() { + return moveCursorBackward(1); + } + + /** + * This finds the next element ending with a line break and moves the cursor after it. Note that if the line break + * is somewhere in the middle of the element, that element will be ignored. + * + * @return this + */ + default T moveCursorToNextLine() { + getRichText().moveCursorToNextLine(); + return getThis(); + } + + /** + * When the cursor is locked it will no longer move automatically when elements are added, but it can still be moved + * manually with the move methods from above. + * + * @return this + * @see #unlockCursor() + */ + default T lockCursor() { + getRichText().lockCursor(); + return getThis(); + } + + /** + * When the cursor is locked it will no longer move automatically when elements are added, but it can still be moved + * manually with the move methods from above. + * + * @return this + * @see #lockCursor() () + */ + default T unlockCursor() { + getRichText().unlockCursor(); + return getThis(); + } + + /** + * Removes all text. + * + * @return this + */ + + default T clearText() { + getRichText().clearText(); + return getThis(); + } + + default T alignment(Alignment alignment) { + getRichText().alignment(alignment); + return getThis(); + } + + default T textColor(int color) { + getRichText().textColor(color); + return getThis(); + } + + default T scale(float scale) { + getRichText().scale(scale); + return getThis(); + } + + default T textShadow(boolean shadow) { + getRichText().textShadow(shadow); + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java new file mode 100644 index 00000000000..40d3e68b683 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java @@ -0,0 +1,17 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.client.gui.Font; + +public interface ITextLine { + + int getWidth(); + + int getHeight(Font font); + + void draw(GuiContext context, Font font, float x, float y, int color, boolean shadow, + int availableWidth, int availableHeight); + + Object getHoveringElement(Font font, int x, int y); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/gui.json b/src/main/java/com/gregtechceu/gtceu/api/mui/base/gui.json new file mode 100644 index 00000000000..1e2c6b1ae07 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/gui.json @@ -0,0 +1,9 @@ +{ + "name": "test", + "gui": [ + { + "widget": "image", + "min": 50 + } + ] +} \ No newline at end of file diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/ILayoutWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/ILayoutWidget.java new file mode 100644 index 00000000000..21e57899ecd --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/ILayoutWidget.java @@ -0,0 +1,55 @@ +package com.gregtechceu.gtceu.api.mui.base.layout; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.widget.INotifyEnabled; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; + +/** + * This is responsible for laying out widgets. This method responsible for laying out its children + * in itself. This includes calling {@link IResizeable#setSizeResized(boolean, boolean)} or one of its variants after a + * size with + * {@link com.gregtechceu.gtceu.api.mui.widget.sizer.Area#setSize(GuiAxis, int)} or one of its variants on each child. + * The same goes for + * position. If this widget also applies margin and padding (this is usually the case), then + * {@link IResizeable#setMarginPaddingApplied(boolean)} + * or one of its variants needs to be called to. + */ +public interface ILayoutWidget extends INotifyEnabled { + + /** + * Called after the children tried to calculate their size. + * Might be called multiple times. + * + * @return true if the layout was successful and no further iteration is needed. + */ + boolean layoutWidgets(); + + /** + * Called after post calculation of this widget. The last call guarantees, that this widget is fully calculated. + * + * @return true if the layout was successful and no further iteration is needed + */ + default boolean postLayoutWidgets() { + return true; + } + + default boolean canCoverByDefaultSize(GuiAxis axis) { + return false; + } + + /** + * Called when determining wrapping size of this widget. + * If this method returns true, size and margin of the queried child will be ignored for calculation. + * Typically return true when the child is disabled and you want to collapse it for layout. + * This method should also be used for layouting children with {@link #layoutWidgets} if it might return true. + */ + default boolean shouldIgnoreChildSize(IWidget child) { + return false; + } + + @Override + default void onChildChangeEnabled(IWidget child, boolean enabled) { + layoutWidgets(); + postLayoutWidgets(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IResizeable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IResizeable.java new file mode 100644 index 00000000000..91c2827bb58 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IResizeable.java @@ -0,0 +1,195 @@ +package com.gregtechceu.gtceu.api.mui.base.layout; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; + +/** + * An interface that handles resizing of widgets. + * Usually this interface is not implemented by the users of this library or will even interact with it. + */ +public interface IResizeable { + + /** + * Called once before resizing + */ + void initResizing(); + + /** + * Resizes the given element + * + * @param guiElement element to resize + * @param isParentLayout if the parent is a layout widget + * @return true if element is fully resized + */ + boolean resize(IGuiElement guiElement, boolean isParentLayout); + + /** + * Called if {@link #resize(IGuiElement, boolean)} returned false after children have been resized. + * + * @param guiElement element to resize + * @return if element is fully resized + */ + boolean postResize(IGuiElement guiElement); + + /** + * Called after all elements in the tree are resized and the absolute positions needs to be calculated from the + * relative position. + * + * @param guiElement element that was resized + */ + default void applyPos(IGuiElement guiElement) {} + + /** + * @return area of the element + */ + // TODO doesnt fit with the other api methods in this interface + Area getArea(); + + /** + * @return true if the relative x position is calculated + */ + boolean isXCalculated(); + + /** + * @return true if the relative y position is calculated + */ + boolean isYCalculated(); + + /** + * @return true if the width is calculated + */ + boolean isWidthCalculated(); + + /** + * @return true if the height is calculated + */ + boolean isHeightCalculated(); + + boolean areChildrenCalculated(); + + boolean isLayoutDone(); + + default boolean isSizeCalculated(GuiAxis axis) { + return axis.isHorizontal() ? isWidthCalculated() : isHeightCalculated(); + } + + default boolean isPosCalculated(GuiAxis axis) { + return axis.isHorizontal() ? isXCalculated() : isYCalculated(); + } + + /** + * @return true if the relative position and size are fully calculated + */ + default boolean isSelfFullyCalculated(boolean isParentLayout) { + return isSelfFullyCalculated() && !canRelayout(isParentLayout); + } + + default boolean isSelfFullyCalculated() { + return isXCalculated() && isYCalculated() && isWidthCalculated() && isHeightCalculated(); + } + + default boolean isFullyCalculated() { + return isSelfFullyCalculated() && areChildrenCalculated() && isLayoutDone(); + } + + default boolean isFullyCalculated(boolean isParentLayout) { + return isSelfFullyCalculated(isParentLayout) && areChildrenCalculated() && isLayoutDone(); + } + + boolean canRelayout(boolean isParentLayout); + + void setChildrenResized(boolean resized); + + void setLayoutDone(boolean done); + + /** + * Marks position and size as calculated. + */ + void setResized(boolean x, boolean y, boolean w, boolean h); + + default void setPosResized(boolean x, boolean y) { + setResized(x, y, isWidthCalculated(), isHeightCalculated()); + } + + default void setSizeResized(boolean w, boolean h) { + setResized(isXCalculated(), isYCalculated(), w, h); + } + + default void setXResized(boolean v) { + setResized(v, isYCalculated(), isWidthCalculated(), isHeightCalculated()); + } + + default void setYResized(boolean v) { + setResized(isXCalculated(), v, isWidthCalculated(), isHeightCalculated()); + } + + default void setPosResized(GuiAxis axis, boolean v) { + if (axis.isHorizontal()) { + setXResized(v); + } else { + setYResized(v); + } + } + + default void setWidthResized(boolean v) { + setResized(isXCalculated(), isYCalculated(), v, isHeightCalculated()); + } + + default void setHeightResized(boolean v) { + setResized(isXCalculated(), isYCalculated(), isWidthCalculated(), v); + } + + default void setSizeResized(GuiAxis axis, boolean v) { + if (axis.isHorizontal()) { + setWidthResized(v); + } else { + setHeightResized(v); + } + } + + default void setResized(boolean b) { + setResized(b, b, b, b); + } + + default void updateResized() { + setResized(isXCalculated(), isYCalculated(), isWidthCalculated(), isHeightCalculated()); + } + + /** + * Sets if margin and padding on the x-axis is applied + * + * @param b true if margin and padding are applied + */ + void setXMarginPaddingApplied(boolean b); + + /** + * Sets if margin and padding on the y-axis is applied + * + * @param b true if margin and padding are applied + */ + void setYMarginPaddingApplied(boolean b); + + default void setMarginPaddingApplied(boolean b) { + setXMarginPaddingApplied(b); + setYMarginPaddingApplied(b); + } + + default void setMarginPaddingApplied(GuiAxis axis, boolean b) { + if (axis.isHorizontal()) { + setXMarginPaddingApplied(b); + } else { + setYMarginPaddingApplied(b); + } + } + + /** + * @return true if margin and padding are applied on the x-axis + */ + boolean isXMarginPaddingApplied(); + + /** + * @return true if margin and padding are applied on the y-axis + */ + boolean isYMarginPaddingApplied(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewport.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewport.java new file mode 100644 index 00000000000..03c2dce0535 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewport.java @@ -0,0 +1,124 @@ +package com.gregtechceu.gtceu.api.mui.base.layout; + +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.utils.HoveredWidgetList; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import java.util.function.Predicate; + +/** + * A gui element which can transform its children f.e. a scrollable list. + */ +public interface IViewport { + + /** + * Apply shifts of this viewport. + * + * @param stack viewport stack + */ + default void transformChildren(IViewportStack stack) {} + + /** + * Gathers all children at a position. Transformations from this viewport are already applied. + * + * @param stack current viewport stack. Should not be modified. + * @param widgets widget list of already gathered widgets. Add children here. + * @param x x position + * @param y y position + */ + void getWidgetsAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y); + + /** + * Gathers all children at a position. Transformations from this viewport are not applied. + * Called before {@link #getWidgetsAt(IViewportStack, HoveredWidgetList, int, int)} + * + * @param stack current viewport stack. Should not be modified. + * @param widgets widget list of already gathered widgets. Add children here. + * @param x x position + * @param y y position + */ + default void getSelfAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) {} + + /** + * Called during drawing twice (before children are drawn). Once with transformation of this viewport and once + * without + * + * @param context gui context + * @param transformed if transformation from this viewport is active + */ + default void preDraw(ModularGuiContext context, boolean transformed) {} + + /** + * Called during drawing twice (after children are drawn). Once with transformation of this viewport and once + * without + * + * @param context gui context + * @param transformed if transformation from this viewport is active + */ + default void postDraw(ModularGuiContext context, boolean transformed) {} + + static void getChildrenAt(IWidget parent, IViewportStack stack, HoveredWidgetList widgetList, int x, int y) { + for (IWidget child : parent.getChildren()) { + if (!child.isEnabled()) { + continue; + } + if (child instanceof IViewport viewport) { + stack.pushViewport(viewport, parent.getArea()); + child.transform(stack); + viewport.getSelfAt(stack, widgetList, x, y); + viewport.transformChildren(stack); + viewport.getWidgetsAt(stack, widgetList, x, y); + stack.popViewport(viewport); + } else { + stack.pushMatrix(); + child.transform(stack); + if (child.isInside(stack, x, y)) { + widgetList.add(child, stack.peek(), child.getAdditionalHoverInfo(stack, x, y)); + } + if (child.hasChildren()) { + getChildrenAt(child, stack, widgetList, x, y); + } + stack.popMatrix(); + } + } + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + static boolean forEachChild(IViewportStack stack, IWidget parent, Predicate predicate, int context) { + for (IWidget child : parent.getChildren()) { + if (!child.isEnabled()) { + continue; + } + stack.popMatrix(); + if (child instanceof IViewport viewport) { + stack.pushViewport(viewport, parent.getArea()); + parent.transform(stack); + if (!predicate.test(child)) { + stack.popViewport(viewport); + return false; + } + viewport.transformChildren(parent.getContext()); + if (child.hasChildren() && !forEachChild(stack, child, predicate, context)) { + stack.popViewport(viewport); + return false; + } + stack.popViewport(viewport); + } else { + stack.pushMatrix(); + parent.transform(stack); + if (!predicate.test(child)) { + stack.popMatrix(); + return false; + } + if (child.hasChildren() && !forEachChild(stack, child, predicate, context)) { + stack.popMatrix(); + return false; + } + stack.popMatrix(); + } + } + return true; + } + + IViewport EMPTY = (viewports, widgets, x, y) -> {}; +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java new file mode 100644 index 00000000000..6e9b6b6b680 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java @@ -0,0 +1,215 @@ +package com.gregtechceu.gtceu.api.mui.base.layout; + +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.TransformationMatrix; + +import com.mojang.blaze3d.vertex.PoseStack; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +/** + * This handles all viewports in a GUI. Also keeps track of a matrix stack used for rendering and + * user interaction. + */ +public interface IViewportStack { + + /** + * Reset all viewports and the matrix stack. + */ + void reset(); + + /** + * @return current viewport + */ + Area getViewport(); + + /** + * Pushes a viewport to the top. Also pushes a new matrix. + * + * @param viewport viewport to push + * @param area area of the viewport + */ + void pushViewport(IViewport viewport, Area area); + + /** + * Only pushes a matrix without a viewport. + */ + void pushMatrix(); + + /** + * Removes the top viewport and its matrix from the stack. + * + * @param viewport viewport to remove from the top. + * @throws IllegalStateException if the given viewport doesn't match the viewport at the top. + */ + void popViewport(IViewport viewport); + + /** + * Removes the top matrix from the stack. + * + * @throws IllegalStateException if the top matrix is a viewport. + */ + void popMatrix(); + + /** + * @return the matrix stack size + */ + int getStackSize(); + + /** + * Removes all matrices ABOVE the given index. + * + * @param index matrices are removed above this index. + */ + void popUntilIndex(int index); + + /** + * Removes all matrices ABOVE the given viewport. + * + * @param viewport matrices are removed above this viewport. + */ + void popUntilViewport(IViewport viewport); + + /** + * Applies translation transformation to the current top matrix. + * + * @param x translation in x + * @param y translation in y + */ + void translate(float x, float y); + + /** + * Applies translation transformation to the current top matrix. + * + * @param x translation in x + * @param y translation in y + * @param z translation in z + */ + void translate(float x, float y, float z); + + /** + * Applies rotation transformation to the current top matrix. + * + * @param angle clockwise rotation angle in radians + * @param x x-axis rotation. 1 for yes, 0 for no + * @param y y-axis rotation. 1 for yes, 0 for no + * @param z z-axis rotation. 1 for yes, 0 for no + */ + void rotate(float angle, float x, float y, float z); + + /** + * Applies rotation transformation to the current top matrix around z. + * + * @param angle clockwise rotation angle in radians + */ + void rotateZ(float angle); + + /** + * Applies scaling transformation to the current top matrix around. + * + * @param x x scale factor + * @param y y scale factor + */ + void scale(float x, float y); + + /** + * Multiplies current matrix transformation by another matrix + * + * @param matrix the matrix to multiply with + */ + void multiply(Matrix4f matrix); + + /** + * Resets the top matrix to the matrix below. + */ + void resetCurrent(); + + /** + * Transforms the x component of a position with the current matrix transformations. + * + * @param x x component of position + * @param y y component of position + * @return transformed x component + */ + int transformX(float x, float y); + + /** + * Transforms the y component of a position with the current matrix transformations. + * + * @param x x component of position + * @param y y component of position + * @return transformed y component + */ + int transformY(float x, float y); + + /** + * Transforms the x component of a position with the current inverted matrix transformations. + * + * @param x x component of position + * @param y y component of position + * @return un-transformed x component + */ + int unTransformX(float x, float y); + + /** + * Transforms the y component of a position with the current inverted matrix transformations. + * + * @param x x component of position + * @param y y component of position + * @return un-transformed y component + */ + int unTransformY(float x, float y); + + /** + * Transforms a vector with the current matrix transformations. + * This modifies the given vector. + * + * @param vec vector to transform + * @return transformed vector + */ + default Vector3f transform(Vector3f vec) { + return transform(vec, vec); + } + + /** + * Transforms a vector with the current matrix transformations. + * + * @param vec vector to transform + * @param dest vector to write the result to + * @return transformed vector + */ + Vector3f transform(Vector3f vec, Vector3f dest); + + /** + * Transforms a vector with the current inverted matrix transformations. + * This modifies the given vector. + * + * @param vec vector to un-transform + * @return un-transformed vector + */ + default Vector3f unTransform(Vector3f vec) { + return unTransform(vec, vec); + } + + /** + * Transforms a vector with the current inverted matrix transformations. + * This modifies the given vector. + * + * @param vec vector to un-transform + * @param dest vector to write the result to + * @return un-transformed vector + */ + Vector3f unTransform(Vector3f vec, Vector3f dest); + + /** + * Applies the current matrix transformations the current OpenGL matrix. + */ + void applyTo(PoseStack poseStack); + + /** + * @return the top matrix or null if stack is empty + */ + @Nullable + TransformationMatrix peek(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IBoolValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IBoolValue.java new file mode 100644 index 00000000000..df57a91e30d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IBoolValue.java @@ -0,0 +1,18 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IBoolValue extends IValue, IIntValue { + + boolean getBoolValue(); + + void setBoolValue(boolean val); + + @Override + default int getIntValue() { + return getBoolValue() ? 1 : 0; + } + + @Override + default void setIntValue(int val) { + setBoolValue(val == 1); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IByteValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IByteValue.java new file mode 100644 index 00000000000..89be4e97256 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IByteValue.java @@ -0,0 +1,28 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IByteValue extends IIntValue, IStringValue { + + @Override + default void setIntValue(int val) { + setByteValue((byte) val); + } + + @Override + default void setStringValue(String val) { + setByteValue(Byte.parseByte(val)); + } + + @Override + default int getIntValue() { + return getByteValue(); + } + + @Override + default String getStringValue() { + return String.valueOf(getByteValue()); + } + + void setByteValue(byte b); + + byte getByteValue(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IDoubleValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IDoubleValue.java new file mode 100644 index 00000000000..0e50cbb29e8 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IDoubleValue.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IDoubleValue extends IValue { + + double getDoubleValue(); + + void setDoubleValue(double val); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IEnumValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IEnumValue.java new file mode 100644 index 00000000000..3d4d2092b28 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IEnumValue.java @@ -0,0 +1,6 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IEnumValue> extends IValue { + + Class getEnumClass(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IFloatValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IFloatValue.java new file mode 100644 index 00000000000..91df2287614 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IFloatValue.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IFloatValue extends IValue { + + float getFloatValue(); + + void setFloatValue(float val); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IIntValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IIntValue.java new file mode 100644 index 00000000000..c61b4fe51a4 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IIntValue.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IIntValue extends IValue { + + int getIntValue(); + + void setIntValue(int val); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/ILongValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/ILongValue.java new file mode 100644 index 00000000000..fe271ae8e4c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/ILongValue.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface ILongValue extends IValue { + + long getLongValue(); + + void setLongValue(long val); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IStringValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IStringValue.java new file mode 100644 index 00000000000..c6983326c70 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IStringValue.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IStringValue extends IValue { + + String getStringValue(); + + void setStringValue(String val); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/ISyncOrValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/ISyncOrValue.java new file mode 100644 index 00000000000..9ab9466ac16 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/ISyncOrValue.java @@ -0,0 +1,126 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * An interface that is implemented on {@link IValue} and {@link com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler + * SyncHandler} for easier + * validation and setters. + */ +@ApiStatus.NonExtendable +public interface ISyncOrValue { + + /** + * A sync handler or value representing null. + */ + ISyncOrValue EMPTY = new ISyncOrValue() { + + @Override + public @Nullable T castNullable(Class type) { + return null; + } + + @Override + public boolean isTypeOrEmpty(Class type) { + return true; + } + }; + + /** + * Returns the given sync handler or value or {@link #EMPTY} if null. + * + * @param syncOrValue sync handler or value + * @return a non-null representation of the given sync handler or value + */ + @NotNull + static ISyncOrValue orEmpty(@Nullable ISyncOrValue syncOrValue) { + return syncOrValue != null ? syncOrValue : EMPTY; + } + + /** + * Returns if this sync handler or value is an instance of the given type or if this represents null. This is + * useful, when the value or + * sync handler can be null in the widget. + * + * @param type type to check for + * @return if this sync handler or value is an instance of the type or empty + */ + default boolean isTypeOrEmpty(Class type) { + return type.isAssignableFrom(getClass()); + } + + /** + * Casts this sync handler or value to the given type or null if this isn't a subtype of the given type. + * + * @param type type to cast this sync handle or value to + * @param type to cast to + * @return this cast sync handler or value + */ + @Nullable + @SuppressWarnings("unchecked") + default T castNullable(Class type) { + return type.isAssignableFrom(getClass()) ? (T) this : null; + } + + /** + * Casts this sync handler or value to a {@link IValue IValue<V>} if it is a value handler and the containing + * value is of type + * {@link V} else null. + * + * @param valueType expected type of the containing value + * @param expected type of the containing value + * @return a {@link IValue IValue<V>} if types match or null + */ + @Nullable + default IValue castValueNullable(Class valueType) { + return null; + } + + /** + * Casts this sync handler or value to the given type or throws an exception if this isn't a subtype of the given + * type. + * + * @param type type to cast this sync handle or value to + * @param type to cast to + * @return this cast sync handler or value + * @throws IllegalStateException if this is not a subtype of the given type + */ + default T castOrThrow(Class type) { + T t = castNullable(type); + if (t == null) { + if (!isSyncHandler() && !isValueHandler()) { + throw new IllegalStateException("Empty sync handler or value can't be used for anything."); + } + String self = isSyncHandler() ? "sync handler" : "value"; + throw new IllegalStateException("Can't cast " + self + " of type '" + getClass().getSimpleName() + + "' to type '" + type.getSimpleName() + "'."); + } + return t; + } + + /** + * Returns if the containing value of this is of the given type. If this is not a value it will always return false. + * + * @param type expected value type + * @return if the containing value of this is of the given type + */ + default boolean isValueOfType(Class type) { + return false; + } + + /** + * @return if this is a sync handler (false if this represents null) + */ + default boolean isSyncHandler() { + return false; + } + + /** + * @return if this is a value handler (false if this represents null) + */ + default boolean isValueHandler() { + return false; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IValue.java new file mode 100644 index 00000000000..2e3e0f9fe77 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IValue.java @@ -0,0 +1,40 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +/** + * A value wrapper for widgets. + * + * @param value type + */ +public interface IValue extends ISyncOrValue { + + /** + * Gets the current value. + * + * @return the current value + */ + T getValue(); + + /** + * Updates the current value. + * + * @param value new value + */ + void setValue(T value); + + Class getValueType(); + + default boolean isValueOfType(Class type) { + return type.isAssignableFrom(getValueType()); + } + + @SuppressWarnings("unchecked") + @Override + default IValue castValueNullable(Class valueType) { + return isValueOfType(valueType) ? (IValue) this : null; + } + + @Override + default boolean isValueHandler() { + return true; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IBoolSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IBoolSyncValue.java new file mode 100644 index 00000000000..a043de7df29 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IBoolSyncValue.java @@ -0,0 +1,37 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IBoolValue; + +/** + * A helper interface for sync values which can be turned into an integer. + * + * @param value type + */ +public interface IBoolSyncValue extends IValueSyncHandler, IBoolValue, IIntSyncValue { + + @Override + default void setBoolValue(boolean val) { + setBoolValue(val, true, true); + } + + default void setBoolValue(boolean val, boolean setSource) { + setBoolValue(val, setSource, true); + } + + void setBoolValue(boolean value, boolean setSource, boolean sync); + + @Override + default void setIntValue(int value, boolean setSource, boolean sync) { + setBoolValue(value == 1, setSource, sync); + } + + @Override + default int getIntValue() { + return IBoolValue.super.getIntValue(); + } + + @Override + default void setIntValue(int val) { + IBoolValue.super.setIntValue(val); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IByteSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IByteSyncValue.java new file mode 100644 index 00000000000..c47b3e2da37 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IByteSyncValue.java @@ -0,0 +1,17 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IByteValue; + +public interface IByteSyncValue extends IByteValue, IValueSyncHandler { + + @Override + default void setByteValue(byte val) { + setByteValue(val, true); + } + + default void setByteValue(byte val, boolean setSource) { + setByteValue(val, setSource, true); + } + + void setByteValue(byte value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IDoubleSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IDoubleSyncValue.java new file mode 100644 index 00000000000..b1c9aa10f0e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IDoubleSyncValue.java @@ -0,0 +1,22 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IDoubleValue; + +/** + * A helper interface for sync values which can be turned into an integer. + * + * @param value type + */ +public interface IDoubleSyncValue extends IValueSyncHandler, IDoubleValue { + + @Override + default void setDoubleValue(double val) { + setDoubleValue(val, true, true); + } + + default void setDoubleValue(double val, boolean setSource) { + setDoubleValue(val, setSource, true); + } + + void setDoubleValue(double value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IFloatSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IFloatSyncValue.java new file mode 100644 index 00000000000..cfa3353e30c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IFloatSyncValue.java @@ -0,0 +1,17 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IFloatValue; + +public interface IFloatSyncValue extends IValueSyncHandler, IFloatValue { + + @Override + default void setFloatValue(float val) { + setFloatValue(val, true, true); + } + + default void setFloatValue(float val, boolean setSource) { + setFloatValue(val, setSource, true); + } + + void setFloatValue(float value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IIntSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IIntSyncValue.java new file mode 100644 index 00000000000..630bd244375 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IIntSyncValue.java @@ -0,0 +1,22 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IIntValue; + +/** + * A helper interface for sync values which can be turned into an integer. + * + * @param value type + */ +public interface IIntSyncValue extends IValueSyncHandler, IIntValue { + + @Override + default void setIntValue(int val) { + setIntValue(val, true, true); + } + + default void setIntValue(int val, boolean setSource) { + setIntValue(val, setSource, true); + } + + void setIntValue(int value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/ILongSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/ILongSyncValue.java new file mode 100644 index 00000000000..5259e61ba0a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/ILongSyncValue.java @@ -0,0 +1,22 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.ILongValue; + +/** + * A helper interface for sync values which can be turned into an integer. + * + * @param value type + */ +public interface ILongSyncValue extends IValueSyncHandler, ILongValue { + + @Override + default void setLongValue(long val) { + setLongValue(val, true, true); + } + + default void setLongValue(long val, boolean setSource) { + setLongValue(val, setSource, true); + } + + void setLongValue(long value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerKeyboardAction.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerKeyboardAction.java new file mode 100644 index 00000000000..412770b744a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerKeyboardAction.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.utils.KeyboardData; + +public interface IServerKeyboardAction { + + void onServerKeyboardAction(KeyboardData data); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerMouseAction.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerMouseAction.java new file mode 100644 index 00000000000..bc9e0bcca2e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerMouseAction.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.utils.MouseData; + +public interface IServerMouseAction { + + void onServerMouseAction(MouseData mouseData); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IStringSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IStringSyncValue.java new file mode 100644 index 00000000000..1a73864b374 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IStringSyncValue.java @@ -0,0 +1,22 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IStringValue; + +/** + * A helper interface for sync values which can be turned into a string. + * + * @param value type + */ +public interface IStringSyncValue extends IValueSyncHandler, IStringValue { + + @Override + default void setStringValue(String val) { + setStringValue(val, true, true); + } + + default void setStringValue(String val, boolean setSource) { + setStringValue(val, setSource, true); + } + + void setStringValue(String value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java new file mode 100644 index 00000000000..c7398c27aeb --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java @@ -0,0 +1,76 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IValue; + +import net.minecraft.network.FriendlyByteBuf; + +/** + * A helper interface for syncing an object value. + * + * @param object value type + */ +public interface IValueSyncHandler extends IValue { + + /** + * Updates the current value and the source and syncs it to client/server. + * + * @param value new value + */ + @Override + default void setValue(T value) { + setValue(value, true, true); + } + + /** + * Updates the current value and syncs it to client/server. + * + * @param value new value + * @param setSource whether the source should be updated with the new value + */ + default void setValue(T value, boolean setSource) { + setValue(value, setSource, true); + } + + /** + * Updates the current value. + * + * @param value new value + * @param setSource whether the source should be updated with the new value + * @param sync whether the new value should be synced to client/server + */ + void setValue(T value, boolean setSource, boolean sync); + + /** + * Determines if the current value is different from source and updates the current value if it is. + * + * @param isFirstSync true if it's the first tick in the ui + * @return true if the current value was different from source + */ + boolean updateCacheFromSource(boolean isFirstSync); + + /** + * Updates the cache from source and syncs it to the other sides. + *

+ * Usually this is + * + * setValue(getter.get(), false, true); + * + *

+ * Where {@code getter} is the source. + */ + void notifyUpdate(); + + /** + * Writes the current value to the buffer + * + * @param buffer buffer to write to + */ + void write(FriendlyByteBuf buffer); + + /** + * Reads a value from the buffer and sets the current value + * + * @param buffer buffer to read from + */ + void read(FriendlyByteBuf buffer); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDragResizeable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDragResizeable.java new file mode 100644 index 00000000000..5f323b224f0 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDragResizeable.java @@ -0,0 +1,119 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.utils.GTUtil; + +/** + * Implement this interface on a {@link IWidget} to allow it being resized by dragging the edges similar to windows. + */ +public interface IDragResizeable { + + /** + * @return if this widget can currently be resized by dragging an edge + */ + default boolean isCurrentlyResizable() { + return true; + } + + /** + * @return if the center position of this widget should be retained, by also resizing the opposite edge + */ + default boolean keepPosOnDragResize() { + return true; + } + + /** + * Called every time the mouse moves one or more pixels while this widget is resized by dragging an edge. + */ + default void onDragResize() { + ((IWidget) this).scheduleResize(); + } + + /** + * @return The border size in which to allow drag resizing in pixels. + */ + default int getDragAreaSize() { + return 3; + } + + /** + * @return The minimum width this widget can be dragged to. + */ + default int getMinDragWidth() { + return 18; + } + + /** + * @return The minimum height this widget can be dragged to. + */ + default int getMinDragHeight() { + return 18; + } + + /** + * An internal method to detect if the mouse is currently hovering an area where a drag resize can be started. + */ + static ResizeDragArea getDragResizeCorner(IDragResizeable widget, Area area, IViewportStack stack, int x, int y) { + if (!widget.isCurrentlyResizable()) return null; + + int mx = stack.unTransformX(x, y); + int my = stack.unTransformY(x, y); + + if (mx < 0 || my < 0 || mx > area.w() || my > area.h()) return null; + + int ras = widget.getDragAreaSize(); + if (mx < ras) { + if (my < ras) return ResizeDragArea.TOP_LEFT; + if (my > area.h() - ras) return ResizeDragArea.BOTTOM_LEFT; + return ResizeDragArea.LEFT; + } + if (mx > area.w() - ras) { + if (my < ras) return ResizeDragArea.TOP_RIGHT; + if (my > area.h() - ras) return ResizeDragArea.BOTTOM_RIGHT; + return ResizeDragArea.RIGHT; + } + if (my < ras) return ResizeDragArea.TOP; + if (my > area.h() - ras) return ResizeDragArea.BOTTOM; + return null; + } + + /** + * An internal method to actually resize the widget while an edge is being dragged. + */ + static void applyDrag(IDragResizeable resizeable, IWidget widget, ResizeDragArea dragArea, Area startArea, int dx, + int dy) { + int keepPosFactor = resizeable.keepPosOnDragResize() || GTUtil.isShiftDown() ? 2 : 1; + if (dx != 0) { + if (dragArea.left) { + int s = startArea.width - dx * keepPosFactor; + if (s >= resizeable.getMinDragWidth()) { + widget.flex().left(startArea.rx + dx); + widget.flex().width(s); + } + } else if (dragArea.right) { + int s = startArea.width + dx * keepPosFactor; + if (s >= resizeable.getMinDragWidth()) { + widget.flex().left(startArea.rx - dx * (keepPosFactor - 1)); + widget.flex().width(s); + } + } + } + if (dy != 0) { + if (dragArea.top) { + int s = startArea.height - dy * keepPosFactor; + if (s >= resizeable.getMinDragHeight()) { + widget.flex().top(startArea.ry + dy); + widget.flex().height(s); + } + } else if (dragArea.bottom) { + int s = startArea.height + dy * keepPosFactor; + if (s >= resizeable.getMinDragHeight()) { + widget.flex().top(startArea.ry - dy * (keepPosFactor - 1)); + widget.flex().height(s); + } + } + } + resizeable.onDragResize(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java new file mode 100644 index 00000000000..42df66f763e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java @@ -0,0 +1,72 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.base.layout.IViewport; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.utils.HoveredWidgetList; +import com.gregtechceu.gtceu.api.mui.widget.DraggableWidget; +import com.gregtechceu.gtceu.api.mui.widget.WidgetTree; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import net.minecraft.client.gui.GuiGraphics; + +import org.jetbrains.annotations.Nullable; + +/** + * Marks a widget as draggable. + * The dragging is handled by ModularUI. + * + * @see DraggableWidget + */ +public interface IDraggable extends IViewport { + + /** + * Gets called every frame after everything else is rendered. + * Is only called when {@link #isMoving()} is true. + * Translate to the mouse pos and draw with {@link WidgetTree#drawTree(IWidget, ModularGuiContext)}. + * + * @param graphics + * @param partialTicks difference from last from + */ + void drawMovingState(GuiGraphics graphics, ModularGuiContext context, float partialTicks); + + /** + * @param button the mouse button that's holding down + * @return false if the action should be canceled + */ + boolean onDragStart(int button); + + /** + * The dragging has ended and getState == IDLE + * + * @param successful is false if this returned to its old position + */ + void onDragEnd(boolean successful); + + void onDrag(int mouseButton, double timeSinceLastClick); + + /** + * Gets called when the mouse is released + * + * @param widget current top most widget below the mouse + * @return if the location is valid + */ + default boolean canDropHere(int x, int y, @Nullable IGuiElement widget) { + return true; + } + + /** + * @return the size and pos during move + */ + @Nullable + Area getMovingArea(); + + boolean isMoving(); + + void setMoving(boolean moving); + + void transform(IViewportStack viewportStack); + + @Override + default void getWidgetsAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) {} +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IFocusedWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IFocusedWidget.java new file mode 100644 index 00000000000..518acdcabb4 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IFocusedWidget.java @@ -0,0 +1,29 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +/** + * An interface for {@link IWidget}'s, that makes them focusable. + * Usually used for text fields to receive keyboard and mouse input first, no matter if its hovered or not. + */ +public interface IFocusedWidget { + + /** + * @return this widget is currently focused + */ + boolean isFocused(); + + /** + * Called when this widget gets focused + * + * @param context gui context + */ + void onFocus(ModularGuiContext context); + + /** + * Called when the focus is removed from this widget + * + * @param context gui context + */ + void onRemoveFocus(ModularGuiContext context); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiAction.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiAction.java new file mode 100644 index 00000000000..4eeb2471f3e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiAction.java @@ -0,0 +1,51 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; + +/** + * Gui action listeners that can be registered in {@link ModularScreen#registerGuiActionListener(IGuiAction)} + */ +public interface IGuiAction { + + @FunctionalInterface + interface MousePressed extends IGuiAction { + + boolean press(double mouseX, double mouseY, int button); + } + + @FunctionalInterface + interface MouseReleased extends IGuiAction { + + boolean release(double mouseX, double mouseY, int button); + } + + @FunctionalInterface + interface KeyPressed extends IGuiAction { + + boolean press(int keyCode, int scanCode, int modifiers); + } + + @FunctionalInterface + interface KeyReleased extends IGuiAction { + + boolean release(int keyCode, int scanCode, int modifiers); + } + + @FunctionalInterface + interface CharTyped extends IGuiAction { + + boolean type(char codePoint, int modifiers); + } + + @FunctionalInterface + interface MouseScroll extends IGuiAction { + + boolean scroll(double mouseX, double mouseY, double delta); + } + + @FunctionalInterface + interface MouseDrag extends IGuiAction { + + boolean drag(double mouseX, double mouseY, int button, double dragX, double dragY); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiElement.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiElement.java new file mode 100644 index 00000000000..e8aaa5b0bf4 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiElement.java @@ -0,0 +1,119 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +/** + * Base interface for gui elements. For example widgets. + */ +public interface IGuiElement { + + /** + * @return the screen this element is in + */ + ModularScreen getScreen(); + + /** + * @return the parent of this element + */ + IGuiElement getParent(); + + /** + * Returns if this element has a parent. This is the case when the widget is valid, but never if it's root widget. + */ + boolean hasParent(); + + IResizeable resizer(); + + /** + * @return the area this element occupies + */ + Area getArea(); + + /** + * Shortcut to get the area of the parent + * + * @return parent area + */ + default Area getParentArea() { + return getParent().getArea(); + } + + /** + * Draws this element + * + * @param context gui context + */ + void draw(ModularGuiContext context); + + /** + * Called when the mouse hovers this element. This means this element is directly below the mouse or there are + * widgets in between which all allow to pass hover through. This is not called when the element is at any point + * below the mouse. + */ + default void onMouseStartHover() {} + + /** + * Called when the mouse no longer hovers this element. This widget can still be below the mouse on some level. + */ + default void onMouseEndHover() {} + + /** + * Called when the mouse enters this element's area with any amount of widgets above it from the current panel. + */ + default void onMouseEnterArea() {} + + /** + * Called when the mouse leaves the area, or it started hovering a different panel. + */ + default void onMouseLeaveArea() {} + + /** + * @return if this widget is currently right below the mouse + */ + default boolean isHovering() { + return isHoveringFor(0); + } + + /** + * + * @param ticks time in ticks + * @return if this element is right below the mouse for a certain amount of time + */ + default boolean isHoveringFor(int ticks) { + return false; + } + + default boolean isBelowMouse() { + return isBelowMouseFor(0); + } + + default boolean isBelowMouseFor(int ticks) { + return false; + } + + /** + * Returns if this element is enabled. Disabled elements are not drawn and can not be interacted with. + */ + boolean isEnabled(); + + /** + * @return default width if it can't be calculated + */ + default int getDefaultWidth() { + return 18; + } + + /** + * @return default height if it can't be calculated + */ + default int getDefaultHeight() { + return 18; + } + + void scheduleResize(); + + boolean requiresResize(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java new file mode 100644 index 00000000000..5f65fb976d1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java @@ -0,0 +1,6 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +public interface INotifyEnabled { + + void onChildChangeEnabled(IWidget child, boolean enabled); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IParentWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IParentWidget.java new file mode 100644 index 00000000000..386a85fc6a9 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IParentWidget.java @@ -0,0 +1,41 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Supplier; + +public interface IParentWidget> { + + W getThis(); + + boolean addChild(I child, int index); + + default W child(int index, I child) { + if (!addChild(child, index)) { + throw new IllegalStateException("Failed to add child"); + } + return getThis(); + } + + default W child(I child) { + if (!addChild(child, -1)) { + throw new IllegalStateException("Failed to add child"); + } + return getThis(); + } + + /** + * @deprecated use {@link #childIf(boolean, Supplier)} + */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated + default W childIf(boolean condition, I child) { + if (condition) return child(child); + return getThis(); + } + + default W childIf(boolean condition, Supplier child) { + if (condition) return child(child.get()); + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IPositioned.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IPositioned.java new file mode 100644 index 00000000000..05c0e871397 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IPositioned.java @@ -0,0 +1,512 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Flex; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Unit; + +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; + +/** + * Helper interface for position and size builder methods for widgets. + * + * @param widget type + */ +@SuppressWarnings({ "unused", "UnusedReturnValue" }) +public interface IPositioned> { + + Flex flex(); + + Area getArea(); + + void scheduleResize(); + + boolean requiresResize(); + + @SuppressWarnings("unchecked") + default W getThis() { + return (W) this; + } + + default W coverChildrenWidth() { + flex().coverChildrenWidth(); + return getThis(); + } + + default W coverChildrenHeight() { + flex().coverChildrenHeight(); + return getThis(); + } + + default W coverChildren() { + return coverChildrenWidth().coverChildrenHeight(); + } + + default W expanded() { + flex().expanded(); + return getThis(); + } + + default W relative(IGuiElement guiElement) { + return relative(guiElement.getArea()); + } + + default W relative(Area guiElement) { + flex().relative(guiElement); + return getThis(); + } + + default W relativeToScreen() { + flex().relativeToScreen(); + return getThis(); + } + + default W relativeToParent() { + flex().relativeToParent(); + return getThis(); + } + + default W bypassLayerRestriction() { + flex().bypassLayerRestriction(); + return getThis(); + } + + default W left(int val) { + flex().left(val, 0, 0, Unit.Measure.PIXEL, true); + return getThis(); + } + + default W leftRel(float val) { + flex().left(val, 0, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W leftRelOffset(float val, int offset) { + flex().left(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W leftRelAnchor(float val, float anchor) { + flex().left(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W leftRel(float val, int offset, float anchor) { + flex().left(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W left(float val, int offset, float anchor, Unit.Measure measure) { + flex().left(val, offset, anchor, measure, false); + return getThis(); + } + + default W left(DoubleSupplier val, Unit.Measure measure) { + flex().left(val, 0, 0, measure, true); + return getThis(); + } + + default W leftRelOffset(DoubleSupplier val, int offset) { + flex().left(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W leftRelAnchor(DoubleSupplier val, float anchor) { + flex().left(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W leftRel(DoubleSupplier val, int offset, float anchor) { + flex().left(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W right(int val) { + flex().right(val, 0, 0, Unit.Measure.PIXEL, true); + return getThis(); + } + + default W rightRel(float val) { + flex().right(val, 0, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W rightRelOffset(float val, int offset) { + flex().right(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W rightRelAnchor(float val, float anchor) { + flex().right(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W rightRel(float val, int offset, float anchor) { + flex().right(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W right(float val, int offset, float anchor, Unit.Measure measure) { + flex().right(val, offset, anchor, measure, false); + return getThis(); + } + + default W right(DoubleSupplier val, Unit.Measure measure) { + flex().right(val, 0, 0, measure, true); + return getThis(); + } + + default W rightRelOffset(DoubleSupplier val, int offset) { + flex().right(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W rightRelAnchor(DoubleSupplier val, float anchor) { + flex().right(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W rightRel(DoubleSupplier val, int offset, float anchor) { + flex().right(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W top(int val) { + flex().top(val, 0, 0, Unit.Measure.PIXEL, true); + return getThis(); + } + + default W topRel(float val) { + flex().top(val, 0, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W topRelOffset(float val, int offset) { + flex().top(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W topRelAnchor(float val, float anchor) { + flex().top(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W topRel(float val, int offset, float anchor) { + flex().top(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W top(float val, int offset, float anchor, Unit.Measure measure) { + flex().top(val, offset, anchor, measure, false); + return getThis(); + } + + default W top(DoubleSupplier val, Unit.Measure measure) { + flex().top(val, 0, 0, measure, true); + return getThis(); + } + + default W topRelOffset(DoubleSupplier val, int offset) { + flex().top(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W topRelAnchor(DoubleSupplier val, float anchor) { + flex().top(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W topRel(DoubleSupplier val, int offset, float anchor) { + flex().top(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W bottom(int val) { + flex().bottom(val, 0, 0, Unit.Measure.PIXEL, true); + return getThis(); + } + + default W bottomRel(float val) { + flex().bottom(val, 0, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W bottomRelOffset(float val, int offset) { + flex().bottom(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W bottomRelAnchor(float val, float anchor) { + flex().bottom(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W bottomRel(float val, int offset, float anchor) { + flex().bottom(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W bottom(float val, int offset, float anchor, Unit.Measure measure) { + flex().bottom(val, offset, anchor, measure, false); + return getThis(); + } + + default W bottom(DoubleSupplier val, Unit.Measure measure) { + flex().bottom(val, 0, 0, measure, true); + return getThis(); + } + + default W bottomRelOffset(DoubleSupplier val, int offset) { + flex().bottom(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W bottomRelAnchor(DoubleSupplier val, float anchor) { + flex().bottom(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W bottomRel(DoubleSupplier val, int offset, float anchor) { + flex().bottom(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W width(int val) { + flex().width(val, 0, Unit.Measure.PIXEL); + return getThis(); + } + + default W widthRel(float val) { + flex().width(val, 0, Unit.Measure.RELATIVE); + return getThis(); + } + + default W widthRelOffset(float val, int offset) { + flex().width(val, offset, Unit.Measure.RELATIVE); + return getThis(); + } + + default W width(float val, Unit.Measure measure) { + flex().width(val, 0, measure); + return getThis(); + } + + default W width(DoubleSupplier val, Unit.Measure measure) { + flex().width(val, 0, measure); + return getThis(); + } + + default W height(int val) { + flex().height(val, 0, Unit.Measure.PIXEL); + return getThis(); + } + + default W heightRel(float val) { + flex().height(val, 0, Unit.Measure.RELATIVE); + return getThis(); + } + + default W height(float val, Unit.Measure measure) { + flex().height(val, 0, measure); + return getThis(); + } + + default W heightRelOffset(DoubleSupplier val, int offset) { + flex().height(val, offset, Unit.Measure.RELATIVE); + return getThis(); + } + + default W height(DoubleSupplier val, Unit.Measure measure) { + flex().height(val, 0, measure); + return getThis(); + } + + default W pos(int x, int y) { + left(x).top(y); + return getThis(); + } + + default W posRel(float x, float y) { + leftRel(x).topRel(y); + return getThis(); + } + + default W size(int w, int h) { + width(w).height(h); + return getThis(); + } + + default W sizeRel(float w, float h) { + widthRel(w).heightRel(h); + return getThis(); + } + + default W size(int val) { + return width(val).height(val); + } + + default W sizeRel(float val) { + return widthRel(val).heightRel(val); + } + + default W fullWidth() { + return widthRel(1f); + } + + default W fullHeight() { + return heightRel(1f); + } + + default W full() { + return widthRel(1f).heightRel(1f); + } + + default W anchorLeft(float val) { + flex().anchorLeft(val); + return getThis(); + } + + default W anchorRight(float val) { + flex().anchorRight(val); + return getThis(); + } + + default W anchorTop(float val) { + flex().anchorTop(val); + return getThis(); + } + + default W anchorBottom(float val) { + flex().anchorBottom(val); + return getThis(); + } + + default W anchor(Alignment alignment) { + flex().anchor(alignment); + return getThis(); + } + + default W alignX(float val) { + leftRel(val).anchorLeft(val); + return getThis(); + } + + default W alignX(Alignment alignment) { + return alignX(alignment.x); + } + + default W alignY(float val) { + topRel(val).anchorTop(val); + return getThis(); + } + + default W alignY(Alignment alignment) { + return alignY(alignment.y); + } + + default W align(Alignment alignment) { + return alignX(alignment).alignY(alignment); + } + + default W horizontalCenter() { + return alignX(Alignment.CENTER); + } + + default W verticalCenter() { + return alignY(Alignment.CENTER); + } + + default W center() { + return align(Alignment.Center); + } + + default W flex(Consumer flexConsumer) { + flexConsumer.accept(flex()); + return getThis(); + } + + default W padding(int left, int right, int top, int bottom) { + getArea().getPadding().all(left, right, top, bottom); + scheduleResize(); + return getThis(); + } + + default W padding(int horizontal, int vertical) { + getArea().getPadding().all(horizontal, vertical); + scheduleResize(); + return getThis(); + } + + default W padding(int all) { + getArea().getPadding().all(all); + scheduleResize(); + return getThis(); + } + + default W paddingLeft(int val) { + getArea().getPadding().left(val); + scheduleResize(); + return getThis(); + } + + default W paddingRight(int val) { + getArea().getPadding().right(val); + scheduleResize(); + return getThis(); + } + + default W paddingTop(int val) { + getArea().getPadding().top(val); + scheduleResize(); + return getThis(); + } + + default W paddingBottom(int val) { + getArea().getPadding().bottom(val); + scheduleResize(); + return getThis(); + } + + default W margin(int left, int right, int top, int bottom) { + getArea().getMargin().all(left, right, top, bottom); + scheduleResize(); + return getThis(); + } + + default W margin(int horizontal, int vertical) { + getArea().getMargin().all(horizontal, vertical); + scheduleResize(); + return getThis(); + } + + default W margin(int all) { + getArea().getMargin().all(all); + scheduleResize(); + return getThis(); + } + + default W marginLeft(int val) { + getArea().getMargin().left(val); + scheduleResize(); + return getThis(); + } + + default W marginRight(int val) { + getArea().getMargin().right(val); + scheduleResize(); + return getThis(); + } + + default W marginTop(int val) { + getArea().getMargin().top(val); + scheduleResize(); + return getThis(); + } + + default W marginBottom(int val) { + getArea().getMargin().bottom(val); + scheduleResize(); + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java new file mode 100644 index 00000000000..56324bacff7 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java @@ -0,0 +1,156 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.base.value.ISyncOrValue; +import com.gregtechceu.gtceu.api.mui.value.sync.GenericSyncValue; +import com.gregtechceu.gtceu.api.mui.value.sync.ModularSyncManager; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Marks a widget as synced + * + * @param widget type + */ +public interface ISynced { + + /** + * @return this cast to the true widget type + */ + @SuppressWarnings("unchecked") + default W getThis() { + return (W) this; + } + + /** + * Called when this widget gets initialised or when this widget is added to the gui + * + * @param syncManager sync manager + * @param late if this is called at any point after the panel this widget belongs to opened + */ + void initialiseSyncHandler(ModularSyncManager syncManager, boolean late); + + /** + * @deprecated use {@link #isValidSyncOrValue(ISyncOrValue)} + */ + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated + default boolean isValidSyncHandler(SyncHandler syncHandler) { + return false; + } + + /** + * Returns if the given value or sync handler is valid for this widget. This is usually a call to + * {@link ISyncOrValue#isTypeOrEmpty(Class)}. If the widget must specify a value (disallow null) instanceof check + * can be used. You can + * check for primitive types which don't have a dedicated {@link com.gregtechceu.gtceu.api.mui.base.value.IValue + * IValue} interface with + * {@link ISyncOrValue#isValueOfType(Class)}. + * + * @param syncOrValue a sync handler or a value, but never null + * @return if the value or sync handler is valid for this class + */ + default boolean isValidSyncOrValue(@NotNull ISyncOrValue syncOrValue) { + return !(syncOrValue instanceof SyncHandler syncHandler) || isValidSyncHandler(syncHandler); + } + + /** + * Checks if the given sync handler is valid for this widget and throws an exception if not. + * Override {@link #isValidSyncHandler(SyncHandler)} + * + * @param syncHandler given sync handler + * @throws IllegalStateException if the given sync handler is invalid for this widget. + */ + @ApiStatus.NonExtendable + default void checkValidSyncOrValue(ISyncOrValue syncHandler) { + if (!isValidSyncOrValue(syncHandler)) { + throw new IllegalStateException( + "SyncHandler of type '" + syncHandler.getClass().getSimpleName() + "' is not valid " + + "for widget '" + this + "'."); + } + } + + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated + default T castIfTypeElseNull(SyncHandler syncHandler, Class clazz) { + return castIfTypeElseNull(syncHandler, clazz, null); + } + + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated + @SuppressWarnings("unchecked") + default T castIfTypeElseNull(SyncHandler syncHandler, Class clazz, @Nullable Consumer setup) { + if (syncHandler != null && clazz.isAssignableFrom(syncHandler.getClass())) { + T t = (T) syncHandler; + if (setup != null) setup.accept(t); + return t; + } + return null; + } + + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated + default GenericSyncValue castIfTypeGenericElseNull(SyncHandler syncHandler, Class clazz) { + return castIfTypeGenericElseNull(syncHandler, clazz, null); + } + + @ApiStatus.ScheduledForRemoval(inVersion = "3.2.0") + @Deprecated + default GenericSyncValue castIfTypeGenericElseNull(SyncHandler syncHandler, Class clazz, + @Nullable Consumer> setup) { + if (syncHandler instanceof GenericSyncValue genericSyncValue && genericSyncValue.isOfType(clazz)) { + GenericSyncValue t = genericSyncValue.cast(); + if (setup != null) setup.accept(t); + return t; + } + return null; + } + + /** + * @return true if this widget has a valid sync handler + */ + boolean isSynced(); + + /** + * @return the sync handler of this widget + * @throws IllegalStateException if this widget has no valid sync handler + */ + @NotNull + SyncHandler getSyncHandler(); + + /** + * Sets the sync handler key. The sync handler will be obtained in + * {@link #initialiseSyncHandler(ModularSyncManager, boolean)} + * + * @param name sync handler key name + * @param id sync handler key id + * @return this + */ + W syncHandler(String name, int id); + + /** + * Sets the sync handler key. The sync handler will be obtained in + * {@link #initialiseSyncHandler(ModularSyncManager, boolean)} + * + * @param key sync handler name + * @return this + */ + default W syncHandler(String key) { + return syncHandler(key, 0); + } + + /** + * Sets the sync handler key. The sync handler will be obtained in + * {@link #initialiseSyncHandler(ModularSyncManager, boolean)} + * + * @param id sync handler id + * @return this + */ + default W syncHandler(int id) { + return syncHandler("_", id); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java new file mode 100644 index 00000000000..1110f29f3a7 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java @@ -0,0 +1,278 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.base.drawable.ITextLine; +import com.gregtechceu.gtceu.api.mui.drawable.text.StyledText; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Helper interface with tooltip builder methods for widgets. + * + * @param widget type + */ +public interface ITooltip> { + + /** + * @return the current tooltip of this widget. Null if there is none + */ + @Nullable + RichTooltip getTooltip(); + + /** + * @return the current tooltip of this widget. Creates a new one if there is none + */ + @NotNull + RichTooltip tooltip(); + + /** + * Overwrites the current tooltip with the given one + * + * @param tooltip new tooltip + * @return this + */ + W tooltip(RichTooltip tooltip); + + /** + * @return true if this widget has a tooltip + */ + default boolean hasTooltip() { + return getTooltip() != null && !getTooltip().isEmpty(); + } + + /** + * @return this cast to the true widget type + */ + @SuppressWarnings("unchecked") + default W getThis() { + return (W) this; + } + + /** + * Helper method to call tooltip setters within a widget tree initialisation. + * Only called once. + * + * @param tooltipConsumer tooltip function + * @return this + */ + default W tooltip(Consumer tooltipConsumer) { + return tooltipStatic(tooltipConsumer); + } + + /** + * Helper method to call tooltip setters within a widget tree initialisation. + * Only called once. + * + * @param tooltipConsumer tooltip function + * @return this + */ + default W tooltipStatic(Consumer tooltipConsumer) { + tooltipConsumer.accept(tooltip()); + return getThis(); + } + + /** + * Sets a tooltip builder. The builder will be called every time the tooltip is marked dirty. + * Should be used for dynamic tooltips. + * + * @param tooltipBuilder tooltip function + * @return this + */ + default W tooltipBuilder(Consumer tooltipBuilder) { + return tooltipDynamic(tooltipBuilder); + } + + /** + * Sets a tooltip builder. The builder will be called every time the tooltip is marked dirty. + * Should be used for dynamic tooltips. + * + * @param tooltipBuilder tooltip function + * @return this + */ + default W tooltipDynamic(Consumer tooltipBuilder) { + tooltip().tooltipBuilder(tooltipBuilder); + return getThis(); + } + + /** + * Sets a general tooltip position. The true position is calculated every frame. + * + * @param pos tooltip pos + * @return this + */ + default W tooltipPos(RichTooltip.Pos pos) { + tooltip().pos(pos); + return getThis(); + } + + /** + * Sets a fixed tooltip position. + * + * @param x x pos + * @param y y pos + * @return this + */ + default W tooltipPos(int x, int y) { + tooltip().pos(x, y); + return getThis(); + } + + /** + * Sets an alignment. The alignment determines how the content is aligned in the tooltip. + * + * @param alignment alignment + * @return this + */ + default W tooltipAlignment(Alignment alignment) { + tooltip().alignment(alignment); + return getThis(); + } + + /** + * Sets if the tooltip text should have shadow enabled by default. + * Can be overridden with {@link StyledText} lines. + * + * @param textShadow true if text should have a shadow + * @return this + */ + default W tooltipTextShadow(boolean textShadow) { + tooltip().textShadow(textShadow); + return getThis(); + } + + /** + * Sets a default tooltip text color. Can be overridden with text formatting. + * + * @param textColor text color + * @return this + */ + default W tooltipTextColor(int textColor) { + tooltip().textColor(textColor); + return getThis(); + } + + /** + * Sets a tooltip scale. The whole tooltip with content will be drawn with this scale. + * + * @param scale scale + * @return this + */ + default W tooltipScale(float scale) { + tooltip().scale(scale); + return getThis(); + } + + /** + * Sets a show up timer. This is the time in ticks needed for the cursor to hover this widget for the tooltip to + * appear. + * + * @param showUpTimer show up timer in ticks + * @return this + */ + default W tooltipShowUpTimer(int showUpTimer) { + tooltip().showUpTimer(showUpTimer); + return getThis(); + } + + /** + * Sets whether the tooltip should automatically update on every render tick. In most of the cases you don't need + * this, + * as ValueSyncHandler handles tooltip update for you when value is updated. However, if you don't handle + * differently, + * you either need to manually set change listener for the sync value, or set auto update to true. + * + * @param update true if the tooltip should automatically update + * @return this + */ + default W tooltipAutoUpdate(boolean update) { + tooltip().autoUpdate(update); + return getThis(); + } + + /** + * Sets whether the tooltip has a title margin, which is 2px space between first and second line inserted by + * default. + * + * @param hasTitleMargin true if the tooltip should have a title margin + * @return this + */ + default W tooltipHasTitleMargin(boolean hasTitleMargin) { + // tooltip().setHasTitleMargin(hasTitleMargin); + return getThis(); + } + + /** + * Sets the line padding for the tooltip. 1px by default, and you can disable it by passing 0. + * + * @param linePadding line padding in px + * @return this + */ + default W tooltipLinePadding(int linePadding) { + // tooltip().setLinePadding(linePadding); + return getThis(); + } + + default W addTooltipElement(String s) { + tooltip().add(s); + return getThis(); + } + + default W addTooltipElement(IDrawable drawable) { + tooltip().add(drawable); + return getThis(); + } + + default W addTooltipLine(ITextLine line) { + tooltip().addLine(line); + return getThis(); + } + + /** + * Adds any drawable as a new line. Inlining elements is currently not possible. + * + * @param drawable drawable element. + * @return this + */ + default W addTooltipLine(IDrawable drawable) { + tooltip().add(drawable).newLine(); + return getThis(); + } + + /** + * Helper method to add a simple string as a line. Adds multiple lines if string contains \n. + * + * @param line text line + * @return this + */ + default W addTooltipLine(String line) { + return addTooltipLine(IKey.str(line)); + } + + /** + * Helper method to add multiple drawable lines. + * + * @param lines collection of drawable elements + * @return this + */ + default W addTooltipDrawableLines(Iterable lines) { + tooltip().addDrawableLines(lines); + return getThis(); + } + + /** + * Helper method to add multiple text lines. + * + * @param lines lines of text + * @return this + */ + default W addTooltipStringLines(Iterable lines) { + tooltip().addStringLines(lines); + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IValueWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IValueWidget.java new file mode 100644 index 00000000000..e3f7cf72915 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IValueWidget.java @@ -0,0 +1,14 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +/** + * Marks a widget as containing a value + * + * @param + */ +public interface IValueWidget extends IWidget { + + /** + * @return stored value + */ + T getWidgetValue(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IVanillaSlot.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IVanillaSlot.java new file mode 100644 index 00000000000..950b6aa86d9 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IVanillaSlot.java @@ -0,0 +1,16 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import net.minecraft.world.inventory.Slot; + +/** + * Marks a {@link IWidget} as containing a vanilla item slot. + */ +public interface IVanillaSlot { + + /** + * @return the item slot of this widget + */ + Slot getVanillaSlot(); + + boolean handleAsVanillaSlot(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java new file mode 100644 index 00000000000..a9af3f54904 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java @@ -0,0 +1,359 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.theme.WidgetThemeEntry; +import com.gregtechceu.gtceu.api.mui.utils.Point; +import com.gregtechceu.gtceu.api.mui.utils.Stencil; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Flex; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.gregtechceu.gtceu.utils.FormattingUtil; + +import com.google.common.base.CharMatcher; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * A widget in a GUI + */ +public interface IWidget extends IGuiElement { + + String WIDGET_TRANSLATION_KEY_FORMAT = "widget.%s.name"; + /** + * This char matcher is used to remove any non-{@code [a-z0-9_.-]} characters in translation keys. + * In essence, it + */ + CharMatcher DISALLOWED_TRANSLATION_KEY_CHARS = CharMatcher.inRange('a', 'z') + .or(CharMatcher.inRange('0', '9')) + .or(CharMatcher.anyOf("-_.")) + .negate(); + + /** + * Validates and initialises this element. + * This element now becomes valid + * + * @param parent the parent this element belongs to + * @param late true if this is called some time after the widget tree of the parent has been initialised + */ + void initialise(@NotNull IWidget parent, boolean late); + + /** + * Invalidates this element. + */ + void dispose(); + + /** + * Determines if this element exist in an active gui. + * + * @return if this is in a valid gui + */ + boolean isValid(); + + /** + * Draws the background of this widget. + * + * @param context gui context + * @param widgetTheme widget theme of this widget + */ + void drawBackground(ModularGuiContext context, WidgetThemeEntry widgetTheme); + + /** + * Draws additional stuff in this widget. + * x = 0 and y = 0 is now in the top left corner of this widget. + * Do NOT override this method, it is never called. Use {@link #draw(ModularGuiContext, WidgetThemeEntry)} instead. + * + * @param context gui context + */ + @ApiStatus.NonExtendable + @Deprecated + @Override + default void draw(ModularGuiContext context) { + draw(context, getWidgetTheme(context.getTheme())); + } + + /** + * Draws extra elements of this widget. Called after {@link #drawBackground(ModularGuiContext, WidgetThemeEntry)} + * and + * before + * {@link #drawOverlay(ModularGuiContext, WidgetThemeEntry)} + * + * @param context gui context + * @param widgetTheme widget theme + */ + void draw(ModularGuiContext context, WidgetThemeEntry widgetTheme); + + /** + * Draws the overlay of this widget. + * + * @param context gui context + * @param widgetTheme widget theme + */ + void drawOverlay(ModularGuiContext context, WidgetThemeEntry widgetTheme); + + /** + * Draws foreground elements of this widget. For example tooltips. + * No transformations are applied here. + * + * @param context gui context + */ + void drawForeground(ModularGuiContext context); + + default void transform(IViewportStack stack) { + stack.translate(getArea().rx, getArea().ry, 0); + } + + default Object getAdditionalHoverInfo(IViewportStack viewportStack, int mouseX, int mouseY) { + return null; + } + + default WidgetThemeEntry getWidgetTheme(ITheme theme) { + return theme.getFallback(); + } + + /** + * Called 20 times per second. + */ + void onUpdate(); + + /** + * @return the area this widget occupies + */ + @Override + Area getArea(); + + default String getTranslationId() { + String className = FormattingUtil.toLowerCaseUnderscore(this.getClass().getSimpleName()); + className = DISALLOWED_TRANSLATION_KEY_CHARS.removeFrom(className); + return WIDGET_TRANSLATION_KEY_FORMAT.formatted(className); + } + + /** + * Calculates if a given pos is inside this widgets area. + * This should be used over {@link Area#isInside(int, int)}, since this accounts for transformations. + * + * @param stack viewport stack + * @param mx x pos + * @param my y pos + * @return if pos is inside this widgets area + */ + default boolean isInside(IViewportStack stack, int mx, int my) { + return isInside(stack, mx, my, true); + } + + /** + * Calculates if a given pos is inside this widgets area. + * This should be used over {@link Area#isInside(int, int)}, since this accounts for transformations. + * + * @param stack viewport stack + * @param mx x pos + * @param my y pos + * @param absolute true if the position is absolute or relative to the current stack transform otherwise + * @return if pos is inside this widgets area + */ + default boolean isInside(IViewportStack stack, int mx, int my, boolean absolute) { + int x = mx; + int y = my; + if (absolute) { + x = stack.unTransformX(mx, my); + y = stack.unTransformY(mx, my); + } + return x >= 0 && x < getArea().w() && y >= 0 && y < getArea().h(); + } + + /** + * Calculates if a given pos is inside this widgets area. + * This should be used over {@link Area#isInside(int, int)}, since this accounts for transformations. + * + * @param stack viewport stack + * @param point position + * @return if pos is inside this widgets area + */ + default boolean isInside(IViewportStack stack, Point point) { + return isInside(stack, point.x, point.y); + } + + /** + * @return all children of this widget + */ + @NotNull + default List getChildren() { + return Collections.emptyList(); + } + + /** + * @return if this widget has any children + */ + default boolean hasChildren() { + return !getChildren().isEmpty(); + } + + /** + * @return the panel this widget is in + */ + @NotNull + ModularPanel getPanel(); + + /** + * Returns if this element is enabled. Disabled elements are not drawn and can not be interacted with. If this is + * disabled, the children + * will be considered disabled to without actually being disabled. + * + * @return if this element is enabled + */ + @Override + boolean isEnabled(); + + void setEnabled(boolean enabled); + + /** + * Checks if all ancestors are enabled. Only then this widget is visible and interactable. + * + * @return if all ancestors are enabled. + */ + default boolean areAncestorsEnabled() { + IWidget parent = this; + do { + if (!parent.isEnabled()) return false; + parent = parent.getParent(); + } while (parent.hasParent()); + return true; + } + + /** + * If this widget can be seen on the screen even partly. If this returns false it will be culled. This is visually + * only! + * + * @param stack viewport stack + * @return false if this widget can not be seen currently and should not be drawn + */ + default boolean canBeSeen(IViewportStack stack) { + return Stencil.isInsideScissorArea(getArea(), stack); + } + + /** + * Determines if this widget can have any hover interaction. Interactions with mouse or keyboard like clicks ignore + * this. + * This is useful, when you have a widget which changes its background when hovered or has a tooltip and some + * decoration child. Normally + * you can click through the child, but while you hover it the widget will not show its tooltip etc. To change that + * return false here. + * + * @return if this widget can have any hover interaction + */ + default boolean canHover() { + return true; + } + + /** + * Determines if widgets below this can receive a click callback. This is only called when this widget didn't + * consume the click. + * + * @return if widgets below this should be able to receive a click + */ + default boolean canClickThrough() { + return true; + } + + default boolean canHoverThrough() { + return false; + } + + /** + * Marks tooltip for this widget as dirty. + */ + void markTooltipDirty(); + + /** + * @return the parent of this widget + */ + @NotNull + IWidget getParent(); + + @Override + default boolean hasParent() { + return isValid(); + } + + /** + * @return the context of the current screen + */ + ModularGuiContext getContext(); + + /** + * @return flex of this widget. Creates a new one if it doesn't already have one. + */ + Flex flex(); + + /** + * Does the same as {@link IPositioned#flex(Consumer)} + * + * @param builder function to build flex + * @return this + */ + default IWidget flexBuilder(Consumer builder) { + builder.accept(flex()); + return this; + } + + /** + * @return resizer of this widget + */ + @NotNull + @Override + IResizeable resizer(); + + /** + * Sets the resizer of this widget. + * + * @param resizer resizer + */ + void resizer(IResizeable resizer); + + /** + * Called before a widget is resized. + */ + default void beforeResize(boolean onOpen) {} + + /** + * Called after a widget is fully resized. + */ + default void onResized() {} + + /** + * Called after the full widget tree is resized and the absolute positions are calculated. + */ + default void postResize() {} + + /** + * @return flex of this widget + */ + Flex getFlex(); + + default boolean isExpanded() { + Flex flex = getFlex(); + return flex != null && flex.isExpanded(); + } + + @Nullable + String getName(); + + default boolean isName(String name) { + return name.equals(getName()); + } + + default boolean isType(Class type) { + return type.isAssignableFrom(getClass()); + } + + default boolean isNameAndType(String name, Class type) { + return isName(name) && isType(type); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java new file mode 100644 index 00000000000..5bd576ae2ce --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java @@ -0,0 +1,232 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.mojang.blaze3d.platform.InputConstants; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.glfw.GLFW; + +/** + * An interface that handles user interactions on {@link IWidget} objects. + * These methods get called on the client + */ +public interface Interactable { + + /** + * Called when this widget is pressed. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param button mouse button that was pressed. + * @return result that determines what happens to other widgets + * {@link #onMouseTapped(double, double, int)} is only called if this returns {@link Result#ACCEPT} or + * {@link Result#SUCCESS} + */ + @NotNull + default Result onMousePressed(double mouseX, double mouseY, int button) { + return Result.ACCEPT; + } + + /** + * Called when a mouse button was released over this widget. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param button mouse button that was released. + * @return whether other widgets should get called to. If this returns false, + * {@link #onMouseTapped(double, double, int)} will NOT be called. + */ + default boolean onMouseReleased(double mouseX, double mouseY, int button) { + return false; + } + + /** + * Called when this widget was pressed and then released within a certain time frame. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param button mouse button that was pressed. + * @return result that determines if other widgets should get tapped to + * {@link Result#IGNORE IGNORE} and {@link Result#ACCEPT ACCEPT} will both "ignore" the result and + * {@link Result#STOP STOP} and {@link Result#SUCCESS SUCCESS} will both stop other widgets + * from getting tapped. + */ + @NotNull + default Result onMouseTapped(double mouseX, double mouseY, int button) { + return Result.IGNORE; + } + + /** + * Called when a key over this widget is pressed. + * + * @param keyCode key that was pressed. + * @param scanCode character that was pressed. + * @param modifiers any modifiers that were used. + * @return result that determines what happens to other widgets + * {@link #onKeyTapped(int, int, int)} is only called if this returns {@link Result#ACCEPT} or + * {@link Result#SUCCESS} + */ + @NotNull + default Result onKeyPressed(int keyCode, int scanCode, int modifiers) { + return Result.IGNORE; + } + + /** + * Called when a key was released over this widget. + * + * @param keyCode key that was pressed. + * @param scanCode character that was pressed. + * @param modifiers any modifiers that were used. + * @return whether other widgets should get called too. If this returns false, {@link #onKeyTapped(int, int, int)} + * will NOT be called. + */ + default boolean onKeyReleased(int keyCode, int scanCode, int modifiers) { + return false; + } + + /** + * Called when this widget was pressed and then released within a certain time frame. + * + * @param keyCode key that was pressed. + * @param scanCode character that was pressed. + * @param modifiers any modifiers that were used. + * @return result that determines if other widgets should get tapped to + * {@link Result#IGNORE} and {@link Result#ACCEPT} will both "ignore" the result and {@link Result#STOP} and + * {@link Result#SUCCESS} will both stop other widgets from getting tapped. + */ + @NotNull + default Result onKeyTapped(int keyCode, int scanCode, int modifiers) { + return Result.IGNORE; + } + + /** + * Called when a key over this widget is pressed. + * + * @param codePoint character that was typed + * @param modifiers any modifiers that were used. + * @return result that determines what happens to other widgets + * {@link #onKeyTapped(int, int, int)} is only called if this returns {@link Result#ACCEPT} or + * {@link Result#SUCCESS} + */ + @NotNull + default Result onCharTyped(char codePoint, int modifiers) { + return Result.IGNORE; + } + + /** + * Called when this widget is focused or when the mouse is above this widget. + * This method should return true if it can scroll at all and not if it scrolled right now. + * If this scroll view scrolled to the end and this returns false, the scroll will get passed through another scroll + * view below this. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param delta amount scrolled by (usually irrelevant) + * @return true if this widget can be scrolled at all + */ + default boolean onMouseScrolled(double mouseX, double mouseY, double delta) { + return false; + } + + /** + * Called when this widget was clicked and mouse is now dragging. + * + * @param mouseX current mouse X coordinate relative to the screen + * @param mouseY current mouse Y coordinate relative to the screen + * @param button mouse button that is held down + * (0 = left button, 1 = right button, 2 = scroll button, 4 and 5 = side buttons) + * @param dragX amount of drag on the X axis (e.g. the distance that has been dragged) + * @param dragY amount of drag on the Y axis (e.g. the distance that has been dragged) + */ + default void onMouseDrag(double mouseX, double mouseY, int button, double dragX, double dragY) {} + + /** + * @return if left or right ctrl/cmd is pressed + */ + @OnlyIn(Dist.CLIENT) + static boolean hasControlDown() { + return Screen.hasControlDown(); + } + + /** + * @return if left or right shift is pressed + */ + @OnlyIn(Dist.CLIENT) + static boolean hasShiftDown() { + return Screen.hasShiftDown(); + } + + /** + * @return if alt or alt gr is pressed + */ + @OnlyIn(Dist.CLIENT) + static boolean hasAltDown() { + return Screen.hasAltDown(); + } + + static boolean isModifierActive(int mod, int key) { + return (mod & key) != 0; + } + + static boolean isControl(int mod) { + return isModifierActive(mod, GLFW.GLFW_MOD_CONTROL); + } + + static boolean isShift(int mod) { + return isModifierActive(mod, GLFW.GLFW_MOD_SHIFT); + } + + static boolean isAlt(int mod) { + return isModifierActive(mod, GLFW.GLFW_MOD_ALT); + } + + /** + * @param key key id, see {@link InputConstants} + * @return if the key is pressed + */ + @OnlyIn(Dist.CLIENT) + static boolean isKeyPressed(int key) { + return InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), key); + } + + /** + * Plays the default button click sound + */ + @OnlyIn(Dist.CLIENT) + static void playButtonClickSound() { + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + } + + enum Result { + + /** + * Nothing happens. + */ + IGNORE(false, false), + /** + * Interaction is accepted, but other widgets will get checked. + */ + ACCEPT(true, false), + /** + * Interaction is rejected and no other widgets will get checked. + */ + STOP(false, true), + /** + * Interaction is accepted and no other widgets will get checked. + */ + SUCCESS(true, true); + + public final boolean accepts; + public final boolean stops; + + Result(boolean accepts, boolean stops) { + this.accepts = accepts; + this.stops = stops; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ResizeDragArea.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ResizeDragArea.java new file mode 100644 index 00000000000..afe811efdbf --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ResizeDragArea.java @@ -0,0 +1,23 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +public enum ResizeDragArea { + + TOP_LEFT(true, true, false, false), + TOP_RIGHT(true, false, false, true), + BOTTOM_LEFT(false, true, true, false), + BOTTOM_RIGHT(false, false, true, true), + TOP(true, false, false, false), + LEFT(false, true, false, false), + BOTTOM(false, false, true, false), + RIGHT(false, false, false, true); + + public final boolean top, left, bottom, right; + + ResizeDragArea(boolean top, boolean left, boolean bottom, boolean right) { + if (top && bottom || left && right) throw new IllegalArgumentException(); + this.top = top; + this.left = left; + this.bottom = bottom; + this.right = right; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/AdaptableUITexture.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/AdaptableUITexture.java new file mode 100644 index 00000000000..e568335764a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/AdaptableUITexture.java @@ -0,0 +1,226 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; + +import com.google.gson.JsonObject; +import com.mojang.blaze3d.systems.RenderSystem; +import org.joml.Matrix4f; + +/** + * This class is a
9-slice texture. It can be created using + * {@link UITexture.Builder#adaptable(int, int, int, int)}. + */ +public class AdaptableUITexture extends UITexture { + + private final int imageWidth, imageHeight, bl, bt, br, bb; + private final boolean tiled; + + /** + * Use {@link UITexture#builder()} with {@link Builder#adaptable(int, int)} + */ + AdaptableUITexture(ResourceLocation location, float u0, float v0, float u1, float v1, ColorType colorType, + boolean nonOpaque, int imageWidth, int imageHeight, int bl, int bt, int br, int bb, + boolean tiled) { + super(location, u0, v0, u1, v1, colorType, nonOpaque); + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.bl = bl; + this.bt = bt; + this.br = br; + this.bb = bb; + this.tiled = tiled; + } + + @Override + public AdaptableUITexture getSubArea(float uStart, float vStart, float uEnd, float vEnd) { + return new AdaptableUITexture(this.location, lerpU(uStart), lerpV(vStart), lerpU(uEnd), lerpV(vEnd), + this.colorType, this.nonOpaque, this.imageWidth, this.imageHeight, this.bl, this.bt, this.br, this.bb, + this.tiled); + } + + @Override + public void draw(GuiContext context, float x, float y, float width, float height) { + if (width == this.imageWidth && height == this.imageHeight) { + super.draw(context, x, y, width, height); + return; + } + if (this.tiled) { + drawTiled(context, x, y, width, height); + } else { + drawStretched(context, x, y, width, height); + } + } + + public void drawStretched(GuiContext context, float x, float y, float width, float height) { + Matrix4f pose = context.getLastGraphicsPose(); + + if (this.bl <= 0 && this.bt <= 0 && this.br <= 0 && this.bb <= 0) { + super.draw(context, x, y, width, height); + return; + } + if (this.nonOpaque) { + RenderSystem.enableBlend(); + } else { + RenderSystem.disableBlend(); + } + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, this.location); + + float uBl = this.bl * 1f / this.imageWidth, uBr = this.br * 1f / this.imageWidth; + float vBt = this.bt * 1f / this.imageHeight, vBb = this.bb * 1f / this.imageHeight; + float x1 = x + width, y1 = y + height; + float uInnerStart = this.u0 + uBl, vInnerStart = this.v0 + vBt, uInnerEnd = this.u1 - uBr, + vInnerEnd = this.v1 - vBb; + + if ((this.bl > 0 || this.br > 0) && this.bt <= 0 && this.bb <= 0) { + // left border + GuiDraw.drawTexture(pose, x, y, x + this.bl, y1, this.u0, this.v0, uInnerStart, this.v1); + // right border + GuiDraw.drawTexture(pose, x1 - this.br, y, x1, y1, uInnerEnd, this.v0, this.u1, this.v1); + // center + GuiDraw.drawTexture(pose, x + this.bl, y, x1 - this.br, y1, uInnerStart, this.v0, uInnerEnd, this.v1); + } else if (this.bl <= 0 && this.br <= 0) { + // top border + GuiDraw.drawTexture(pose, x, y, x1, y + this.bt, this.u0, this.v0, this.u1, vInnerStart); + // bottom border + GuiDraw.drawTexture(pose, x, y1 - this.bb, x1, y1, this.u0, vInnerEnd, this.u1, this.v1); + // center + GuiDraw.drawTexture(pose, x, y + this.bt, x1, y1 - this.bb, this.u0, vInnerStart, this.u1, vInnerEnd); + } else { + // top left corner + GuiDraw.drawTexture(pose, x, y, x + this.bl, y + this.bt, this.u0, this.v0, uInnerStart, vInnerStart); + // top right corner + GuiDraw.drawTexture(pose, x1 - this.br, y, x1, y + this.bt, uInnerEnd, this.v0, this.u1, vInnerStart); + // bottom left corner + GuiDraw.drawTexture(pose, x, y1 - this.bb, x + this.bl, y1, this.u0, vInnerEnd, uInnerStart, this.v1); + // bottom right corner + GuiDraw.drawTexture(pose, x1 - this.br, y1 - this.bb, x1, y1, uInnerEnd, vInnerEnd, this.u1, this.v1); + + // left border + GuiDraw.drawTexture(pose, x, y + this.bt, x + this.bl, y1 - this.bb, this.u0, vInnerStart, uInnerStart, + vInnerEnd); + // top border + GuiDraw.drawTexture(pose, x + this.bl, y, x1 - this.br, y + this.bt, uInnerStart, this.v0, uInnerEnd, + vInnerStart); + // right border + GuiDraw.drawTexture(pose, x1 - this.br, y + this.bt, x1, y1 - this.bb, uInnerEnd, vInnerStart, this.u1, + vInnerEnd); + // bottom border + GuiDraw.drawTexture(pose, x + this.bl, y1 - this.bb, x1 - this.br, y1, uInnerStart, vInnerEnd, uInnerEnd, + this.v1); + + // center + GuiDraw.drawTexture(pose, x + this.bl, y + this.bt, x1 - this.br, y1 - this.bb, uInnerStart, vInnerStart, + uInnerEnd, vInnerEnd); + } + RenderSystem.disableBlend(); + } + + public void drawTiled(GuiContext context, float x, float y, float width, float height) { + Matrix4f pose = context.getLastGraphicsPose(); + + if (this.bl <= 0 && this.bt <= 0 && this.br <= 0 && this.bb <= 0) { + GuiDraw.drawTiledTexture(pose, this.location, x, y, width, height, this.u0, this.v0, this.u1, this.v1, + this.imageWidth, this.imageHeight, 0); + return; + } + if (this.nonOpaque) { + RenderSystem.enableBlend(); + } else { + RenderSystem.disableBlend(); + } + RenderSystem.setShader(GameRenderer::getPositionTexShader); + RenderSystem.setShaderTexture(0, this.location); + + float uBl = this.bl * 1f / this.imageWidth, uBr = this.br * 1f / this.imageWidth; + float vBt = this.bt * 1f / this.imageHeight, vBb = this.bb * 1f / this.imageHeight; + float x1 = x + width, y1 = y + height; + float uInnerStart = this.u0 + uBl, vInnerStart = this.v0 + vBt, uInnerEnd = this.u1 - uBr, + vInnerEnd = this.v1 - vBb; + + int tw = (int) (this.imageWidth * (this.u1 - this.u0)); + int th = (int) (this.imageHeight * (this.v1 - this.v0)); + + if ((this.bl > 0 || this.br > 0) && this.bt <= 0 && this.bb <= 0) { + // left border + GuiDraw.drawTiledTexture(pose, x, y, this.bl, height, this.u0, this.v0, uInnerStart, this.v1, this.bl, th, + 0); + // right border + GuiDraw.drawTiledTexture(pose, x1 - this.br, y, this.br, height, uInnerEnd, this.v0, this.u1, this.v1, + this.br, th, 0); + // center + GuiDraw.drawTiledTexture(pose, x + this.bl, y, width - this.bl - this.br, height, uInnerStart, this.v0, + uInnerEnd, this.v1, tw - this.bl - this.br, th, 0); + } else if (this.bl <= 0 && this.br <= 0) { + // top border + GuiDraw.drawTiledTexture(pose, x, y, width, this.bt, this.u0, this.v0, this.u1, vInnerStart, tw, this.bt, + 0); + // bottom border + GuiDraw.drawTiledTexture(pose, x, y1 - this.bb, width, this.bb, this.u0, vInnerEnd, this.u1, this.v1, tw, + this.bb, 0); + // center + GuiDraw.drawTiledTexture(pose, x, y + this.bt, width, height - this.bt - this.bb, this.u0, vInnerStart, + this.u1, vInnerEnd, tw, th - this.bt - this.bb, 0); + } else { + // top left corner + GuiDraw.drawTiledTexture(pose, x, y, this.bl, this.bt, this.u0, this.v0, uInnerStart, vInnerStart, this.bl, + this.bt, 0); + // top right corner + GuiDraw.drawTiledTexture(pose, x1 - this.br, y, this.br, this.bt, uInnerEnd, this.v0, this.u1, vInnerStart, + this.br, this.bt, 0); + // bottom left corner + GuiDraw.drawTiledTexture(pose, x, y1 - this.bb, this.bl, this.bb, this.u0, vInnerEnd, uInnerStart, this.v1, + this.bl, this.bb, 0); + // bottom right corner + GuiDraw.drawTiledTexture(pose, x1 - this.br, y1 - this.bb, this.br, this.bb, uInnerEnd, vInnerEnd, this.u1, + this.v1, this.br, this.bb, 0); + + // left border + GuiDraw.drawTiledTexture(pose, x, y + this.bt, this.bl, height - this.bt - this.bb, this.u0, vInnerStart, + uInnerStart, vInnerEnd, this.bl, th - this.bt - this.bb, 0); + // top border + GuiDraw.drawTiledTexture(pose, x + this.bl, y, width - this.bl - this.br, this.bt, uInnerStart, this.v0, + uInnerEnd, vInnerStart, tw - this.bl - this.bb, this.bt, 0); + // right border + GuiDraw.drawTiledTexture(pose, x1 - this.br, y + this.bt, this.br, height - this.bt - this.bb, uInnerEnd, + vInnerStart, this.u1, vInnerEnd, this.br, th - this.bt - this.bb, 0); + // bottom border + GuiDraw.drawTiledTexture(pose, x + this.bl, y1 - this.bb, width - this.bl - this.br, this.bb, uInnerStart, + vInnerEnd, uInnerEnd, this.v1, tw - this.bl - this.br, this.bb, 0); + + // center + GuiDraw.drawTiledTexture(pose, x + this.bl, y + this.bt, width - this.bl - this.br, + height - this.bt - this.bb, uInnerStart, vInnerStart, uInnerEnd, vInnerEnd, + tw - this.bl - this.br, th - this.bt - this.bb, 0); + } + RenderSystem.disableBlend(); + } + + @Override + public boolean saveToJson(JsonObject json) { + super.saveToJson(json); + if (json.entrySet().size() == 1) return true; + json.addProperty("imageWidth", this.imageWidth); + json.addProperty("imageHeight", this.imageHeight); + json.addProperty("bl", this.bl); + json.addProperty("br", this.br); + json.addProperty("bt", this.bt); + json.addProperty("bb", this.bb); + json.addProperty("tiled", this.tiled); + return true; + } + + @Override + protected AdaptableUITexture copy() { + return new AdaptableUITexture(location, u0, v0, u1, v1, colorType, nonOpaque, imageWidth, imageHeight, bl, bt, + br, bb, tiled); + } + + @Override + public AdaptableUITexture withColorOverride(int color) { + return (AdaptableUITexture) super.withColorOverride(color); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Circle.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Circle.java new file mode 100644 index 00000000000..73f6a24acfe --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Circle.java @@ -0,0 +1,94 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.animation.IAnimatable; +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.utils.Interpolations; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.google.gson.JsonObject; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Accessors(fluent = true, chain = true) +public class Circle implements IDrawable, IJsonSerializable, IAnimatable { + + @Setter + private int colorInner, colorOuter, segments; + + public Circle() { + this.colorInner = 0; + this.colorOuter = 0; + this.segments = 40; + } + + public Circle setColorInner(int colorInner) { + return colorInner(colorInner); + } + + public Circle setColorOuter(int colorOuter) { + return colorOuter(colorOuter); + } + + public Circle setColor(int inner, int outer) { + return color(inner, outer); + } + + public Circle setSegments(int segments) { + return segments(segments); + } + + public Circle color(int inner, int outer) { + this.colorInner = inner; + this.colorOuter = outer; + return this; + } + + public Circle color(int color) { + return color(color, color); + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(GuiContext context, int x0, int y0, int width, int height, WidgetTheme widgetTheme) { + applyColor(widgetTheme.getColor()); + GuiDraw.drawEllipse(context.getGraphics(), x0, y0, width, height, + this.colorInner, this.colorOuter, this.segments); + } + + @Override + public void loadFromJson(JsonObject json) { + this.colorInner = JsonHelper.getColor(json, Color.WHITE.main, "colorInner", "color"); + this.colorOuter = JsonHelper.getColor(json, Color.WHITE.main, "colorOuter", "color"); + this.segments = JsonHelper.getInt(json, 40, "segments"); + } + + @Override + public boolean saveToJson(JsonObject json) { + json.addProperty("colorInner", this.colorInner); + json.addProperty("colorOuter", this.colorOuter); + json.addProperty("segments", this.segments); + return true; + } + + @Override + public Circle interpolate(Circle start, Circle end, float t) { + this.colorInner = Color.lerp(start.colorInner, end.colorInner, t); + this.colorOuter = Color.lerp(start.colorOuter, end.colorOuter, t); + this.segments = Interpolations.lerp(start.segments, end.segments, t); + return this; + } + + @Override + public Circle copyOrImmutable() { + return new Circle() + .setColor(this.colorInner, this.colorOuter) + .setSegments(this.segments); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/ColorType.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/ColorType.java new file mode 100644 index 00000000000..865d312d993 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/ColorType.java @@ -0,0 +1,36 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import lombok.Getter; + +import java.util.Map; +import java.util.function.ToIntFunction; + +public class ColorType { + + private static final Map COLOR_TYPES = new Object2ObjectOpenHashMap<>(); + + public static ColorType get(String name) { + return COLOR_TYPES.getOrDefault(name, DEFAULT); + } + + public static final ColorType DEFAULT = new ColorType("default", WidgetTheme::getColor); + public static final ColorType TEXT = new ColorType("text", WidgetTheme::getTextColor); + public static final ColorType ICON = new ColorType("default", WidgetTheme::getIconColor); + + @Getter + private final String name; + private final ToIntFunction colorGetter; + + public ColorType(String name, ToIntFunction colorGetter) { + this.name = name; + this.colorGetter = colorGetter; + COLOR_TYPES.put(name, this); + } + + public int getColor(WidgetTheme theme) { + return colorGetter.applyAsInt(theme); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DelegateDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DelegateDrawable.java new file mode 100644 index 00000000000..d451834f7ed --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DelegateDrawable.java @@ -0,0 +1,64 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class DelegateDrawable implements IDrawable { + + @NotNull + private IDrawable drawable; + + public DelegateDrawable(@Nullable IDrawable drawable) { + setDrawable(drawable); + } + + // protected, so subclasses can define mutability + protected void setDrawable(@Nullable IDrawable drawable) { + this.drawable = drawable != null ? drawable : IDrawable.EMPTY; + } + + @NotNull + public IDrawable getWrappedDrawable() { + return drawable; + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + this.drawable.draw(context, x, y, width, height, widgetTheme); + } + + @Override + public boolean canApplyTheme() { + return this.drawable.canApplyTheme(); + } + + @Override + public void applyColor(int themeColor) { + this.drawable.applyColor(themeColor); + } + + @Override + public int getDefaultWidth() { + return this.drawable.getDefaultWidth(); + } + + @Override + public int getDefaultHeight() { + return this.drawable.getDefaultHeight(); + } + + @Override + public Widget asWidget() { + return this.drawable.asWidget(); + } + + @Override + public Icon asIcon() { + return this.drawable.asIcon(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DelegateIcon.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DelegateIcon.java new file mode 100644 index 00000000000..fd8ac8e3827 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DelegateIcon.java @@ -0,0 +1,61 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IIcon; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Box; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +public class DelegateIcon implements IIcon { + + private IIcon icon; + + public DelegateIcon(IIcon icon) { + this.icon = icon; + } + + @Override + public int getWidth() { + return this.icon.getWidth(); + } + + @Override + public int getHeight() { + return this.icon.getHeight(); + } + + @Override + public Box getMargin() { + return this.icon.getMargin(); + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + this.icon.draw(context, x, y, width, height, widgetTheme); + } + + @Override + public IIcon getWrappedDrawable() { + return icon; + } + + public IIcon getDelegate() { + return icon; + } + + public IIcon findRootDelegate() { + IIcon icon = this; + while (icon instanceof DelegateIcon di) { + icon = di.getDelegate(); + } + return icon; + } + + protected void setDelegate(IIcon icon) { + this.icon = icon; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + this.icon + ")"; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DrawableSerialization.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DrawableSerialization.java new file mode 100644 index 00000000000..410a9d7588b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DrawableSerialization.java @@ -0,0 +1,223 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +import com.google.gson.*; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.function.Function; + +public class DrawableSerialization implements JsonSerializer, JsonDeserializer { + + private static final Map> DRAWABLE_TYPES = new Object2ObjectOpenHashMap<>(); + private static final Map, String> REVERSE_DRAWABLE_TYPES = new Object2ObjectOpenHashMap<>(); + private static final Map TEXTURES = new Object2ObjectOpenHashMap<>(); + private static final Map REVERSE_TEXTURES = new Object2ObjectOpenHashMap<>(); + + public static void registerTexture(String s, UITexture texture) { + TEXTURES.put(s, texture); + REVERSE_TEXTURES.put(texture, s); + } + + public static UITexture getTexture(String s) { + return TEXTURES.get(s); + } + + public static String getTextureId(UITexture texture) { + return REVERSE_TEXTURES.get(texture); + } + + public static < + T extends IDrawable & IJsonSerializable> void registerDrawableType(String id, Class type, + Function<@NotNull JsonObject, ? extends @NotNull IDrawable> creator) { + if (DRAWABLE_TYPES.containsKey(id)) { + throw new IllegalArgumentException("Drawable type '" + id + "' already exists!"); + } + DRAWABLE_TYPES.put(id, creator); + if (type != null) { + REVERSE_DRAWABLE_TYPES.put(type, id); + } + } + + @ApiStatus.Internal + public static void init() { + // empty, none and text are special cases + registerDrawableType("texture", UITexture.class, UITexture::parseFromJson); + registerDrawableType("color", Rectangle.class, json -> new Rectangle()); + registerDrawableType("rectangle", Rectangle.class, json -> new Rectangle()); + registerDrawableType("ellipse", Circle.class, json -> new Circle()); + registerDrawableType("item", ItemDrawable.class, ItemDrawable::ofJson); + registerDrawableType("icon", Icon.class, Icon::ofJson); + registerDrawableType("stack", DrawableStack.class, DrawableStack::parseJson); + registerDrawableType("scrollbar", Scrollbar.class, Scrollbar::ofJson); + } + + public static IDrawable deserialize(JsonElement json) { + return JsonHelper.deserialize(json, IDrawable.class); + } + + public static JsonElement serialize(IDrawable drawable) { + return JsonHelper.serialize(drawable); + } + + @Override + public IDrawable deserialize(JsonElement element, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { + if (element.isJsonNull()) { + return IDrawable.EMPTY; + } + if (element.isJsonPrimitive()) { + if ("empty".equals(element.getAsString()) || "null".equals(element.getAsString())) { + return IDrawable.EMPTY; + } + if ("none".equals(element.getAsString())) { + return IDrawable.NONE; + } + } + if (element.isJsonArray()) { + return DrawableStack.parseJson(element.getAsJsonArray()); + } + if (!element.isJsonObject()) { + GTCEu.LOGGER.throwing(new JsonParseException("Drawable json should be an object or an array.")); + return IDrawable.EMPTY; + } + JsonObject json = element.getAsJsonObject(); + if (json.entrySet().isEmpty()) { + return IDrawable.EMPTY; + } + String type = JsonHelper.getString(json, "empty", "type"); + if ("text".equals(type)) { + IKey key = parseText(json); + key.loadFromJson(json); + return key; + } + if (!DRAWABLE_TYPES.containsKey(type)) { + GTCEu.LOGGER + .throwing(new JsonParseException("Drawable type '" + type + "' is not json serializable!")); + return IDrawable.EMPTY; + } + IDrawable drawable = DRAWABLE_TYPES.get(type).apply(json); + ((IJsonSerializable) drawable).loadFromJson(json); + return drawable; + } + + @Override + public JsonElement serialize(IDrawable src, Type typeOfSrc, JsonSerializationContext context) { + if (src == IDrawable.EMPTY) return JsonNull.INSTANCE; + if (src == IDrawable.NONE) return new JsonPrimitive("none"); + if (src instanceof DrawableStack drawableStack) { + JsonArray jsonArray = new JsonArray(); + for (IDrawable drawable : drawableStack.getDrawables()) { + jsonArray.add(JsonHelper.serialize(drawable)); + } + return jsonArray; + } + JsonObject json = new JsonObject(); + if (src instanceof IKey key) { + json.addProperty("type", "text"); + // TODO serialize text properly + json.addProperty("text", Component.Serializer.toJson(key.getFormatted())); + } else if (!(src instanceof IJsonSerializable serializable)) { + throw new IllegalArgumentException("Can't serialize IDrawable which doesn't implement IJsonSerializable!"); + } else { + Class type = src.getClass(); + String key = REVERSE_DRAWABLE_TYPES.get(type); + while (key == null && type != null && type != Object.class) { + type = type.getSuperclass(); + key = REVERSE_DRAWABLE_TYPES.get(type); + } + if (key == null) { + GTCEu.LOGGER.error( + "Serialization of drawable {} failed, because a key for the type could not be found!", + src.getClass().getSimpleName()); + return JsonNull.INSTANCE; + } + json.addProperty("type", key); + if (!serializable.saveToJson(json)) { + GTCEu.LOGGER.error("Serialization of drawable {} failed!", src.getClass().getSimpleName()); + } + } + return json; + } + + private static IKey parseText(JsonObject json) throws JsonParseException { + JsonParseException exception = new JsonParseException("Could not parse IKey from %s".formatted(json)); + try { + MutableComponent component = Component.Serializer.fromJson(json); + if (component != null) { + return unpackSiblings(component); + } + } catch (JsonSyntaxException e) { + exception = e; + } + JsonElement element = JsonHelper.getJsonElement(json, "text", "string", "key"); + if (element == null || element.isJsonNull()) { + return IKey.str("No text found!"); + } else if (element.isJsonPrimitive()) { + String s = element.getAsString(); + if (s.startsWith("translate:")) { + return IKey.lang(s.substring(10)); + } + return JsonHelper.getBoolean(json, false, "lang", "translate") ? IKey.lang(s) : IKey.str(s); + } else if (element.isJsonArray()) { + ObjectArrayList strings = new ObjectArrayList<>(); + for (JsonElement element1 : element.getAsJsonArray()) { + strings.add(parseText(element1)); + } + strings.trim(); + return IKey.comp(strings.elements()); + } + throw exception; + } + + private static IKey parseText(JsonElement element) throws JsonParseException { + JsonParseException exception = new JsonParseException("Could not parse IKey from %s".formatted(element)); + try { + MutableComponent component = Component.Serializer.fromJson(element); + if (component != null) { + return IKey.lang(component); + } + } catch (JsonSyntaxException e) { + exception = e; + } + if (element.isJsonPrimitive()) { + String s = element.getAsString(); + if (s.startsWith("translate:")) { + return IKey.lang(s.substring(10)); + } + return IKey.str(s); + } + if (element.isJsonObject()) { + return parseText(element.getAsJsonObject()); + } + throw exception; + } + + private static IKey parseKeyFromJson(JsonObject json, Function keyFunction) { + return keyFunction.apply(JsonHelper.getString(json, "No text found!", "text", "string", "key")); + } + + private static IKey unpackSiblings(Component component) { + if (component.getSiblings().isEmpty()) { + return IKey.lang(component); + } + ObjectArrayList siblings = new ObjectArrayList<>(); + for (Component sibling : component.getSiblings()) { + siblings.add(unpackSiblings(sibling)); + } + siblings.trim(); + return IKey.comp(siblings.elements()); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DrawableStack.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DrawableStack.java new file mode 100644 index 00000000000..49aea3410a1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DrawableStack.java @@ -0,0 +1,92 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +/** + * A stack of {@link IDrawable} backed by an array which are drawn on top of each other. + */ +public class DrawableStack implements IDrawable, IJsonSerializable { + + public static final IDrawable[] EMPTY_BACKGROUND = {}; + public static final DrawableStack EMPTY = new DrawableStack(EMPTY_BACKGROUND); + + @Getter + private final IDrawable[] drawables; + + public DrawableStack(IDrawable... drawables) { + this.drawables = drawables == null || drawables.length == 0 ? EMPTY_BACKGROUND : drawables; + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + for (IDrawable drawable : this.drawables) { + if (drawable != null) drawable.draw(context, x, y, width, height, widgetTheme); + } + } + + @Override + public boolean canApplyTheme() { + for (IDrawable drawable : this.drawables) { + if (drawable != null && drawable.canApplyTheme()) { + return true; + } + } + return false; + } + + public static IDrawable parseJson(JsonObject json) { + JsonElement drawables = JsonHelper.getJsonElement(json, "drawables", "children"); + if (drawables != null && drawables.isJsonArray()) { + return parseJson(drawables.getAsJsonArray()); + } + GTCEu.LOGGER.throwing( + new JsonParseException("DrawableStack json should have an array named 'drawables' or 'children'.")); + return IDrawable.EMPTY; + } + + public static IDrawable parseJson(JsonArray drawables) { + List list = new ArrayList<>(); + for (JsonElement child : drawables) { + IDrawable drawable = JsonHelper.deserialize(child, IDrawable.class); + if (drawable != null) { + list.add(drawable); + } + } + if (list.isEmpty()) { + return IDrawable.EMPTY; + } + if (list.size() == 1) { + return list.get(0); + } + return new DrawableStack(list.toArray(IDrawable[]::new)); + } + + // this method should never be called, but the special casing code is copied here in case it does. + @Override + public boolean saveToJson(JsonObject json) { + JsonArray jsonArray = new JsonArray(); + for (IDrawable drawable : this.getDrawables()) { + jsonArray.add(JsonHelper.serialize(drawable)); + } + json.add("drawables", jsonArray); + return true; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DynamicDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DynamicDrawable.java new file mode 100644 index 00000000000..147c2a72bcb --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/DynamicDrawable.java @@ -0,0 +1,47 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import lombok.Getter; + +import java.util.function.Supplier; + +/** + * Takes supplier of {@link IDrawable} and draws conditional drawable. + * Return value of the supplier should be deterministic per render frame, + * in order to apply {@link ITheme} to correct object. + */ +public class DynamicDrawable implements IDrawable { + + @Getter + private final Supplier supplier; + + public DynamicDrawable(Supplier supplier) { + this.supplier = supplier; + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + IDrawable drawable = this.supplier.get(); + if (drawable != null) { + drawable.draw(context, x, y, width, height, widgetTheme); + } + } + + @Override + public boolean canApplyTheme() { + IDrawable drawable = this.supplier.get(); + if (drawable != null) { + return drawable.canApplyTheme(); + } else { + return false; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/EntityDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/EntityDrawable.java new file mode 100644 index 00000000000..c48bc3bdeff --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/EntityDrawable.java @@ -0,0 +1,21 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.world.entity.LivingEntity; + +public class EntityDrawable implements IDrawable { + + private LivingEntity entity; + + public EntityDrawable(LivingEntity entity) { + this.entity = entity; + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + GuiDraw.drawLivingEntity(context.getGraphics(), this.entity, x, y, width, height, context.getCurrentDrawingZ()); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/FlowDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/FlowDrawable.java new file mode 100644 index 00000000000..6940afde4d6 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/FlowDrawable.java @@ -0,0 +1,199 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IIcon; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Box; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; +import java.util.function.IntFunction; + +public class FlowDrawable implements IDrawable { + + public static FlowDrawable row() { + return new FlowDrawable(GuiAxis.X); + } + + public static FlowDrawable column() { + return new FlowDrawable(GuiAxis.Y); + } + + @Getter + private final GuiAxis axis; + @Getter + private final List icons = new ArrayList<>(); + private Alignment.MainAxis mainAxisAlignment = Alignment.MainAxis.START; + private Alignment.CrossAxis crossAxisAlignment = Alignment.CrossAxis.CENTER; + + public FlowDrawable(GuiAxis axis) { + this.axis = axis; + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + if (this.icons.isEmpty()) return; + if (this.icons.size() == 1) { + this.icons.get(0).draw(context, x, y, width, height, widgetTheme); + return; + } + + GuiAxis otherAxis = this.axis.getOther(); + int size = this.axis.isHorizontal() ? width : height; + int crossSize = this.axis.isHorizontal() ? height : width; + int pos = this.axis.isHorizontal() ? x : y; + int amount = this.icons.size(); + int childrenSize = 0; + int expandedAmount = 0; + int space = 0; // child margin + + for (IIcon icon : this.icons) { + int s = icon.getSize(this.axis); + if (s <= 0) { + expandedAmount++; + } else { + childrenSize += s; + } + } + Alignment.MainAxis maa = this.mainAxisAlignment; + if (maa == Alignment.MainAxis.SPACE_BETWEEN || maa == Alignment.MainAxis.SPACE_AROUND) { + if (expandedAmount > 0) { + maa = Alignment.MainAxis.START; + } else { + space = 0; + } + } + final int spaceCount = Math.max(amount - 1, 0); + childrenSize += spaceCount * space; + + int lastP = pos; + if (maa == Alignment.MainAxis.CENTER) { + lastP += (int) (size / 2f - childrenSize / 2f); + } else if (maa == Alignment.MainAxis.END) { + lastP += size - childrenSize; + } + + int expanderSize = (size - childrenSize) / expandedAmount; + int is, ics, ip, icp; // s = size, p = pos, c = cross + for (IIcon icon : this.icons) { + Box margin = icon.getMargin(); + int s = icon.getSize(this.axis); + int cs = icon.getSize(otherAxis); + is = s; + ics = cs; + ip = lastP; + icp = 0; + if (s <= 0) { + is = expanderSize; + if (margin != null) ip += margin.getStart(this.axis); + } + if (cs <= 0) { + ics = crossSize; + if (margin != null) { + ics -= margin.getTotal(otherAxis); + icp = margin.getStart(otherAxis); + } + } else { + if (this.crossAxisAlignment == Alignment.CrossAxis.CENTER) { + icp = (crossSize - cs) / 2; + } else if (this.crossAxisAlignment == Alignment.CrossAxis.END) { + icp = crossSize - cs; + } + } + if (this.axis.isHorizontal()) { + icon.draw(context, ip, icp, is, ics, widgetTheme); + } else { + icon.draw(context, icp, ip, ics, is, widgetTheme); + } + lastP += is; + + if (maa == Alignment.MainAxis.SPACE_BETWEEN) { + lastP += (size - childrenSize) / spaceCount; + } + } + } + + @Override + public int getDefaultWidth() { + return this.axis.isHorizontal() ? getMainAxisDefaultSize() : getCrossAxisDefaultSize(); + } + + @Override + public int getDefaultHeight() { + return this.axis.isHorizontal() ? getCrossAxisDefaultSize() : getMainAxisDefaultSize(); + } + + public int getMainAxisDefaultSize() { + int s = 0; + for (IIcon icon : this.icons) { + int is = icon.getSize(this.axis); + if (is <= 0) is = this.axis.isHorizontal() ? icon.getDefaultWidth() : icon.getDefaultHeight(); + if (is <= 0) is = 10; + s += is + icon.getMargin().getTotal(this.axis); + } + return s; + } + + public int getCrossAxisDefaultSize() { + int s = 0; + GuiAxis axis = this.axis.getOther(); + for (IIcon icon : this.icons) { + int is = icon.getSize(axis); + if (is <= 0) is = axis.isHorizontal() ? icon.getDefaultWidth() : icon.getDefaultHeight(); + if (is <= 0) is = 10; + s += is + icon.getMargin().getTotal(axis); + } + return s; + } + + public FlowDrawable mainAxisAlignment(Alignment.MainAxis maa) { + this.mainAxisAlignment = maa; + return this; + } + + public FlowDrawable crossAxisAlignment(Alignment.CrossAxis caa) { + this.crossAxisAlignment = caa; + return this; + } + + public FlowDrawable icon(IIcon icon) { + this.icons.add(icon); + return this; + } + + public FlowDrawable icons(int amount, IntFunction func) { + for (int i = 0; i < amount; i++) { + icon(func.apply(i)); + } + return this; + } + + public FlowDrawable icons(Iterable it, Function func) { + for (T t : it) { + icon(func.apply(t)); + } + return this; + } + + public FlowDrawable icons(Collection icons) { + this.icons.addAll(icons); + return this; + } + + public FlowDrawable removeIcon(IIcon icon) { + this.icons.remove(icon); + return this; + } + + public FlowDrawable removeAll() { + this.icons.clear(); + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/FluidDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/FluidDrawable.java new file mode 100644 index 00000000000..0052a92052b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/FluidDrawable.java @@ -0,0 +1,51 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraftforge.fluids.FluidStack; + +import org.jetbrains.annotations.NotNull; + +public class FluidDrawable implements IDrawable { + + private FluidStack fluid = null; + + public FluidDrawable() {} + + /** + * Takes a fluid stack, it can be null but will not draw anything + * + * @param fluid - fluid stack to draw + */ + public FluidDrawable(@NotNull FluidStack fluid) { + setFluid(fluid); + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + GuiDraw.drawFluidTexture(context.getGraphics(), fluid, x, y, width, height, context.getCurrentDrawingZ()); + } + + @Override + public int getDefaultWidth() { + return 16; + } + + @Override + public int getDefaultHeight() { + return 16; + } + + @Override + public Widget asWidget() { + return IDrawable.super.asWidget().size(16); + } + + public FluidDrawable setFluid(FluidStack fluid) { + this.fluid = fluid; + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/GuiDraw.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/GuiDraw.java new file mode 100644 index 00000000000..ff062921344 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/GuiDraw.java @@ -0,0 +1,878 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.drawable.text.TextRenderer; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; +import com.gregtechceu.gtceu.client.mui.screen.event.RichTooltipEvent; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.gregtechceu.gtceu.client.renderer.GTRenderTypes; +import com.gregtechceu.gtceu.utils.FormattingUtil; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.LightTexture; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.client.renderer.entity.EntityRenderDispatcher; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.client.event.RenderTooltipEvent; +import net.minecraftforge.client.extensions.common.IClientFluidTypeExtensions; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fluids.FluidStack; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.blaze3d.vertex.VertexFormat; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4d; +import org.joml.Matrix4f; +import org.joml.Quaternionf; +import org.joml.Vector3d; + +import java.util.List; + +public class GuiDraw { + + private static final TextRenderer textRenderer = new TextRenderer(); + + public static final double TWO_PI = Math.PI * 2; + public static final double HALF_PI = Math.PI / 2; + + public static void drawRect(GuiGraphics graphics, float x0, float y0, float w, float h, int color) { + Matrix4f pose = graphics.pose().last().pose(); + VertexConsumer builder = graphics.bufferSource().getBuffer(RenderType.guiOverlay()); + drawRectRaw(builder, pose, x0, y0, x0 + w, y0 + h, color); + } + + public static void drawHorizontalGradientRect(GuiGraphics graphics, float x0, float y0, float w, float h, + int colorLeft, + int colorRight) { + drawRect(graphics, x0, y0, w, h, colorLeft, colorRight, colorLeft, colorRight); + } + + public static void drawVerticalGradientRect(GuiGraphics graphics, float x0, float y0, float w, float h, + int colorTop, int colorBottom) { + drawRect(graphics, x0, y0, w, h, colorTop, colorTop, colorBottom, colorBottom); + } + + public static void drawRect(GuiGraphics graphics, float x0, float y0, float w, float h, int colorTL, int colorTR, + int colorBL, + int colorBR) { + Matrix4f pose = graphics.pose().last().pose(); + VertexConsumer bufferbuilder = graphics.bufferSource().getBuffer(RenderType.guiOverlay()); + + float x1 = x0 + w, y1 = y0 + h; + bufferbuilder.vertex(pose, x0, y0, 0.0f) + .color(Color.getRed(colorTL), Color.getGreen(colorTL), Color.getBlue(colorTL), Color.getAlpha(colorTL)) + .endVertex(); + bufferbuilder.vertex(pose, x0, y1, 0.0f) + .color(Color.getRed(colorBL), Color.getGreen(colorBL), Color.getBlue(colorBL), Color.getAlpha(colorBL)) + .endVertex(); + bufferbuilder.vertex(pose, x1, y1, 0.0f) + .color(Color.getRed(colorBR), Color.getGreen(colorBR), Color.getBlue(colorBR), Color.getAlpha(colorBR)) + .endVertex(); + bufferbuilder.vertex(pose, x1, y0, 0.0f) + .color(Color.getRed(colorTR), Color.getGreen(colorTR), Color.getBlue(colorTR), Color.getAlpha(colorTR)) + .endVertex(); + } + + public static void drawRectRaw(VertexConsumer buffer, Matrix4f pose, float x0, float y0, float x1, float y1, + int color) { + int r = Color.getRed(color); + int g = Color.getGreen(color); + int b = Color.getBlue(color); + int a = Color.getAlpha(color); + drawRectRaw(buffer, pose, x0, y0, x1, y1, r, g, b, a); + } + + public static void drawRectRaw(VertexConsumer buffer, Matrix4f pose, float x0, float y0, float x1, float y1, int r, + int g, int b, int a) { + buffer.vertex(pose, x0, y0, 0.0f).color(r, g, b, a).endVertex(); + buffer.vertex(pose, x0, y1, 0.0f).color(r, g, b, a).endVertex(); + buffer.vertex(pose, x1, y1, 0.0f).color(r, g, b, a).endVertex(); + buffer.vertex(pose, x1, y0, 0.0f).color(r, g, b, a).endVertex(); + } + + public static void drawCircle(GuiGraphics graphics, float x0, float y0, float diameter, int color, int segments) { + drawEllipse(graphics, x0, y0, diameter, diameter, color, color, segments); + } + + public static void drawCircle(GuiGraphics graphics, float x0, float y0, float diameter, int centerColor, + int outerColor, int segments) { + drawEllipse(graphics, x0, y0, diameter, diameter, centerColor, outerColor, segments); + } + + public static void drawEllipse(GuiGraphics graphics, float x0, float y0, float w, float h, int color, + int segments) { + drawEllipse(graphics, x0, y0, w, h, color, color, segments); + } + + public static void drawEllipse(GuiGraphics graphics, float x0, float y0, float w, float h, int centerColor, + int outerColor, + int segments) { + Matrix4f pose = graphics.pose().last().pose(); + VertexConsumer bufferbuilder = graphics.bufferSource().getBuffer(GTRenderTypes.guiOverlayTriangleFan()); + + float x_2 = x0 + w / 2f, y_2 = y0 + h / 2f; + // start at center + bufferbuilder.vertex(pose, x_2, y_2, 0.0f) + .color(Color.getRed(centerColor), Color.getGreen(centerColor), Color.getBlue(centerColor), + Color.getAlpha(centerColor)) + .endVertex(); + int a = Color.getAlpha(outerColor), r = Color.getRed(outerColor), g = Color.getGreen(outerColor), + b = Color.getBlue(outerColor); + float incr = (float) (TWO_PI / segments); + for (int i = 0; i <= segments; i++) { + float angle = incr * i; + float x = (float) (Math.sin(angle) * (w / 2) + x_2); + float y = (float) (Math.cos(angle) * (h / 2) + y_2); + bufferbuilder.vertex(x, y, 0.0f).color(r, g, b, a).endVertex(); + } + RenderSystem.disableBlend(); + } + + public static void drawRoundedRect(GuiGraphics graphics, float x0, float y0, float w, float h, int color, + int cornerRadius, + int segments) { + drawRoundedRect(graphics, x0, y0, w, h, color, color, color, color, cornerRadius, segments); + } + + public static void drawVerticalGradientRoundedRect(GuiGraphics graphics, float x0, float y0, float w, float h, + int colorTop, + int colorBottom, int cornerRadius, int segments) { + drawRoundedRect(graphics, x0, y0, w, h, colorTop, colorTop, colorBottom, colorBottom, cornerRadius, segments); + } + + public static void drawHorizontalGradientRoundedRect(GuiGraphics graphics, float x0, float y0, float w, float h, + int colorLeft, + int colorRight, int cornerRadius, int segments) { + drawRoundedRect(graphics, x0, y0, w, h, colorLeft, colorRight, colorLeft, colorRight, cornerRadius, segments); + } + + public static void drawRoundedRect(GuiGraphics graphics, float x0, float y0, float w, float h, int colorTL, + int colorTR, int colorBL, + int colorBR, int cornerRadius, int segments) { + Matrix4f pose = graphics.pose().last().pose(); + VertexConsumer bufferbuilder = graphics.bufferSource().getBuffer(GTRenderTypes.guiOverlayTriangleFan()); + + float x1 = x0 + w, y1 = y0 + h; + int color = Color.average(colorBL, colorBR, colorTR, colorTL); + // start at center + bufferbuilder.vertex(pose, x0 + w / 2f, y0 + h / 2f, 0.0f) + .color(Color.getRed(color), Color.getGreen(color), Color.getBlue(color), Color.getAlpha(color)) + .endVertex(); + // left side + bufferbuilder.vertex(pose, x0, y0 + cornerRadius, 0.0f) + .color(Color.getRed(colorTL), Color.getGreen(colorTL), Color.getBlue(colorTL), Color.getAlpha(colorTL)) + .endVertex(); + bufferbuilder.vertex(pose, x0, y1 - cornerRadius, 0.0f) + .color(Color.getRed(colorBL), Color.getGreen(colorBL), Color.getBlue(colorBL), Color.getAlpha(colorBL)) + .endVertex(); + // bottom left corner + for (int i = 1; i <= segments; i++) { + float x = (float) (x0 + cornerRadius - Math.cos(HALF_PI / segments * i) * cornerRadius); + float y = (float) (y1 - cornerRadius + Math.sin(HALF_PI / segments * i) * cornerRadius); + bufferbuilder.vertex(x, y, 0.0f) + .color(Color.getRed(colorBL), Color.getGreen(colorBL), Color.getBlue(colorBL), + Color.getAlpha(colorBL)) + .endVertex(); + } + // bottom side + bufferbuilder.vertex(pose, x1 - cornerRadius, y1, 0.0f) + .color(Color.getRed(colorBR), Color.getGreen(colorBR), Color.getBlue(colorBR), Color.getAlpha(colorBR)) + .endVertex(); + // bottom right corner + for (int i = 1; i <= segments; i++) { + float x = (float) (x1 - cornerRadius + Math.sin(HALF_PI / segments * i) * cornerRadius); + float y = (float) (y1 - cornerRadius + Math.cos(HALF_PI / segments * i) * cornerRadius); + bufferbuilder.vertex(pose, x, y, 0.0f) + .color(Color.getRed(colorBR), Color.getGreen(colorBR), Color.getBlue(colorBR), + Color.getAlpha(colorBR)) + .endVertex(); + } + // right side + bufferbuilder.vertex(pose, x1, y0 + cornerRadius, 0.0f) + .color(Color.getRed(colorTR), Color.getGreen(colorTR), Color.getBlue(colorTR), Color.getAlpha(colorTR)) + .endVertex(); + // top right corner + for (int i = 1; i <= segments; i++) { + float x = (float) (x1 - cornerRadius + Math.cos(HALF_PI / segments * i) * cornerRadius); + float y = (float) (y0 + cornerRadius - Math.sin(HALF_PI / segments * i) * cornerRadius); + bufferbuilder.vertex(pose, x, y, 0.0f) + .color(Color.getRed(colorTR), Color.getGreen(colorTR), Color.getBlue(colorTR), + Color.getAlpha(colorTR)) + .endVertex(); + } + // top side + bufferbuilder.vertex(pose, x0 + cornerRadius, y0, 0.0f) + .color(Color.getRed(colorTL), Color.getGreen(colorTL), Color.getBlue(colorTL), Color.getAlpha(colorTL)) + .endVertex(); + // top left corner + for (int i = 1; i <= segments; i++) { + float x = (float) (x0 + cornerRadius - Math.sin(HALF_PI / segments * i) * cornerRadius); + float y = (float) (y0 + cornerRadius - Math.cos(HALF_PI / segments * i) * cornerRadius); + bufferbuilder.vertex(pose, x, y, 0.0f) + .color(Color.getRed(colorTL), Color.getGreen(colorTL), Color.getBlue(colorTL), + Color.getAlpha(colorTL)) + .endVertex(); + } + bufferbuilder.vertex(pose, x0, y0 + cornerRadius, 0.0f) + .color(Color.getRed(colorTL), Color.getGreen(colorTL), Color.getBlue(colorTL), Color.getAlpha(colorTL)) + .endVertex(); + } + + public static void drawTexture(Matrix4f pose, ResourceLocation location, float x, float y, float w, float h, int u, + int v, + int textureWidth, int textureHeight) { + RenderSystem.setShaderTexture(0, location); + drawTexture(pose, x, y, u, v, w, h, textureWidth, textureHeight); + } + + public static void drawTexture(Matrix4f pose, float x, float y, int u, int v, float w, float h, int textureW, + int textureH) { + drawTexture(pose, x, y, u, v, w, h, textureW, textureH, 0); + } + + /** + * Draw a textured quad with given UV, dimensions and custom texture size + */ + public static void drawTexture(Matrix4f pose, float x, float y, int u, int v, float w, float h, int textureW, + int textureH, float z) { + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder buffer = tesselator.getBuilder(); + + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + drawTexture(pose, buffer, x, y, u, v, w, h, textureW, textureH, z); + tesselator.end(); + } + + public static void drawTexture(Matrix4f pose, VertexConsumer buffer, float x, float y, int u, int v, float w, + float h, int textureW, + int textureH, float z) { + float tw = 1F / textureW; + float th = 1F / textureH; + + buffer.vertex(pose, x, y + h, z).uv(u * tw, (v + h) * th).endVertex(); + buffer.vertex(pose, x + w, y + h, z).uv((u + w) * tw, (v + h) * th).endVertex(); + buffer.vertex(pose, x + w, y, z).uv((u + w) * tw, v * th).endVertex(); + buffer.vertex(pose, x, y, z).uv(u * tw, v * th).endVertex(); + } + + public static void drawTexture(Matrix4f pose, float x, float y, int u, int v, float w, float h, int textureW, + int textureH, int tu, + int tv) { + drawTexture(pose, x, y, u, v, w, h, textureW, textureH, tu, tv, 0); + } + + /** + * Draw a textured quad with given UV, dimensions and custom texture size + */ + public static void drawTexture(Matrix4f pose, float x, float y, int u, int v, float w, float h, int textureW, + int textureH, int tu, + int tv, float z) { + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder buffer = tesselator.getBuilder(); + + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + drawTexture(pose, buffer, x, y, u, v, w, h, textureW, textureH, tu, tv, z); + tesselator.end(); + } + + public static void drawTexture(Matrix4f pose, VertexConsumer buffer, float x, float y, int u, int v, float w, + float h, int textureW, + int textureH, int tu, int tv, float z) { + float tw = 1F / textureW; + float th = 1F / textureH; + + buffer.vertex(pose, x, y + h, z).uv(u * tw, tv * th).endVertex(); + buffer.vertex(pose, x + w, y + h, z).uv(tu * tw, tv * th).endVertex(); + buffer.vertex(pose, x + w, y, z).uv(tu * tw, v * th).endVertex(); + buffer.vertex(pose, x, y, z).uv(u * tw, v * th).endVertex(); + } + + public static void drawTexture(Matrix4f pose, ResourceLocation location, float x0, float y0, float x1, float y1, + float u0, float v0, float u1, float v1) { + drawTexture(pose, location, x0, y0, x1, y1, u0, v0, u1, v1, false); + } + + public static void drawTexture(Matrix4f pose, ResourceLocation location, float x0, float y0, float x1, float y1, + float u0, float v0, float u1, float v1, boolean withBlend) { + RenderSystem.setShaderTexture(0, location); + RenderSystem.setShader(GameRenderer::getPositionTexShader); + if (withBlend) { + RenderSystem.enableBlend(); + } else { + RenderSystem.disableBlend(); + } + drawTexture(pose, x0, y0, x1, y1, u0, v0, u1, v1, 0); + } + + public static void drawTexture(Matrix4f pose, float x0, float y0, float x1, float y1, float u0, float v0, float u1, + float v1) { + drawTexture(pose, x0, y0, x1, y1, u0, v0, u1, v1, 0); + } + + public static void drawTexture(Matrix4f pose, float x0, float y0, float x1, float y1, float u0, float v0, float u1, + float v1, float z) { + RenderSystem.disableDepthTest(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder buffer = tesselator.getBuilder(); + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + drawTexture(pose, buffer, x0, y0, x1, y1, u0, v0, u1, v1, z); + tesselator.end(); + } + + public static void drawTexture(Matrix4f pose, VertexConsumer buffer, float x0, float y0, float x1, float y1, + float u0, float v0, + float u1, float v1, float z) { + buffer.vertex(pose, x0, y1, z).uv(u0, v1).endVertex(); + buffer.vertex(pose, x1, y1, z).uv(u1, v1).endVertex(); + buffer.vertex(pose, x1, y0, z).uv(u1, v0).endVertex(); + buffer.vertex(pose, x0, y0, z).uv(u0, v0).endVertex(); + } + + public static void drawTiledTexture(Matrix4f pose, ResourceLocation location, float x, float y, float w, float h, + int u, int v, + int tileW, int tileH, int tw, int th, float z) { + RenderSystem.setShaderTexture(0, location); + drawTiledTexture(pose, x, y, w, h, u, v, tileW, tileH, tw, th, z); + } + + public static void drawTiledTexture(Matrix4f pose, float x, float y, float w, float h, int u, int v, int tileW, + int tileH, int tw, + int th, float z) { + int countX = (((int) w - 1) / tileW) + 1; + int countY = (((int) h - 1) / tileH) + 1; + float fillerX = w - (countX - 1) * tileW; + float fillerY = h - (countY - 1) * tileH; + + RenderSystem.disableDepthTest(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder buffer = tesselator.getBuilder(); + + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + + for (int i = 0, c = countX * countY; i < c; i++) { + int ix = i % countX; + int iy = i / countX; + float xx = x + ix * tileW; + float yy = y + iy * tileH; + float xw = ix == countX - 1 ? fillerX : tileW; + float yh = iy == countY - 1 ? fillerY : tileH; + + drawTexture(pose, buffer, xx, yy, u, v, xw, yh, tw, th, z); + } + } + + public static void drawTiledTexture(Matrix4f pose, ResourceLocation location, float x, float y, float w, float h, + float u0, float v0, + float u1, float v1, int textureWidth, int textureHeight, float z) { + RenderSystem.enableBlend(); + RenderSystem.setShaderTexture(0, location); + drawTiledTexture(pose, x, y, w, h, u0, v0, u1, v1, textureWidth, textureHeight, z); + RenderSystem.disableBlend(); + } + + public static void drawTiledTexture(Matrix4f pose, float x, float y, float w, float h, float u0, float v0, float u1, + float v1, + int tileWidth, int tileHeight, float z) { + int countX = (((int) w - 1) / tileWidth) + 1; + int countY = (((int) h - 1) / tileHeight) + 1; + float fillerX = w - (countX - 1) * tileWidth; + float fillerY = h - (countY - 1) * tileHeight; + float fillerU = u0 + (u1 - u0) * fillerX / tileWidth; + float fillerV = v0 + (v1 - v0) * fillerY / tileHeight; + + RenderSystem.disableDepthTest(); + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder buffer = tesselator.getBuilder(); + + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_TEX); + + for (int i = 0, c = countX * countY; i < c; i++) { + int ix = i % countX; + int iy = i / countX; + float xx = x + ix * tileWidth; + float yy = y + iy * tileHeight; + float xw = tileWidth, yh = tileHeight, uEnd = u1, vEnd = v1; + if (ix == countX - 1) { + xw = fillerX; + uEnd = fillerU; + } + if (iy == countY - 1) { + yh = fillerY; + vEnd = fillerV; + } + + drawTexture(pose, buffer, xx, yy, xx + xw, yy + yh, u0, v0, uEnd, vEnd, z); + } + + tesselator.end(); + } + + public static void drawLivingEntity(GuiGraphics graphics, LivingEntity entity, int x, int y, float width, + float height, int z) { + int scale = 132; + Quaternionf pose = new Quaternionf(1.414f, 0.0f, 1.0f, 0.0f); + graphics.pose().pushPose(); + graphics.pose().translate((double) x + width / 2, (double) y + height, 50.0D); + graphics.pose() + .mulPoseMatrix((new Matrix4f()).scaling((float) width / 2, (float) height / 2, (float) (-scale))); + graphics.pose().mulPose(pose); + Lighting.setupForEntityInInventory(); + + EntityRenderDispatcher erd = Minecraft.getInstance().getEntityRenderDispatcher(); + erd.setRenderShadow(false); + + RenderSystem.runAsFancy(() -> { + erd.render(entity, 0.0d, 0.0d, 0.0d, 0.0f, 1.0f, graphics.pose(), graphics.bufferSource(), + LightTexture.FULL_BRIGHT); + }); + graphics.flush(); + erd.setRenderShadow(true); + graphics.pose().popPose(); + Lighting.setupFor3DItems(); + } + + public static void drawItem(GuiGraphics graphics, ItemStack item, int x, int y, float width, float height, int z) { + if (item.isEmpty()) return; + graphics.pose().pushPose(); + graphics.pose().translate(x, y, z); + graphics.pose().scale(width / 16f, height / 16f, 1); + graphics.renderItem(item, 0, 0); + graphics.renderItemDecorations(Minecraft.getInstance().font, item, 0, 0); + graphics.pose().popPose(); + } + + public static void drawFluidTexture(GuiGraphics graphics, FluidStack content, float x0, float y0, float width, + float height, float z) { + if (content == null || content.isEmpty()) { + return; + } + Fluid fluid = content.getFluid(); + ResourceLocation fluidStill = IClientFluidTypeExtensions.of(fluid).getStillTexture(content); + TextureAtlasSprite sprite = Minecraft.getInstance().getTextureAtlas(InventoryMenu.BLOCK_ATLAS) + .apply(fluidStill); + int fluidColor = IClientFluidTypeExtensions.of(fluid).getTintColor(content); + graphics.setColor(Color.getRedF(fluidColor), Color.getGreenF(fluidColor), Color.getBlueF(fluidColor), + Color.getAlphaF(fluidColor)); + RenderSystem.setShader(GameRenderer::getPositionTexShader); + drawTiledTexture(graphics.pose().last().pose(), InventoryMenu.BLOCK_ATLAS, x0, y0, width, height, + sprite.getU0(), sprite.getV0(), + sprite.getU1(), sprite.getV1(), sprite.contents().width(), sprite.contents().height(), z); + graphics.setColor(1f, 1f, 1f, 1f); + } + + public static void drawStandardSlotAmountText(ModularGuiContext context, int amount, String format, Area area, + float z) { + drawAmountText(context, amount, format, 0, 0, area.width, area.height, Alignment.BottomRight, z); + } + + public static void drawAmountText(ModularGuiContext context, int amount, String format, int x, int y, int width, + int height, Alignment alignment, float z) { + if (amount == 1) return; + + String amountText = FormattingUtil.formatNumberReadable(amount, false); + if (format != null) { + amountText = format + amountText; + } + drawScaledAlignedTextInBox(context, amountText, x, y, width, height, alignment, 1f, z); + } + + public static void drawScaledAlignedTextInBox(ModularGuiContext context, String amountText, int x, int y, int width, + int height, Alignment alignment) { + drawScaledAlignedTextInBox(context, amountText, x, y, width, height, alignment, 1f, 0.0f); + } + + public static void drawScaledAlignedTextInBox(ModularGuiContext context, String amountText, int x, int y, int width, + int height, + Alignment alignment, float maxScale, float z) { + if (amountText == null || amountText.isEmpty()) return; + // render the amount overlay + textRenderer.setShadow(true); + textRenderer.setScale(1f); + textRenderer.setColor(Color.WHITE.main); + textRenderer.setAlignment(alignment, width, height); + textRenderer.setPos(x, y); + textRenderer.setHardWrapOnBorder(false); + RenderSystem.disableDepthTest(); + RenderSystem.disableBlend(); + if (amountText.length() > 2 && width > 16) { // we know that numbers below 100 will always fit in standard slots + // simulate and calculate scale with width + textRenderer.setSimulate(true); + textRenderer.draw(context.getGraphics(), amountText); + textRenderer.setSimulate(false); + textRenderer.setScale(Math.min(maxScale, width / textRenderer.getLastWidth())); + } + context.graphicsPose().translate(0, 0, 100 + z); + textRenderer.draw(context.getGraphics(), amountText); + textRenderer.setHardWrapOnBorder(true); + RenderSystem.enableDepthTest(); + RenderSystem.enableBlend(); + } + + public static void drawSprite(Matrix4f pose, TextureAtlasSprite sprite, float x0, float y0, float w, float h) { + RenderSystem.enableBlend(); + RenderSystem.setShaderTexture(0, sprite.atlasLocation()); + drawTexture(pose, x0, y0, x0 + w, y0 + h, sprite.getU0(), sprite.getV0(), sprite.getU1(), sprite.getV1()); + RenderSystem.disableBlend(); + } + + public static void drawTiledSprite(Matrix4f pose, TextureAtlasSprite sprite, float x0, float y0, float w, float h) { + RenderSystem.enableBlend(); + RenderSystem.setShaderTexture(0, sprite.atlasLocation()); + drawTiledTexture(pose, sprite.atlasLocation(), x0, y0, x0 + w, y0 + h, sprite.getU0(), sprite.getV0(), + sprite.getU1(), + sprite.getV1(), sprite.contents().width(), sprite.contents().height(), 0); + RenderSystem.disableBlend(); + } + + public static void drawOutlineCenter(GuiGraphics graphics, int x, int y, int offset, int color) { + drawOutlineCenter(graphics, x, y, offset, color, 1); + } + + public static void drawOutlineCenter(GuiGraphics graphics, int x, int y, int offset, int color, int border) { + drawOutline(graphics, x - offset, y - offset, x + offset, y + offset, color, border); + } + + public static void drawOutline(GuiGraphics graphics, int left, int top, int right, int bottom, int color) { + drawOutline(graphics, left, top, right, bottom, color, 1); + } + + /** + * Draw rectangle outline with given border + */ + public static void drawOutline(GuiGraphics graphics, int left, int top, int right, int bottom, int color, + int border) { + graphics.fill(left, top, left + border, bottom, color); + graphics.fill(right - border, top, right, bottom, color); + graphics.fill(left + border, top, right - border, top + border, color); + graphics.fill(left + border, bottom - border, right - border, bottom, color); + } + + private static void drawBorderLTRB(GuiGraphics graphics, float left, float top, float right, float bottom, + float border, int color, boolean outside) { + if (outside) { + left -= border; + top -= border; + right += border; + bottom += border; + } + float x0 = left, y0 = top, x1 = right, y1 = bottom, d = border; + + var buffer = graphics.bufferSource().getBuffer(GTRenderTypes.guiTriangleStrip()); + var pose = graphics.pose().last().pose(); + pc(buffer, pose, x0, y0, color); + pc(buffer, pose, x1 - d, y0 + d, color); + pc(buffer, pose, x1, y0, color); + pc(buffer, pose, x1 - d, y1 - d, color); + pc(buffer, pose, x1, y1, color); + pc(buffer, pose, x0 + d, y1 - d, color); + pc(buffer, pose, x0, y1, color); + pc(buffer, pose, x0 + d, y0 + d, color); + pc(buffer, pose, x0, y0, color); + pc(buffer, pose, x1 - d, y0 + d, color); + } + + public static void drawBorderOutsideLTRB(GuiGraphics graphics, float left, float top, float right, float bottom, + int color) { + drawBorderLTRB(graphics, left, top, right, bottom, 1, color, true); + } + + public static void drawBorderOutsideLTRB(GuiGraphics graphics, float left, float top, float right, float bottom, + float border, int color) { + drawBorderLTRB(graphics, left, top, right, bottom, border, color, true); + } + + public static void drawBorderInsideLTRB(GuiGraphics graphics, float left, float top, float right, float bottom, + int color) { + drawBorderLTRB(graphics, left, top, right, bottom, 1, color, false); + } + + public static void drawBorderInsideLTRB(GuiGraphics graphics, float left, float top, float right, float bottom, + float border, int color) { + drawBorderLTRB(graphics, left, top, right, bottom, border, color, false); + } + + private static void drawBorderXYWH(GuiGraphics graphics, float x, float y, float w, float h, float border, + int color, boolean outside) { + drawBorderLTRB(graphics, x, y, x + w, y + h, border, color, outside); + } + + public static void drawBorderOutsideXYWH(GuiGraphics graphics, float x, float y, float w, float h, float border, + int color) { + drawBorderXYWH(graphics, x, y, w, h, border, color, true); + } + + public static void drawBorderOutsideXYWH(GuiGraphics graphics, float x, float y, float w, float h, int color) { + drawBorderXYWH(graphics, x, y, w, h, 1, color, true); + } + + public static void drawBorderInsideXYWH(GuiGraphics graphics, float x, float y, float w, float h, float border, + int color) { + drawBorderXYWH(graphics, x, y, w, h, border, color, false); + } + + public static void drawBorderInsideXYWH(GuiGraphics graphics, float x, float y, float w, float h, int color) { + drawBorderXYWH(graphics, x, y, w, h, 1, color, false); + } + + private static void pc(VertexConsumer buffer, Matrix4f pose, float x, float y, int c) { + buffer.vertex(pose, x, y, 0).color(Color.getRed(c), Color.getGreen(c), Color.getBlue(c), Color.getAlpha(c)) + .endVertex(); + } + + /** + * Draws a rectangular shadow + * + * @param x left of solid shadow part + * @param y top of solid shadow part + * @param w width of solid shadow part + * @param h height of solid shadow part + * @param oX shadow gradient size in x + * @param oY shadow gradient size in y + * @param opaque solid shadow color + * @param shadow gradient end color + */ + public static void drawDropShadow(Matrix4f pose, int x, int y, int w, int h, int oX, int oY, int opaque, + int shadow) { + float a1 = Color.getAlphaF(opaque); + float r1 = Color.getRedF(opaque); + float g1 = Color.getGreenF(opaque); + float b1 = Color.getBlueF(opaque); + float a2 = Color.getAlphaF(shadow); + float r2 = Color.getRedF(shadow); + float g2 = Color.getGreenF(shadow); + float b2 = Color.getBlueF(shadow); + + RenderSystem.setShader(GameRenderer::getPositionColorShader); + RenderSystem.enableBlend(); + RenderSystem.disableDepthTest(); + RenderSystem.blendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, + GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, + GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO); + + Tesselator tesselator = Tesselator.getInstance(); + BufferBuilder buffer = tesselator.getBuilder(); + + buffer.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR); + + float x1 = x + w, y1 = y + h; + + /* Draw opaque part */ + buffer.vertex(pose, x1, y, 0).color(r1, g1, b1, a1).endVertex(); + buffer.vertex(pose, x, y, 0).color(r1, g1, b1, a1).endVertex(); + buffer.vertex(pose, x, y1, 0).color(r1, g1, b1, a1).endVertex(); + buffer.vertex(pose, x1, y1, 0).color(r1, g1, b1, a1).endVertex(); + + /* Draw top shadow */ + buffer.vertex(pose, x1 + oX, y - oY, 0).color(r2, g2, b2, a2).endVertex(); + buffer.vertex(pose, x - oX, y - oY, 0).color(r2, g2, b2, a2).endVertex(); + buffer.vertex(pose, x, y, 0).color(r1, g1, b1, a1).endVertex(); + buffer.vertex(pose, x1, y, 0).color(r1, g1, b1, a1).endVertex(); + + /* Draw bottom shadow */ + buffer.vertex(pose, x1, y1, 0).color(r1, g1, b1, a1).endVertex(); + buffer.vertex(pose, x, y1, 0).color(r1, g1, b1, a1).endVertex(); + buffer.vertex(pose, x - oX, y1 + oY, 0).color(r2, g2, b2, a2).endVertex(); + buffer.vertex(pose, x1 + oX, y1 + oY, 0).color(r2, g2, b2, a2).endVertex(); + + /* Draw left shadow */ + buffer.vertex(pose, x, y, 0).color(r1, g1, b1, a1).endVertex(); + buffer.vertex(pose, x - oX, y - oY, 0).color(r2, g2, b2, a2).endVertex(); + buffer.vertex(pose, x - oX, y1 + oY, 0).color(r2, g2, b2, a2).endVertex(); + buffer.vertex(pose, x, y1, 0).color(r1, g1, b1, a1).endVertex(); + + /* Draw right shadow */ + buffer.vertex(pose, x1 + oX, y - oY, 0).color(r2, g2, b2, a2).endVertex(); + buffer.vertex(pose, x1, y, 0).color(r1, g1, b1, a1).endVertex(); + buffer.vertex(pose, x1, y1, 0).color(r1, g1, b1, a1).endVertex(); + buffer.vertex(pose, x1 + oX, y1 + oY, 0).color(r2, g2, b2, a2).endVertex(); + + tesselator.end(); + RenderSystem.disableBlend(); + } + + public static void drawDropCircleShadow(GuiGraphics graphics, int x, int y, int radius, int segments, int opaque, + int shadow) { + Matrix4f pose = graphics.pose().last().pose(); + Matrix4d poseD = new Matrix4d(pose); + + float a1 = Color.getAlphaF(opaque); + float r1 = Color.getRedF(opaque); + float g1 = Color.getGreenF(opaque); + float b1 = Color.getBlueF(opaque); + float a2 = Color.getAlphaF(shadow); + float r2 = Color.getRedF(shadow); + float g2 = Color.getGreenF(shadow); + float b2 = Color.getBlueF(shadow); + + VertexConsumer buffer = graphics.bufferSource().getBuffer(GTRenderTypes.guiOverlayTriangleFan()); + buffer.vertex(pose, x, y, 0).color(r1, g1, b1, a1).endVertex(); + + Vector3d pos = new Vector3d(); + for (int i = 0; i <= segments; i++) { + double a = i / (double) segments * TWO_PI - HALF_PI; + circleVertex(buffer, poseD, pos, x, Math.cos(a), y, Math.sin(a), radius).color(r2, g2, b2, a2).endVertex(); + } + } + + public static void drawDropCircleShadow(GuiGraphics graphics, int x, int y, int radius, int offset, int segments, + int opaque, + int shadow) { + if (offset >= radius) { + drawDropCircleShadow(graphics, x, y, radius, segments, opaque, shadow); + return; + } + Matrix4f pose = graphics.pose().last().pose(); + Matrix4d poseD = new Matrix4d(pose); + + float a1 = Color.getAlphaF(opaque); + float r1 = Color.getRedF(opaque); + float g1 = Color.getGreenF(opaque); + float b1 = Color.getBlueF(opaque); + float a2 = Color.getAlphaF(shadow); + float r2 = Color.getRedF(shadow); + float g2 = Color.getGreenF(shadow); + float b2 = Color.getBlueF(shadow); + + VertexConsumer buffer = graphics.bufferSource().getBuffer(GTRenderTypes.guiOverlayTriangleFan()); + /* Draw opaque base */ + buffer.vertex(pose, x, y, 0).color(r1, g1, b1, a1).endVertex(); + + Vector3d pos = new Vector3d(); + for (int i = 0; i <= segments; i++) { + double a = i / (double) segments * TWO_PI - HALF_PI; + circleVertex(buffer, poseD, pos, x, Math.cos(a), y, Math.sin(a), offset).color(r1, g1, b1, a1).endVertex(); + } + + /* Draw outer shadow */ + buffer = graphics.bufferSource().getBuffer(RenderType.gui()); + + for (int i = 0; i < segments; i++) { + double alpha1 = i / (double) segments * TWO_PI - HALF_PI; + double alpha2 = (i + 1) / (double) segments * TWO_PI - HALF_PI; + + double cosA1 = Math.cos(alpha1); + double cosA2 = Math.cos(alpha2); + double sinA1 = Math.sin(alpha1); + double sinA2 = Math.sin(alpha2); + + circleVertex(buffer, poseD, pos, x, cosA2, y, sinA2, offset).color(r1, g1, b1, a1).endVertex(); + circleVertex(buffer, poseD, pos, x, cosA1, y, sinA1, offset).color(r1, g1, b1, a1).endVertex(); + circleVertex(buffer, poseD, pos, x, cosA1, y, sinA1, radius).color(r2, g2, b2, a2).endVertex(); + circleVertex(buffer, poseD, pos, x, cosA2, y, sinA2, radius).color(r2, g2, b2, a2).endVertex(); + } + } + + private static VertexConsumer circleVertex(VertexConsumer buffer, Matrix4d pose, Vector3d pos, double x, + double xOffset, double y, + double yOffset, double mul) { + pos.x = x - xOffset * mul; + pos.y = y + yOffset * mul; + pose.transformPosition(pos); + return buffer.vertex(pos.x, pos.y, pos.z); + } + + @OnlyIn(Dist.CLIENT) + public static void drawBorder(GuiGraphics graphics, float x, float y, float width, float height, int color, + float border) { + drawBorderLTRB(graphics, x, y, x + width, y + height, border, color, false); + // drawRect(graphics, x - border, y - border, width + 2 * border, border, color); + // drawRect(graphics, x - border, y + height, width + 2 * border, border, color); + // drawRect(graphics, x - border, y, border, height, color); + // drawRect(graphics, x + width, y, border, height, color); + } + + @OnlyIn(Dist.CLIENT) + public static void drawText(GuiGraphics graphics, String text, float x, float y, float scale, int color, + boolean shadow) { + graphics.pose().pushPose(); + graphics.pose().scale(scale, scale, 0f); + float sf = 1 / scale; + graphics.drawString(Minecraft.getInstance().font, text, x * sf, y * sf, color, shadow); + graphics.pose().popPose(); + } + + @OnlyIn(Dist.CLIENT) + public static void drawText(GuiGraphics graphics, Component text, float x, float y, float scale, int color, + boolean shadow) { + drawText(graphics, text.getVisualOrderText(), x, y, scale, color, shadow); + } + + @OnlyIn(Dist.CLIENT) + public static void drawText(GuiGraphics graphics, FormattedCharSequence text, float x, float y, float scale, + int color, + boolean shadow) { + graphics.pose().pushPose(); + graphics.pose().scale(scale, scale, 0f); + float sf = 1 / scale; + graphics.drawString(Minecraft.getInstance().font, text, x * sf, y * sf, color, shadow); + graphics.pose().popPose(); + } + + @SuppressWarnings("UnstableApiUsage") + public static void drawTooltipBackground(GuiContext context, ItemStack stack, List lines, + int x, int y, int textWidth, int height, @Nullable RichTooltip tooltip) { + GuiGraphics graphics = context.getGraphics(); + + // TODO theme color + int backgroundTop = 0xF0100010; + int backgroundBottom = backgroundTop; + int borderColorStart = 0x505000FF; + int borderColorEnd = (borderColorStart & 0xFEFEFE) >> 1 | borderColorStart & 0xFF000000; + RenderTooltipEvent.Color colorEvent; + + if (tooltip != null) { + colorEvent = new RichTooltipEvent.Color(stack, graphics, x, y, context.getFont(), + backgroundTop, borderColorStart, borderColorEnd, lines, tooltip); + } else { + colorEvent = new RenderTooltipEvent.Color(stack, graphics, x, y, context.getFont(), + backgroundTop, borderColorStart, borderColorEnd, lines); + } + + MinecraftForge.EVENT_BUS.post(colorEvent); + backgroundTop = colorEvent.getBackgroundStart(); + backgroundBottom = colorEvent.getBackgroundEnd(); + borderColorStart = colorEvent.getBorderStart(); + borderColorEnd = colorEvent.getBorderEnd(); + + // top background border + drawVerticalGradientRect(graphics, x - 3, y - 4, textWidth + 6, 1, backgroundTop, backgroundTop); + // bottom background border + drawVerticalGradientRect(graphics, x - 3, y + height + 3, textWidth + 6, 1, backgroundBottom, backgroundBottom); + // center background + drawVerticalGradientRect(graphics, x - 3, y - 3, textWidth + 6, height + 6, backgroundTop, backgroundBottom); + // left background border + drawVerticalGradientRect(graphics, x - 4, y - 3, 1, height + 6, backgroundTop, backgroundBottom); + // right background border + drawVerticalGradientRect(graphics, x + textWidth + 3, y - 3, 1, height + 6, backgroundTop, backgroundBottom); + + // left accent border + drawVerticalGradientRect(graphics, x - 3, y - 2, 1, height + 4, borderColorStart, borderColorEnd); + // right accent border + drawVerticalGradientRect(graphics, x + textWidth + 2, y - 2, 1, height + 4, borderColorStart, borderColorEnd); + // top accent border + drawVerticalGradientRect(graphics, x - 3, y - 3, textWidth + 6, 1, borderColorStart, borderColorStart); + // bottom accent border + drawVerticalGradientRect(graphics, x - 3, y + height + 2, textWidth + 6, 1, borderColorEnd, borderColorEnd); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/HoverableIcon.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/HoverableIcon.java new file mode 100644 index 00000000000..ab944967ecf --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/HoverableIcon.java @@ -0,0 +1,46 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IHoverable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IIcon; +import com.gregtechceu.gtceu.api.mui.base.widget.ITooltip; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class HoverableIcon extends DelegateIcon implements IHoverable, ITooltip { + + private final Area area = new Area(); + @Getter + private @Nullable RichTooltip tooltip; + + public HoverableIcon(IIcon icon) { + super(icon); + setRenderedAt(0, 0); + } + + @Override + public void setRenderedAt(int x, int y) { + this.area.set(x, y, getWidth(), getHeight()); + } + + @Override + public Area getRenderedArea() { + this.area.setSize(getWidth(), getHeight()); + return this.area; + } + + @Override + public @NotNull RichTooltip tooltip() { + if (this.tooltip == null) this.tooltip = new RichTooltip().parent(area -> area.set(getRenderedArea())); + return tooltip; + } + + @Override + public HoverableIcon tooltip(RichTooltip tooltip) { + this.tooltip = tooltip; + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/HueBar.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/HueBar.java new file mode 100644 index 00000000000..11d91b38108 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/HueBar.java @@ -0,0 +1,44 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +public class HueBar implements IDrawable { + + private static final int[] COLORS = { + Color.ofHSV(60, 1f, 1f, 1f), + Color.ofHSV(120, 1f, 1f, 1f), + Color.ofHSV(180, 1f, 1f, 1f), + Color.ofHSV(240, 1f, 1f, 1f), + Color.ofHSV(300, 1f, 1f, 1f), + Color.ofHSV(0, 1f, 1f, 1f) + }; + + private final GuiAxis axis; + + public HueBar(GuiAxis axis) { + this.axis = axis; + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + applyColor(widgetTheme.getColor()); + int size = this.axis.isHorizontal() ? width : height; + float step = size / 6f; + int previous = COLORS[5]; + for (int i = 0; i < 6; i++) { + int current = COLORS[i]; + if (this.axis.isHorizontal()) { + GuiDraw.drawHorizontalGradientRect(context.getGraphics(), x + step * i, y, step, height, previous, + current); + } else { + GuiDraw.drawVerticalGradientRect(context.getGraphics(), x, y + step * i, width, step, previous, + current); + } + previous = current; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Icon.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Icon.java new file mode 100644 index 00000000000..b26e8e7c513 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Icon.java @@ -0,0 +1,195 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IIcon; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Box; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.google.gson.JsonObject; +import lombok.Getter; + +/** + * A {@link IDrawable} wrapper with a fixed size and an alignment. + */ +public class Icon implements IIcon, IJsonSerializable { + + private final IDrawable drawable; + @Getter + private int width = 0, height = 0; + private float aspectRatio = 0; + @Getter + private Alignment alignment = Alignment.Center; + @Getter + private final Box margin = new Box(); + + public Icon(IDrawable drawable) { + this.drawable = drawable; + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + x += this.margin.left(); + y += this.margin.top(); + width -= this.margin.horizontal(); + height -= this.margin.vertical(); + int frameWidth = width; + int frameHeight = height; + if (this.width > 0) width = this.width; + if (this.height > 0) height = this.height; + if (this.aspectRatio > 0) { + if (this.width <= 0) { + if (this.height <= 0) { + // width and height is unset, so adjust width or height so that one of them takes the full space + float w = width, h = height; + float properW = this.aspectRatio * h; + if (w > properW) { + width = (int) properW; + } else if (w < properW) { + height = (int) (w / this.aspectRatio); + } + } else { + // height is set, so adjust width to height + float properW = this.aspectRatio * height; + width = (int) properW; + } + } else if (this.height <= 0) { + // width is set, so adjust height to width + height = (int) (width / this.aspectRatio); + } else if (GTCEu.isDev()) { + GTCEu.LOGGER.error("Aspect ratio in Icon can't be applied when width and height are specified"); + // remove aspect ratio to avoid log spamming, it does nothing in the current state anyway + this.aspectRatio = 0; + } + } + // apply alignment + if (width != frameWidth) { + x += (int) (frameWidth * this.alignment.x - width * this.alignment.x); + } + if (height != frameHeight) { + y += (int) (frameHeight * this.alignment.y - height * this.alignment.y); + } + this.drawable.draw(context, x, y, width, height, widgetTheme); + } + + @Override + public IDrawable getWrappedDrawable() { + return drawable; + } + + public Icon expandWidth() { + return width(0); + } + + public Icon expandHeight() { + return height(0); + } + + public Icon width(int width) { + this.width = Math.max(0, width); + return this; + } + + public Icon height(int height) { + this.height = Math.max(0, height); + return this; + } + + public Icon size(int width, int height) { + return width(width).height(height); + } + + public Icon size(int size) { + return width(size).height(size); + } + + public Icon aspectRatio(float aspectRatio) { + this.aspectRatio = aspectRatio; + return this; + } + + public Icon alignment(Alignment alignment) { + this.alignment = alignment; + return this; + } + + public Icon center() { + return alignment(Alignment.Center); + } + + public Icon margin(int left, int right, int top, int bottom) { + this.margin.all(left, right, top, bottom); + return this; + } + + public Icon margin(int horizontal, int vertical) { + this.margin.all(horizontal, vertical); + return this; + } + + public Icon margin(int all) { + this.margin.all(all); + return this; + } + + public Icon marginLeft(int val) { + this.margin.left(val); + return this; + } + + public Icon marginRight(int val) { + this.margin.right(val); + return this; + } + + public Icon marginTop(int val) { + this.margin.top(val); + return this; + } + + public Icon marginBottom(int val) { + this.margin.bottom(val); + return this; + } + + @Override + public void loadFromJson(JsonObject json) { + this.width = (json.has("autoWidth") || json.has("autoSize")) && + JsonHelper.getBoolean(json, true, "autoWidth", "autoSize") ? 0 : + JsonHelper.getInt(json, 0, "width", "w", "size"); + this.height = (json.has("autoHeight") || json.has("autoSize")) && + JsonHelper.getBoolean(json, true, "autoHeight", "autoSize") ? 0 : + JsonHelper.getInt(json, 0, "height", "h", "size"); + this.aspectRatio = JsonHelper.getFloat(json, 0, "aspectRatio"); + this.alignment = JsonHelper.deserialize(json, Alignment.class, Alignment.Center, "alignment", "align"); + this.margin.fromJson(json); + } + + public static Icon ofJson(JsonObject json) { + return JsonHelper.deserialize(json, IDrawable.class, IDrawable.EMPTY, "drawable", "icon").asIcon(); + } + + @Override + public boolean saveToJson(JsonObject json) { + json.add("drawable", JsonHelper.serialize(this.drawable)); + json.addProperty("width", this.width); + json.addProperty("height", this.height); + json.addProperty("aspectRatio", this.aspectRatio); + json.add("alignment", JsonHelper.serialize(this.alignment)); + this.margin.toJson(json); + return true; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(" + this.drawable.getClass().getSimpleName() + ")"; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/IconRenderer.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/IconRenderer.java new file mode 100644 index 00000000000..8771d653e3f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/IconRenderer.java @@ -0,0 +1,154 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IIcon; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.drawable.text.StyledText; +import com.gregtechceu.gtceu.api.mui.drawable.text.TextIcon; +import com.gregtechceu.gtceu.api.mui.drawable.text.TextRenderer; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.Component; +import net.minecraft.util.FormattedCharSequence; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Deprecated +public class IconRenderer { + + public static final IconRenderer SHARED = new IconRenderer(); + + protected float maxWidth = -1, maxHeight = -1; + protected int x = 0, y = 0; + protected Alignment alignment = Alignment.TopLeft; + @Setter + protected float scale = 1f; + @Setter + protected boolean shadow = false; + @Setter + protected int color = 0; + @Setter + protected int linePadding = 1; + @Setter + protected boolean simulate; + @Getter + protected float lastWidth = 0, lastHeight = 0; + @Setter + protected boolean useWholeWidth = false; + + public void setAlignment(Alignment alignment, float maxWidth) { + setAlignment(alignment, maxWidth, -1); + } + + public void setAlignment(Alignment alignment, float maxWidth, float maxHeight) { + this.alignment = alignment; + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + } + + public void setPos(int x, int y) { + this.x = x; + this.y = y; + } + + public void draw(GuiContext context, IDrawable text) { + draw(context, Collections.singletonList(text)); + } + + public void draw(GuiContext context, List lines) { + drawMeasuredLines(context, measureLines(lines)); + } + + public void drawMeasuredLines(GuiContext context, List lines) { + TextRenderer.SHARED.setColor(this.color); + TextRenderer.SHARED.setShadow(this.shadow); + TextRenderer.SHARED.setScale(this.scale); + TextRenderer.SHARED.setAlignment(this.alignment, this.maxWidth); + int totalHeight = -1, maxWidth = 0; + if (this.useWholeWidth) { + maxWidth = (int) this.maxWidth; + } + for (IIcon icon : lines) { + totalHeight += icon.getHeight() + this.linePadding; + if (!this.useWholeWidth && icon.getWidth() > 0) { + maxWidth = Math.max(maxWidth, icon.getWidth()); + } + } + if (!lines.isEmpty()) { + // don't add padding to last line + totalHeight -= this.linePadding; + } + int y = getStartY(totalHeight); + for (IIcon icon : lines) { + int x = icon.getWidth() > 0 ? getStartX(icon.getWidth()) : this.x; + if (!this.simulate) { + icon.draw(context, x, y, maxWidth, icon.getHeight(), WidgetTheme.getDefault().getTheme()); + } + y += (int) ((icon.getHeight() + this.linePadding) * this.scale); + } + this.lastWidth = this.maxWidth > 0 ? Math.min(this.maxWidth, maxWidth) : maxWidth; + this.lastHeight = totalHeight * this.scale; + } + + public List measureLines(List lines) { + List icons = new ArrayList<>(); + for (IDrawable element : lines) { + if (element instanceof IIcon icon) { + icons.add(icon); + } else if (element instanceof IKey key) { + float scale = this.scale; + Alignment alignment1 = this.alignment; + if (element instanceof StyledText styledText) { + scale = styledText.getScale(); + alignment1 = styledText.getAlignment(); + } + Component text = key.get(); + int width = (int) (getFont().width(text) * scale); + icons.add(new TextIcon(text, width, (int) (getFont().lineHeight * scale), scale, alignment1)); + } else { + icons.add(element.asIcon().height(getFont().lineHeight)); + } + } + return icons; + } + + public List wrapLine(Component line, float scale) { + return this.maxWidth > 0 ? getFont().split(line, (int) (this.maxWidth / scale)) : + Collections.singletonList(line.getVisualOrderText()); + } + + protected int getStartY(int totalHeight) { + if (this.alignment.y > 0 && this.maxHeight > 0) { + float height = totalHeight * this.scale; + return (int) (this.y + (this.maxHeight * this.alignment.y) - height * this.alignment.y); + } + return this.y; + } + + protected int getStartX(float lineWidth) { + if (this.alignment.x > 0 && this.maxWidth > 0) { + return (int) (this.x + (this.maxWidth * this.alignment.x) - lineWidth * this.alignment.x); + } + return this.x; + } + + public float getFontHeight() { + return getFont().lineHeight * this.scale; + } + + @OnlyIn(Dist.CLIENT) + public static Font getFont() { + return Minecraft.getInstance().font; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/IngredientDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/IngredientDrawable.java new file mode 100644 index 00000000000..2d44f13ca7c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/IngredientDrawable.java @@ -0,0 +1,61 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.Util; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Tolerate; + +public class IngredientDrawable implements IDrawable, IJsonSerializable { + + @Getter + @Setter + private ItemStack[] items; + @Getter + @Setter + private int cycleTime = 1000; + + public IngredientDrawable(Ingredient ingredient) { + this(ingredient.getItems()); + } + + public IngredientDrawable(ItemStack... items) { + setItems(items); + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + if (this.items.length == 0) return; + ItemStack item = this.items[(int) (Util.getMillis() % (this.cycleTime * this.items.length)) / this.cycleTime]; + if (item != null) { + GuiDraw.drawItem(context.getGraphics(), item, x, y, width, height, context.getCurrentDrawingZ()); + } + } + + /** + * Sets how many milliseconds each item shows up + * + * @param cycleTime time per item in milliseconds + * @return this + */ + @Tolerate + public IngredientDrawable cycleTime(int cycleTime) { + this.cycleTime = cycleTime; + return this; + } + + @Tolerate + public void setItems(Ingredient ingredient) { + setItems(ingredient.getItems()); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/InteractableIcon.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/InteractableIcon.java new file mode 100644 index 00000000000..bda49a42b6f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/InteractableIcon.java @@ -0,0 +1,89 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IIcon; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiAction; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; + +import lombok.Setter; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.NotNull; + +@Accessors(fluent = true, chain = true) +public class InteractableIcon extends DelegateIcon implements Interactable { + + @Setter + private IGuiAction.MousePressed onMousePressed; + @Setter + private IGuiAction.MouseReleased onMouseReleased; + @Setter + private IGuiAction.MousePressed onMouseTapped; + @Setter + private IGuiAction.MouseScroll onMouseScrolled; + @Setter + private IGuiAction.KeyPressed onKeyPressed; + @Setter + private IGuiAction.KeyReleased onKeyReleased; + @Setter + private IGuiAction.KeyPressed onKeyTapped; + + public InteractableIcon(IIcon icon) { + super(icon); + } + + public void playClickSound() { + // if (this.playClickSound) { + Interactable.playButtonClickSound(); + // } + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + if (this.onMousePressed != null && this.onMousePressed.press(mouseX, mouseY, button)) { + playClickSound(); + return Result.SUCCESS; + } + return Result.ACCEPT; + } + + @Override + public boolean onMouseReleased(double mouseX, double mouseY, int button) { + return this.onMouseReleased != null && this.onMouseReleased.release(mouseX, mouseY, button); + } + + @NotNull + @Override + public Result onMouseTapped(double mouseX, double mouseY, int button) { + if (this.onMouseTapped != null && this.onMouseTapped.press(mouseX, mouseY, button)) { + playClickSound(); + return Result.SUCCESS; + } + return Result.IGNORE; + } + + @Override + public @NotNull Result onKeyPressed(int keyCode, int scanCode, int modifiers) { + if (this.onKeyPressed != null && this.onKeyPressed.press(keyCode, scanCode, modifiers)) { + return Result.SUCCESS; + } + return Result.ACCEPT; + } + + @Override + public boolean onKeyReleased(int keyCode, int scanCode, int modifiers) { + return this.onKeyReleased != null && this.onKeyReleased.release(keyCode, scanCode, modifiers); + } + + @NotNull + @Override + public Result onKeyTapped(int keyCode, int scanCode, int modifiers) { + if (this.onKeyTapped != null && this.onKeyTapped.press(keyCode, scanCode, modifiers)) { + return Result.SUCCESS; + } + return Result.IGNORE; + } + + @Override + public boolean onMouseScrolled(double mouseX, double mouseY, double delta) { + return this.onMouseScrolled != null && this.onMouseScrolled.scroll(mouseX, mouseY, delta); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/ItemDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/ItemDrawable.java new file mode 100644 index 00000000000..49b86465cc0 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/ItemDrawable.java @@ -0,0 +1,140 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.mojang.serialization.JsonOps; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.NoSuchElementException; + +public class ItemDrawable implements IDrawable, IJsonSerializable { + + @Getter + private ItemStack item = ItemStack.EMPTY; + + public ItemDrawable() {} + + public ItemDrawable(@NotNull ItemStack item) { + setItem(item); + } + + public ItemDrawable(@NotNull Item item) { + setItem(item); + } + + public ItemDrawable(@NotNull Item item, int amount) { + setItem(item, amount); + } + + public ItemDrawable(@NotNull Item item, int amount, @Nullable CompoundTag nbt) { + setItem(item, amount, nbt); + } + + public ItemDrawable(@NotNull Block item) { + setItem(item); + } + + public ItemDrawable(@NotNull Block item, int amount) { + setItem(new ItemStack(item, amount)); + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + applyColor(widgetTheme.getColor()); + GuiDraw.drawItem(context.getGraphics(), this.item, x, y, width, height, context.getCurrentDrawingZ()); + } + + @Override + public int getDefaultWidth() { + return 16; + } + + @Override + public int getDefaultHeight() { + return 16; + } + + @Override + public Widget asWidget() { + return IDrawable.super.asWidget().size(16); + } + + public ItemDrawable setItem(@NotNull ItemStack item) { + this.item = item; + return this; + } + + public ItemDrawable setItem(@NotNull Item item) { + return setItem(item, 1, null); + } + + public ItemDrawable setItem(@NotNull Item item, int amount) { + return setItem(item, amount, null); + } + + public ItemDrawable setItem(@NotNull Item item, int amount, @Nullable CompoundTag nbt) { + ItemStack itemStack = new ItemStack(item, amount); + itemStack.setTag(nbt); + return setItem(itemStack); + } + + public ItemDrawable setItem(@NotNull Block item) { + return setItem(item, 1); + } + + public ItemDrawable setItem(@NotNull Block item, int amount) { + return setItem(new ItemStack(item, amount)); + } + + public static ItemDrawable ofJson(JsonObject json) { + String itemName = JsonHelper.getString(json, null, "item"); + if (itemName == null) throw new JsonParseException("Item property not found!"); + if (itemName.isEmpty()) return new ItemDrawable(); + ItemStack stack; + try { + ResourceLocation id = new ResourceLocation(itemName); + stack = new ItemStack(BuiltInRegistries.ITEM.get(id)); + } catch (NoSuchElementException e) { + throw new JsonParseException(e); + } + if (json.has("nbt")) { + CompoundTag nbt = (CompoundTag) JsonOps.INSTANCE.convertTo(NbtOps.INSTANCE, + JsonHelper.getObject(json, new JsonObject(), o -> o, "nbt")); + stack.setTag(nbt); + } + return new ItemDrawable(stack); + } + + @Override + public boolean saveToJson(JsonObject json) { + if (this.item == null || this.item.isEmpty()) { + json.addProperty("item", ""); + return true; + } + json.addProperty("item", this.item.getItemHolder().unwrapKey().get().location().toString()); + if (this.item.hasTag()) { + json.addProperty("nbt", this.item.getTag().toString()); + } + return true; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/NamedDrawableRow.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/NamedDrawableRow.java new file mode 100644 index 00000000000..0a5fe5b765f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/NamedDrawableRow.java @@ -0,0 +1,66 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IIcon; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +public class NamedDrawableRow implements IDrawable { + + @Getter + private IKey name; + @Getter + private IIcon drawable; + + public NamedDrawableRow() { + this(null, null); + } + + public NamedDrawableRow(@Nullable IKey name, @Nullable IIcon drawable) { + this.name = name; + this.drawable = drawable; + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + if (this.name != null) { + this.name.drawAligned(context, x, y, width, height, widgetTheme, Alignment.CenterLeft); + } + if (this.drawable != null) { + int wd = this.drawable.getWidth() + this.drawable.getMargin().horizontal(); + int xd = x + width - wd; + this.drawable.draw(context, xd, y, wd, height, widgetTheme); + } + } + + @Override + public int getDefaultWidth() { + int w = 0; + if (this.name != null) w += this.name.getDefaultWidth(); + if (this.drawable != null) w += this.drawable.getWidth(); + return w; + } + + @Override + public int getDefaultHeight() { + int h = 0; + if (this.name != null) h += this.name.getDefaultHeight(); + if (this.drawable != null) h += this.drawable.getHeight(); + return h; + } + + public NamedDrawableRow name(@Nullable IKey key) { + this.name = key; + return this; + } + + public NamedDrawableRow drawable(@Nullable IIcon drawable) { + this.drawable = drawable; + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Rectangle.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Rectangle.java new file mode 100644 index 00000000000..3222416724f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Rectangle.java @@ -0,0 +1,209 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.animation.IAnimatable; +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.utils.Interpolations; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.client.renderer.GTRenderTypes; +import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.blaze3d.vertex.VertexConsumer; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.joml.Matrix4f; + +import java.util.function.IntConsumer; + +@Accessors(fluent = true, chain = true) +public class Rectangle implements IDrawable, IJsonSerializable, IAnimatable { + + private int cornerRadius, colorTL, colorTR, colorBL, colorBR; + @Setter + private int cornerSegments; + private float borderThickness; + @Getter + @Setter + private boolean canApplyTheme = false; + + public Rectangle() { + color(0xFFFFFFFF); + this.cornerRadius = 0; + this.cornerSegments = 6; + } + + public int getColor() { + return this.colorTL; + } + + public Rectangle cornerRadius(int cornerRadius) { + this.cornerRadius = Math.max(0, cornerRadius); + if (this.borderThickness > 0 && cornerRadius > 0) { + GTCEu.LOGGER.error("Hollow rectangles currently can't have a corner radius."); + } + return this; + } + + public Rectangle color(int colorTL, int colorTR, int colorBL, int colorBR) { + this.colorTL = colorTL; + this.colorTR = colorTR; + this.colorBL = colorBL; + this.colorBR = colorBR; + return this; + } + + public Rectangle verticalGradient(int colorTop, int colorBottom) { + return color(colorTop, colorTop, colorBottom, colorBottom); + } + + public Rectangle horizontalGradient(int colorLeft, int colorRight) { + return color(colorLeft, colorRight, colorLeft, colorRight); + } + + public Rectangle color(int color) { + return color(color, color, color, color); + } + + public Rectangle solid() { + this.borderThickness = 0; + return this; + } + + public Rectangle hollow(float borderThickness) { + this.borderThickness = borderThickness; + if (borderThickness > 0 && this.cornerRadius > 0) { + GTCEu.LOGGER.error("Hollow rectangles currently can't have a corner radius."); + } + return this; + } + + public Rectangle hollow() { + return hollow(1); + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(GuiContext context, int x0, int y0, int width, int height, WidgetTheme widgetTheme) { + applyColor(widgetTheme.getColor()); + if (this.borderThickness <= 0) { + if (this.cornerRadius <= 0) { + GuiDraw.drawRect(context.getGraphics(), x0, y0, width, height, + this.colorTL, this.colorTR, this.colorBL, this.colorBR); + return; + } + GuiDraw.drawRoundedRect(context.getGraphics(), x0, y0, width, height, + this.colorTL, this.colorTR, this.colorBL, this.colorBR, + this.cornerRadius, this.cornerSegments); + } else { + float d = this.borderThickness; + float x1 = x0 + width, y1 = y0 + height; + + Matrix4f pose = context.getGraphics().pose().last().pose(); + VertexConsumer bufferbuilder = context.getGraphics().bufferSource() + .getBuffer(GTRenderTypes.guiTriangleStrip()); + v(pose, bufferbuilder, x0, y0, this.colorTL); + v(pose, bufferbuilder, x1 - d, y0 + d, this.colorTR); + v(pose, bufferbuilder, x1, y0, this.colorTR); + v(pose, bufferbuilder, x1 - d, y1 - d, this.colorBR); + v(pose, bufferbuilder, x1, y1, this.colorBR); + v(pose, bufferbuilder, x0 + d, y1 - d, this.colorBL); + v(pose, bufferbuilder, x0, y1, this.colorBL); + v(pose, bufferbuilder, x0 + d, y0 + d, this.colorTL); + v(pose, bufferbuilder, x0, y0, this.colorTL); + v(pose, bufferbuilder, x1 - d, y0 + d, this.colorTR); + } + } + + private static void v(Matrix4f pose, VertexConsumer buffer, float x, float y, int c) { + buffer.vertex(pose, x, y, 0).color(Color.getRed(c), Color.getGreen(c), Color.getBlue(c), Color.getAlpha(c)) + .endVertex(); + } + + @Override + public void loadFromJson(JsonObject json) { + if (json.has("color")) { + color(Color.ofJson(json.get("color"))); + } + if (json.has("colorTop")) { + int c = Color.ofJson(json.get("colorTop")); + this.colorTL = c; + this.colorTR = c; + } + if (json.has("colorBottom")) { + int c = Color.ofJson(json.get("colorBottom")); + this.colorBL = c; + this.colorBR = c; + } + if (json.has("colorLeft")) { + int c = Color.ofJson(json.get("colorLeft")); + this.colorTL = c; + this.colorBL = c; + } + if (json.has("colorRight")) { + int c = Color.ofJson(json.get("colorRight")); + this.colorTR = c; + this.colorBR = c; + } + setColor(json, val -> this.colorTL = val, "colorTopLeft", "colorTL"); + setColor(json, val -> this.colorTR = val, "colorTopRight", "colorTR"); + setColor(json, val -> this.colorBL = val, "colorBottomLeft", "colorBL"); + setColor(json, val -> this.colorBR = val, "colorBottomRight", "colorBR"); + this.cornerRadius = JsonHelper.getInt(json, 0, "cornerRadius"); + this.cornerSegments = JsonHelper.getInt(json, 10, "cornerSegments"); + if (JsonHelper.getBoolean(json, false, "solid")) { + this.borderThickness = 0; + } else if (JsonHelper.getBoolean(json, false, "hollow")) { + this.borderThickness = 1; + } else { + this.borderThickness = JsonHelper.getFloat(json, 0, "borderThickness"); + } + } + + @Override + public boolean saveToJson(JsonObject json) { + json.addProperty("colorTL", this.colorTL); + json.addProperty("colorTR", this.colorTR); + json.addProperty("colorBL", this.colorBL); + json.addProperty("colorBR", this.colorBR); + json.addProperty("cornerRadius", this.cornerRadius); + json.addProperty("cornerSegments", this.cornerSegments); + json.addProperty("borderThickness", this.borderThickness); + return true; + } + + private void setColor(JsonObject json, IntConsumer color, String... keys) { + JsonElement element = JsonHelper.getJsonElement(json, keys); + if (element != null) { + color.accept(Color.ofJson(element)); + } + } + + @Override + public Rectangle interpolate(Rectangle start, Rectangle end, float t) { + this.cornerRadius = Interpolations.lerp(start.cornerRadius, end.cornerRadius, t); + this.cornerSegments = Interpolations.lerp(start.cornerSegments, end.cornerSegments, t); + this.colorTL = Color.lerp(start.colorTL, end.colorTL, t); + this.colorTR = Color.lerp(start.colorTR, end.colorTR, t); + this.colorBL = Color.lerp(start.colorBL, end.colorBL, t); + this.colorBR = Color.lerp(start.colorBR, end.colorBR, t); + return this; + } + + @Override + public Rectangle copyOrImmutable() { + return new Rectangle() + .color(this.colorTL, this.colorTR, this.colorBL, this.colorBR) + .cornerRadius(this.cornerRadius) + .cornerSegments(this.cornerSegments) + .canApplyTheme(this.canApplyTheme); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/SchemaRenderer.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/SchemaRenderer.java new file mode 100644 index 00000000000..c9f4a10a285 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/SchemaRenderer.java @@ -0,0 +1,96 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.schema.ISchema; +import com.gregtechceu.gtceu.client.mui.schemarenderer.BaseSchemaRenderer; +import com.gregtechceu.gtceu.client.mui.schemarenderer.BlockHighlight; +import com.gregtechceu.gtceu.client.mui.schemarenderer.Camera; + +import net.minecraft.world.phys.BlockHitResult; + +import com.mojang.blaze3d.vertex.PoseStack; +import lombok.Setter; +import lombok.experimental.Accessors; +import lombok.experimental.Tolerate; +import org.jetbrains.annotations.NotNull; + +import java.util.function.*; + +@Accessors(fluent = true, chain = true) +public class SchemaRenderer extends BaseSchemaRenderer { + + @Setter + protected DoubleSupplier scale; + @Setter + protected BooleanSupplier disableBER; + @Setter + protected Consumer afterRender; + @Setter + protected BiConsumer cameraFunc; + @Setter + protected Supplier highlight; + @Setter + protected boolean isometric = false; + @Setter + private boolean rayTracing = false; + + public SchemaRenderer(ISchema schema) { + super(schema); + } + + @Tolerate + public SchemaRenderer scale(double scale) { + this.scale = () -> scale; + return this; + } + + @Tolerate + public SchemaRenderer disableBER(boolean disableBER) { + this.disableBER = () -> disableBER; + return this; + } + + public SchemaRenderer highlightRenderer(BlockHighlight highlight) { + this.highlight = () -> highlight; + this.rayTracing = true; + return this; + } + + @Override + protected void onSetupCamera() { + if (this.scale != null) { + camera().scaleDistanceKeepLookAt((float) this.scale.getAsDouble()); + } + if (this.cameraFunc != null) { + this.cameraFunc.accept(camera(), schema()); + } + } + + @Override + protected void onRendered() { + if (this.afterRender != null) { + this.afterRender.accept(this); + } + } + + @Override + protected void onSuccessfulRayTrace(PoseStack poseStack, @NotNull BlockHitResult result) { + if (this.highlight != null) { + this.highlight.get().renderHighlight(poseStack, result, camera().pos()); + } + } + + @Override + public boolean doRayTrace() { + return this.rayTracing; + } + + @Override + public boolean isBEREnabled() { + return this.disableBER == null || !this.disableBER.getAsBoolean(); + } + + @Override + public boolean isIsometric() { + return isometric; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Scrollbar.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Scrollbar.java new file mode 100644 index 00000000000..6886f662488 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/Scrollbar.java @@ -0,0 +1,69 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; + +import com.google.gson.JsonObject; +import lombok.Getter; + +public class Scrollbar implements IDrawable, IJsonSerializable { + + public static final Scrollbar DEFAULT = new Scrollbar(false); + public static final Scrollbar VANILLA = new Scrollbar(true); + + public static Scrollbar ofJson(JsonObject json) { + if (JsonHelper.getBoolean(json, false, "striped", "vanilla")) { + return VANILLA; + } + return DEFAULT; + } + + @Getter + private final boolean striped; + + public Scrollbar(boolean striped) { + this.striped = striped; + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + GuiDraw.drawRect(context.getGraphics(), x, y, width, height, Color.mix(0xFFEEEEEE, widgetTheme.getColor())); + GuiDraw.drawRect(context.getGraphics(), x + 1, y + 1, width - 1, height - 1, + Color.mix(0xFF666666, widgetTheme.getColor())); + GuiDraw.drawRect(context.getGraphics(), x + 1, y + 1, width - 2, height - 2, + Color.mix(0xFFAAAAAA, widgetTheme.getColor())); + + if (isStriped()) { + if (height <= 5 && width <= 5) return; + int color = widgetTheme.getTextColor(); + if (height >= width) { + int start = y + 2; + int end = height + start - 4; + for (int cy = start; cy < end; cy += 2) { + GuiDraw.drawRect(context.getGraphics(), x + 2, cy, width - 4, 1, color); + } + } else { + int start = x + 2; + int end = width + start - 4; + for (int cx = start; cx < end; cx += 2) { + GuiDraw.drawRect(context.getGraphics(), cx, y + 2, 1, height - 4, color); + } + } + } + } + + @Override + public boolean canApplyTheme() { + return true; + } + + @Override + public boolean saveToJson(JsonObject json) { + json.addProperty("striped", this.striped); + return true; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/SpriteDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/SpriteDrawable.java new file mode 100644 index 00000000000..8b2c374511b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/SpriteDrawable.java @@ -0,0 +1,43 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.client.renderer.texture.TextureAtlasSprite; + +public class SpriteDrawable implements IDrawable { + + private final TextureAtlasSprite sprite; + private boolean canApplyTheme = false; + + public SpriteDrawable(TextureAtlasSprite sprite) { + this.sprite = sprite; + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + GuiDraw.drawSprite(context.getLastGraphicsPose(), this.sprite, x, y, width, height); + } + + @Override + public Widget asWidget() { + return IDrawable.super.asWidget().size(this.sprite.contents().width(), this.sprite.contents().height()); + } + + @Override + public Icon asIcon() { + return IDrawable.super.asIcon().size(this.sprite.contents().width(), this.sprite.contents().height()); + } + + public SpriteDrawable canApplyTheme(boolean canApplyTheme) { + this.canApplyTheme = canApplyTheme; + return this; + } + + @Override + public boolean canApplyTheme() { + return canApplyTheme; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/TabTexture.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/TabTexture.java new file mode 100644 index 00000000000..8d7ee69158b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/TabTexture.java @@ -0,0 +1,102 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; + +import lombok.Getter; + +import java.util.Objects; + +public class TabTexture { + + public static TabTexture of(UITexture texture, GuiAxis axis, boolean positive, int width, int height, + int textureInset) { + Objects.requireNonNull(texture); + UITexture sa, ma, ea, si, mi, ei; + if (axis.isVertical() && !positive) { + si = texture.getSubArea(0, 0, 1 / 3f, 0.5f); + mi = texture.getSubArea(1 / 3f, 0, 2 / 3f, 0.5f); + ei = texture.getSubArea(2 / 3f, 0, 1f, 0.5f); + sa = texture.getSubArea(0, 0.5f, 1 / 3f, 1); + ma = texture.getSubArea(1 / 3f, 0.5f, 2 / 3f, 1); + ea = texture.getSubArea(2 / 3f, 0.5f, 1f, 1); + } else if (axis.isVertical()) { + sa = texture.getSubArea(0, 0, 1 / 3f, 0.5f); + ma = texture.getSubArea(1 / 3f, 0, 2 / 3f, 0.5f); + ea = texture.getSubArea(2 / 3f, 0, 1f, 0.5f); + si = texture.getSubArea(0, 0.5f, 1 / 3f, 1); + mi = texture.getSubArea(1 / 3f, 0.5f, 2 / 3f, 1); + ei = texture.getSubArea(2 / 3f, 0.5f, 1f, 1); + } else if (axis.isHorizontal() && !positive) { + si = texture.getSubArea(0, 0, 0.5f, 1 / 3f); + mi = texture.getSubArea(0, 1 / 3f, 0.5f, 2 / 3f); + ei = texture.getSubArea(0, 2 / 3f, 0.5f, 1f); + sa = texture.getSubArea(0.5f, 0, 1, 1 / 3f); + ma = texture.getSubArea(0.5f, 1 / 3f, 1, 2 / 3f); + ea = texture.getSubArea(0.5f, 2 / 3f, 1, 1f); + } else if (axis.isHorizontal()) { + sa = texture.getSubArea(0, 0, 0.5f, 1 / 3f); + ma = texture.getSubArea(0, 1 / 3f, 0.5f, 2 / 3f); + ea = texture.getSubArea(0, 2 / 3f, 0.5f, 1f); + si = texture.getSubArea(0.5f, 0, 1, 1 / 3f); + mi = texture.getSubArea(0.5f, 1 / 3f, 1, 2 / 3f); + ei = texture.getSubArea(0.5f, 2 / 3f, 1, 1f); + } else { + throw new IllegalArgumentException(); + } + return new TabTexture(sa, ma, ea, si, mi, ei, width, height, textureInset, axis, positive); + } + + private final UITexture startActive; + private final UITexture active; + private final UITexture endActive; + + private final UITexture startInactive; + private final UITexture inactive; + private final UITexture endInactive; + @Getter + private final int width, height; + @Getter + private final int textureInset; + @Getter + private final GuiAxis axis; + @Getter + private final boolean positive; + + public TabTexture(UITexture startActive, UITexture active, UITexture endActive, UITexture startInactive, + UITexture inactive, UITexture endInactive, int width, int height, int textureInset, GuiAxis axis, + boolean positive) { + this.startActive = startActive; + this.active = active; + this.endActive = endActive; + this.startInactive = startInactive; + this.inactive = inactive; + this.endInactive = endInactive; + this.width = width; + this.height = height; + this.textureInset = textureInset; + this.axis = axis; + this.positive = positive; + } + + public UITexture getStart(boolean active) { + return active ? this.startActive : this.startInactive; + } + + public UITexture getMiddle(boolean active) { + return active ? this.active : this.inactive; + } + + public UITexture getEnd(boolean active) { + return active ? this.endActive : this.endInactive; + } + + public UITexture get(int location, boolean active) { + if (location == 0) { + return getMiddle(active); + } + if (location < 0) { + return getStart(active); + } + return getEnd(active); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/TiledUITexture.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/TiledUITexture.java new file mode 100644 index 00000000000..aa94aee4431 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/TiledUITexture.java @@ -0,0 +1,55 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.resources.ResourceLocation; + +import com.google.gson.JsonObject; +import com.mojang.blaze3d.systems.RenderSystem; + +public class TiledUITexture extends UITexture { + + private final int imageWidth, imageHeight; + + /** + * Use {@link UITexture#builder()} with {@link Builder#tiled()} + */ + TiledUITexture(ResourceLocation location, float u0, float v0, float u1, float v1, int imageWidth, int imageHeight, + ColorType colorType, boolean nonOpaque) { + super(location, u0, v0, u1, v1, colorType, nonOpaque); + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + } + + @Override + public void draw(GuiContext context, float x, float y, float width, float height) { + if (width == this.imageWidth && height == this.imageHeight) { + super.draw(context, x, y, width, height); + return; + } + RenderSystem.setShader(GameRenderer::getPositionTexShader); + GuiDraw.drawTiledTexture(context.getLastGraphicsPose(), this.location, x, y, width, height, + this.u0, this.v0, this.u1, this.v1, + this.imageWidth, this.imageHeight, 0); + } + + @Override + public boolean saveToJson(JsonObject json) { + super.saveToJson(json); + if (json.entrySet().size() > 1) { + json.addProperty("tiled", true); + } + return true; + } + + @Override + protected TiledUITexture copy() { + return new TiledUITexture(location, u0, v0, u1, v1, imageWidth, imageHeight, colorType, nonOpaque); + } + + @Override + public TiledUITexture withColorOverride(int color) { + return (TiledUITexture) super.withColorOverride(color); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/UITexture.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/UITexture.java new file mode 100644 index 00000000000..4de608a7cc9 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/UITexture.java @@ -0,0 +1,578 @@ +package com.gregtechceu.gtceu.api.mui.drawable; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.utils.Interpolations; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; + +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.Nullable; + +@Accessors(fluent = true, chain = true) +public class UITexture implements IDrawable, IJsonSerializable { + + public static final UITexture DEFAULT = fullImage("gui/options_background", ColorType.DEFAULT); + + private static final ResourceLocation ICONS_LOCATION = GTCEu.id("textures/gui/icons.png"); + + // only for usage in GuiTextures + static UITexture icon(String name, int x, int y, int w, int h) { + return UITexture.builder() + .location(ICONS_LOCATION) + .imageSize(256, 256) + .subAreaXYWH(x, y, w, h) + .iconColorType() + .name(name) + .build(); + } + + static UITexture icon(String name, int x, int y) { + return icon(name, x, y, 16, 16); + } + + private static final String TEXTURES_PREFIX = "textures/"; + private static final String PNG_SUFFIX = ".png"; + + @Getter + public final ResourceLocation location; + public final float u0, v0, u1, v1; + @Getter + @Nullable + public final ColorType colorType; + public final boolean nonOpaque; + + private int colorOverride = 0; + + /** + * Creates a drawable texture + * + * @param location location of the texture + * @param u0 x offset of the image (0-1) + * @param v0 y offset of the image (0-1) + * @param u1 x end offset of the image (0-1) + * @param v1 y end offset of the image (0-1) + * @param colorType a function to get which color from a widget theme should be used to color this texture. + */ + public UITexture(ResourceLocation location, float u0, float v0, float u1, float v1, @Nullable ColorType colorType) { + this(location, u0, v0, u1, v1, colorType, false); + } + + /** + * Creates a drawable texture + * + * @param location location of the texture + * @param u0 x offset of the image (0-1) + * @param v0 y offset of the image (0-1) + * @param u1 x end offset of the image (0-1) + * @param v1 y end offset of the image (0-1) + * @param colorType a function to get which color from a widget theme should be used to color this texture. + * @param nonOpaque whether the texture should draw with blend (if true) or not (if false) + */ + public UITexture(ResourceLocation location, float u0, float v0, float u1, float v1, @Nullable ColorType colorType, + boolean nonOpaque) { + this.colorType = colorType; + boolean png = !location.getPath().endsWith(".png"); + boolean textures = !location.getPath().startsWith("textures/"); + if (png || textures) { + String path = location.getPath(); + path = png ? (textures ? TEXTURES_PREFIX + path + PNG_SUFFIX : path + PNG_SUFFIX) : TEXTURES_PREFIX + path; + location = new ResourceLocation(location.getNamespace(), path); + } + this.location = location; + this.u0 = u0; + this.v0 = v0; + this.u1 = u1; + this.v1 = v1; + this.nonOpaque = nonOpaque; + } + + public static Builder builder() { + return new Builder(); + } + + public static UITexture fullImage(ResourceLocation location) { + return new UITexture(location, 0, 0, 1, 1, null); + } + + public static UITexture fullImage(String location) { + return fullImage(new ResourceLocation(location), null); + } + + public static UITexture fullImage(String mod, String location) { + return fullImage(new ResourceLocation(mod, location), null); + } + + public static UITexture fullImage(ResourceLocation location, ColorType colorType) { + return new UITexture(location, 0, 0, 1, 1, colorType); + } + + public static UITexture fullImage(String location, ColorType colorType) { + return fullImage(new ResourceLocation(location), colorType); + } + + public static UITexture fullImage(String mod, String location, ColorType colorType) { + return fullImage(new ResourceLocation(mod, location), colorType); + } + + public UITexture getSubArea(Area bounds) { + return getSubArea(bounds.x, bounds.y, bounds.ex(), bounds.ey()); + } + + /** + * Returns a texture with a sub area relative to this area texture + * + * @param uStart x offset of the image (0-1) + * @param vStart y offset of the image (0-1) + * @param uEnd x end offset of the image (0-1) + * @param vEnd y end offset of the image (0-1) + * @return relative sub area + */ + public UITexture getSubArea(float uStart, float vStart, float uEnd, float vEnd) { + return new UITexture(this.location, lerpU(uStart), lerpV(vStart), lerpU(uEnd), lerpV(vEnd), this.colorType); + } + + protected final float lerpU(float u) { + return Interpolations.lerp(this.u0, this.u1, u); + } + + protected final float lerpV(float v) { + return Interpolations.lerp(this.v0, this.v1, v); + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + applyColor(this.colorType != null ? this.colorType.getColor(widgetTheme) : + ColorType.DEFAULT.getColor(widgetTheme)); + draw(context, (float) x, y, width, height); + } + + public void draw(GuiContext context, float x, float y, float width, float height) { + GuiDraw.drawTexture(context.getLastGraphicsPose(), this.location, x, y, x + width, y + height, this.u0, this.v0, + this.u1, this.v1); + } + + public void drawSubArea(GuiContext context, float x, float y, float width, float height, float uStart, float vStart, + float uEnd, + float vEnd, WidgetTheme widgetTheme) { + applyColor(this.colorType != null ? this.colorType.getColor(widgetTheme) : + ColorType.DEFAULT.getColor(widgetTheme)); + GuiDraw.drawTexture(context.getLastGraphicsPose(), this.location, x, y, x + width, y + height, lerpU(uStart), + lerpV(vStart), lerpU(uEnd), + lerpV(vEnd), this.nonOpaque); + } + + @Override + public void applyColor(int themeColor) { + if (this.colorOverride != 0) { + Color.setGlColor(this.colorOverride); + } else { + IDrawable.super.applyColor(themeColor); + } + } + + public static UITexture parseFromJson(JsonObject json) { + String name = JsonHelper.getString(json, null, "name", "id"); + if (name != null) { + UITexture drawable = DrawableSerialization.getTexture(name); + if (drawable != null) return drawable; + } + Builder builder = builder(); + builder.location(JsonHelper.getString(json, GTCEu.MOD_ID + ":gui/widgets/error", "location")) + .imageSize(JsonHelper.getInt(json, defaultImageWidth, "imageWidth", "iw"), + JsonHelper.getInt(json, defaultImageHeight, "imageHeight", "ih")); + boolean mode1 = json.has("x") || json.has("y") || json.has("w") || json.has("h") || json.has("width") || + json.has("height"); + boolean mode2 = json.has("u0") || json.has("v0") || json.has("u1") || json.has("u1"); + if (mode1) { + if (mode2) { + throw new JsonParseException("Tried to specify x, y, w, h and u0, v0, u1, v1!"); + } + builder.subAreaXYWH(JsonHelper.getInt(json, 0, "x"), + JsonHelper.getInt(json, 0, "y"), + JsonHelper.getInt(json, builder.iw, "w", "width"), + JsonHelper.getInt(json, builder.ih, "h", "height")); + } else if (mode2) { + builder.subAreaUV(JsonHelper.getFloat(json, 0, "u0"), + JsonHelper.getFloat(json, 0, "v0"), + JsonHelper.getFloat(json, 1, "u1"), + JsonHelper.getFloat(json, 1, "v1")); + } + int bl = JsonHelper.getInt(json, 0, "bl", "borderLeft", "borderX", "border"); + int br = JsonHelper.getInt(json, 0, "br", "borderRight", "borderY", "border"); + int bt = JsonHelper.getInt(json, 0, "bt", "borderTop", "borderBottom", "border"); + int bb = JsonHelper.getInt(json, 0, "bb", "borderBottom", "borderTop", "border"); + if (bl > 0 || br > 0 || bt > 0 || bb > 0) { + builder.adaptable(bl, bt, br, bb); + } + if (JsonHelper.getBoolean(json, false, "tiled")) { + builder.tiled(); + } + String colorTypeName = JsonHelper.getString(json, null, "colorType", "color"); + if (colorTypeName != null) { + builder.colorType(ColorType.get(colorTypeName)); + } else if (JsonHelper.getBoolean(json, false, "canApplyTheme")) { + builder.canApplyTheme(); + } + UITexture uiTexture = builder.build(); + uiTexture.colorOverride = JsonHelper.getColor(json, 0, "colorOverride"); + return builder.build(); + } + + @Override + public boolean saveToJson(JsonObject json) { + String name = DrawableSerialization.getTextureId(this); + if (name != null) { + json.addProperty("id", name); + return true; + } + json.addProperty("location", this.location.toString()); + json.addProperty("u0", this.u0); + json.addProperty("v0", this.v0); + json.addProperty("u1", this.u1); + json.addProperty("v1", this.v1); + if (this.colorType != null) json.addProperty("colorType", this.colorType.getName()); + json.addProperty("colorOverride", this.colorOverride); + return true; + } + + protected UITexture copy() { + return new UITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType); + } + + public UITexture withColorOverride(int color) { + UITexture t = copy(); + t.colorOverride = color; + return t; + } + + private static int defaultImageWidth = 16, defaultImageHeight = 16; + + public static void setDefaultImageSize(int w, int h) { + defaultImageWidth = w; + defaultImageHeight = h; + } + + /** + * A builder class to help create image textures. + */ + public static class Builder { + + private ResourceLocation location; + private int iw = defaultImageWidth, ih = defaultImageHeight; + private int x, y, w, h; + private float u0 = 0, v0 = 0, u1 = 1, v1 = 1; + private Mode mode = Mode.FULL; + private int bl = 0, bt = 0, br = 0, bb = 0; + private String name; + private boolean tiled = false; + private ColorType colorType = null; + private boolean nonOpaque = false; + + /** + * @param loc location of the image to draw + */ + public Builder location(ResourceLocation loc) { + this.location = loc; + return this; + } + + /** + * @param mod mod location of the image to draw + * @param path path of the image to draw + */ + public Builder location(String mod, String path) { + this.location = new ResourceLocation(mod, path); + return this; + } + + /** + * @param path path of the image to draw in minecraft asset folder + */ + public Builder location(String path) { + this.location = new ResourceLocation(path); + return this; + } + + /** + * Set the image size. Required for {@link #tiled()}, {@link #adaptable(int, int)} and + * {@link #subAreaXYWH(int, int, int, int)} + * + * @param w image width + * @param h image height + */ + public Builder imageSize(int w, int h) { + this.iw = w; + this.ih = h; + return this; + } + + /** + * This will make the image be drawn tiled rather than stretched. + * + * @param imageWidth image width + * @param imageHeight image height + */ + public Builder tiled(int imageWidth, int imageHeight) { + return tiled().imageSize(imageWidth, imageHeight); + } + + /** + * This will make the image be drawn tiled rather than stretched. + */ + public Builder tiled() { + this.tiled = true; + return this; + } + + /** + * Will draw the whole image file. + */ + public Builder fullImage() { + this.mode = Mode.FULL; + return this; + } + + /** + * Specify a sub area of the image in pixels, with a position and a size. + * + * @param x x in pixels + * @param y y in pixels + * @param w width in pixels + * @param h height in pixels + */ + public Builder subAreaXYWH(int x, int y, int w, int h) { + this.mode = Mode.PIXEL; + this.x = x; + this.y = y; + this.w = w; + this.h = h; + return this; + } + + /** + * Specify a sub area of the image in pixels, with a start position and an end position. + * + * @param left start position on the x-axis (equivalent to x in above methods) + * @param top start position on the y-axis (equivalent to y in above methods) + * @param right end position on the x-axis (equivalent to x + w in above methods) + * @param bottom end position on the y-axis (equivalent to y + h in above methods) + */ + public Builder subAreaLTRB(int left, int top, int right, int bottom) { + return subAreaXYWH(left, top, right - left, bottom - top); + } + + /** + * Specify a sub area of the image in relative uv values (0 - 1). u0 and v0 are start positions, while u1 and v1 + * are end positions. + * This means that the relative size is u1 - u0 and v1 - v0. + * + * @param u0 x start + * @param v0 y start + * @param u1 x end + * @param v1 y end + */ + public Builder subAreaUV(float u0, float v0, float u1, float v1) { + this.mode = Mode.RELATIVE; + this.u0 = u0; + this.v0 = v0; + this.u1 = u1; + this.v1 = v1; + return this; + } + + /** + * This will draw the corners, edges and body of the image separately. This will only stretch/tile the + * body so the border looks right on all sizes. This is also known as a + * 9-slice texture. + * + * @param bl left border width. Can be 0. + * @param bt top border width. Can be 0. + * @param br right border width. Can be 0. + * @param bb bottom border width. Can be 0. + */ + public Builder adaptable(int bl, int bt, int br, int bb) { + this.bl = bl; + this.bt = bt; + this.br = br; + this.bb = bb; + return this; + } + + /** + * This will draw the corners, edges and body of the image separately. This will only stretch/tile the + * body so the border looks right on all sizes. This is also known as a + * 9-slice texture. + * + * @param borderX left and right border width. Can be 0. + * @param borderY top and bottom border width. Can be 0 + */ + public Builder adaptable(int borderX, int borderY) { + return adaptable(borderX, borderY, borderX, borderY); + } + + /** + * This will draw the corners, edges and body of the image separately. This will only stretch/tile the + * body so the border looks right on all sizes. This is also known as a + * 9-slice texture. + * + * @param border border width + */ + public Builder adaptable(int border) { + return adaptable(border, border); + } + + /** + * Specify if theme color should apply to this texture. + * + * @see #defaultColorType() + */ + public Builder canApplyTheme() { + return defaultColorType(); + } + + /** + * Sets a function which defines how theme color is applied to this texture. Null means no color will be + * applied. + * + *
  • Background textures should use {@link ColorType#DEFAULT} or {@link #defaultColorType()}
  • + *
  • White icons (only has a shape and some grey shading) should use {@link ColorType#ICON} or + * {@link #iconColorType()}
  • + *
  • Text should use {@link ColorType#TEXT} or {@link #textColorType()}
  • + *
  • Everything else (f.e. colored icons and overlays) should use null
  • + *
    + * + * @param colorType function which defines how theme color is applied to this texture + * @return this + */ + public Builder colorType(@Nullable ColorType colorType) { + this.colorType = colorType; + return this; + } + + /** + * Sets this texture to use default theme color. + * Usually used for background textures (grey shaded). + * + * @return this + * @see #colorType(ColorType) + */ + public Builder defaultColorType() { + return colorType(ColorType.DEFAULT); + } + + /** + * Sets this texture to use text theme color. + * Usually used for texts. + * + * @return this + * @see #colorType(ColorType) + */ + public Builder textColorType() { + return colorType(ColorType.TEXT); + } + + /** + * Sets this texture to use icon theme color. + * Usually used for grey shaded icons without color. + * + * @return this + * @see #colorType(ColorType) + */ + public Builder iconColorType() { + return colorType(ColorType.ICON); + } + + /** + * Registers the texture with a name, so it can be used in json without creating the texture again. + * By default, theme color is applicable. + * + * @param name texture name + */ + public Builder name(String name) { + this.name = name; + return this; + } + + /** + * Sets this texture as at least partially transparent, will not disable glBlend when drawing. + */ + public Builder nonOpaque() { + this.nonOpaque = true; + return this; + } + + /** + * Creates the texture + * + * @return the created texture + */ + public UITexture build() { + UITexture texture = create(); + if (this.name == null) { + String[] p = texture.location.getPath().split("/"); + p = p[p.length - 1].split("\\."); + this.name = texture.location.getNamespace().equals(GTCEu.MOD_ID) ? p[0] : + texture.location.getNamespace() + ":" + p[0]; + if (DrawableSerialization.getTexture(this.name) != null) { + return texture; + } + } + DrawableSerialization.registerTexture(this.name, texture); + return texture; + } + + private UITexture create() { + if (this.location == null) { + throw new NullPointerException("Location must not be null"); + } + if (this.iw <= 0 || this.ih <= 0) throw new IllegalArgumentException("Image size must be > 0"); + if (this.mode == Mode.FULL) { + this.u0 = 0; + this.v0 = 0; + this.u1 = 1; + this.v1 = 1; + this.mode = Mode.RELATIVE; + } else if (this.mode == Mode.PIXEL) { + float tw = 1f / this.iw, th = 1f / this.ih; + this.u0 = this.x * tw; + this.v0 = this.y * th; + this.u1 = (this.x + this.w) * tw; + this.v1 = (this.y + this.h) * th; + this.mode = Mode.RELATIVE; + } + if (this.mode == Mode.RELATIVE) { + if (this.u0 < 0 || this.v0 < 0 || this.u1 > 1 || this.v1 > 1) + throw new IllegalArgumentException("UV values must be 0 - 1"); + if (this.bl > 0 || this.bt > 0 || this.br > 0 || this.bb > 0) { + return new AdaptableUITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType, + this.nonOpaque, this.iw, this.ih, this.bl, this.bt, this.br, this.bb, this.tiled); + } + if (this.tiled) { + return new TiledUITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.iw, this.ih, + this.colorType, this.nonOpaque); + } + return new UITexture(this.location, this.u0, this.v0, this.u1, this.v1, this.colorType, this.nonOpaque); + } + throw new IllegalStateException(); + } + } + + private enum Mode { + FULL, + PIXEL, + RELATIVE + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/AutoMajorTickFinder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/AutoMajorTickFinder.java new file mode 100644 index 00000000000..62a6fd327c8 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/AutoMajorTickFinder.java @@ -0,0 +1,54 @@ +package com.gregtechceu.gtceu.api.mui.drawable.graph; + +import lombok.Getter; +import lombok.Setter; + +public class AutoMajorTickFinder implements MajorTickFinder { + + @Getter + private final boolean autoAdjust; + @Setter + private double multiple = 10; + + public AutoMajorTickFinder(boolean autoAdjust) { + this.autoAdjust = autoAdjust; + } + + public AutoMajorTickFinder(double multiple) { + autoAdjust = false; + this.multiple = multiple; + } + + @Override + public double[] find(double min, double max, double[] ticks) { + int s = (int) Math.ceil((max - min) / multiple) + 2; + if (s > ticks.length) ticks = new double[s]; + double next = (Math.floor(min / multiple) * multiple); + for (int i = 0; i < s; i++) { + ticks[i] = next; + if (next > max) { + s = i + 1; + break; + } + next += multiple; + } + if (ticks.length > s) ticks[s] = Float.NaN; + return ticks; + } + + void calculateAutoTickMultiple(double min, double max) { + double step = (max - min) / 5; + if (step < 1) { + int significantPlaces = (int) Math.abs(Math.log10(step)) + 2; + double ten = Math.pow(10, significantPlaces); + step = (int) (step * ten + 0.2f) / ten; + } else if (step == 1) { + step = 0.2f; + } else { + int significantPlaces = (int) Math.log10(step) - 1; + double ten = Math.pow(10, significantPlaces); + step = (int) (step / ten + 0.2f) * ten; + } + setMultiple(step); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/AutoMinorTickFinder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/AutoMinorTickFinder.java new file mode 100644 index 00000000000..6354eff7af7 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/AutoMinorTickFinder.java @@ -0,0 +1,32 @@ +package com.gregtechceu.gtceu.api.mui.drawable.graph; + +public class AutoMinorTickFinder implements MinorTickFinder { + + private int amountBetweenMajors; + + public AutoMinorTickFinder(int amountBetweenMajors) { + this.amountBetweenMajors = amountBetweenMajors; + } + + @Override + public double[] find(double min, double max, double[] majorTicks, double[] ticks) { + int s = majorTicks.length * this.amountBetweenMajors; + if (ticks.length < s) ticks = new double[s]; + int k = 0; + for (int i = 0; i < majorTicks.length - 1; i++) { + if (Double.isNaN(majorTicks[i + 1])) break; + double next = majorTicks[i]; + double d = (majorTicks[i + 1] - next) / (amountBetweenMajors + 1); + for (int j = 0; j < amountBetweenMajors; j++) { + next += d; + if (next >= min) ticks[k++] = next; + if (next > max) { + ticks[k] = Float.NaN; + break; + } + } + } + if (k < ticks.length) ticks[k] = Float.NaN; + return ticks; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/GraphAxis.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/GraphAxis.java new file mode 100644 index 00000000000..e5670579252 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/GraphAxis.java @@ -0,0 +1,201 @@ +package com.gregtechceu.gtceu.api.mui.drawable.graph; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.api.mui.drawable.text.TextRenderer; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.utils.DAM; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.network.chat.Component; + +import com.mojang.blaze3d.vertex.VertexConsumer; +import lombok.Getter; +import org.jetbrains.annotations.ApiStatus; +import org.joml.Matrix4f; + +import java.text.DecimalFormat; +import java.util.List; + +@ApiStatus.Experimental +public class GraphAxis { + + private static final TextRenderer textRenderer = new TextRenderer(); + private static final float TICK_LABEL_SCALE = 0.4f; + private static final float AXIS_LABEL_SCALE = 1f; + private static final float TICK_LABEL_OFFSET = 2f; + private static final float AXIS_LABEL_OFFSET = 3f; + + @Getter + public final GuiAxis axis; + + public double[] majorTicks = new double[8]; + public double[] minorTicks = new double[16]; + public TextRenderer.Line[] tickLabels = new TextRenderer.Line[8]; + private float maxLabelWidth = 0; + @Getter + public MajorTickFinder majorTickFinder = new AutoMajorTickFinder(true); + @Getter + public MinorTickFinder minorTickFinder = new AutoMinorTickFinder(2); + @Getter + public String label; + @Getter + public double min, max; + public boolean autoLimits = true; + public float[] data; + + public GraphAxis(GuiAxis axis) { + this.axis = axis; + } + + void compute(List plots) { + if (this.autoLimits) { + if (plots.isEmpty()) { + this.min = 0; + this.max = 0; + } else if (plots.size() == 1) { + this.min = DAM.min(plots.get(0).getData(this.axis)); + this.max = DAM.max(plots.get(0).getData(this.axis)); + } else { + double min = Double.MAX_VALUE, max = Double.MIN_VALUE; + for (Plot plot : plots) { + double m = DAM.min(plot.getData(this.axis)); + if (m < min) min = m; + m = DAM.max(plot.getData(this.axis)); + if (m > max) max = m; + } + this.min = min; + this.max = max; + } + if (this.axis.isVertical()) { + double padding = (this.max - this.min) * 0.05f; + this.max += padding; + this.min -= padding; + } + } + if (this.majorTickFinder instanceof AutoMajorTickFinder tickFinder && tickFinder.isAutoAdjust()) { + tickFinder.calculateAutoTickMultiple(this.min, this.max); + } + this.majorTicks = this.majorTickFinder.find(this.min, this.max, this.majorTicks); + this.minorTicks = this.minorTickFinder.find(this.min, this.max, this.majorTicks, this.minorTicks); + + if (this.tickLabels.length < this.majorTicks.length) { + this.tickLabels = new TextRenderer.Line[this.majorTicks.length]; + } + textRenderer.setScale(TICK_LABEL_SCALE); + this.maxLabelWidth = 0; + double maxDiff = DAM.max(DAM.diff(this.majorTicks)); + int significantPlaces = (int) Math.abs(Math.log10(maxDiff)) + 2; + DecimalFormat format = new DecimalFormat(); + format.setMaximumFractionDigits(significantPlaces); + for (int i = 0; i < this.tickLabels.length; i++) { + if (Double.isNaN(this.majorTicks[i])) break; + this.tickLabels[i] = textRenderer + .line(Component.literal(format.format(this.majorTicks[i])).getVisualOrderText()); + if (this.tickLabels[i].getWidth() > this.maxLabelWidth) { + this.maxLabelWidth = this.tickLabels[i].getWidth(); + } + } + } + + void applyPadding(GraphView graphView) { + textRenderer.setScale(TICK_LABEL_SCALE); + if (this.axis.isHorizontal()) { + graphView.sy1 -= textRenderer.getFontHeight() + TICK_LABEL_OFFSET; + if (this.label != null) { + textRenderer.setScale(AXIS_LABEL_SCALE); + graphView.sy1 -= textRenderer.getFontHeight() + AXIS_LABEL_OFFSET; + } + } else { + float off = this.maxLabelWidth + TICK_LABEL_OFFSET; + if (this.label != null) { + textRenderer.setScale(AXIS_LABEL_SCALE); + off += textRenderer.getFontHeight() + AXIS_LABEL_OFFSET; + } + graphView.sx0 += off; + } + } + + void drawGridLines(Matrix4f pose, VertexConsumer buffer, GraphView view, GraphAxis other, boolean major, float d, + int r, int g, int b, int a) { + double[] pos = major ? this.majorTicks : this.minorTicks; + float dHalf = d / 2; + if (axis.isHorizontal()) { + float otherMin = view.g2sY(other.max); + float otherMax = view.g2sY(other.min); + drawLinesOnHorizontal(pose, buffer, view, pos, dHalf, otherMin, otherMax, r, g, b, a); + } else { + float otherMin = view.g2sX(other.min); + float otherMax = view.g2sX(other.max); + drawLinesOnVertical(pose, buffer, view, pos, dHalf, otherMin, otherMax, r, g, b, a); + } + } + + void drawTicks(Matrix4f pose, VertexConsumer buffer, GraphView view, GraphAxis other, boolean major, + float thickness, float length, int r, int g, int b, int a) { + double[] pos = major ? this.majorTicks : this.minorTicks; + float dHalf = thickness / 2; + if (axis.isHorizontal()) { + float otherMin = view.g2sY(other.min); + drawLinesOnHorizontal(pose, buffer, view, pos, dHalf, otherMin - length, otherMin, r, g, b, a); + } else { + float otherMin = view.g2sX(other.min); + drawLinesOnVertical(pose, buffer, view, pos, dHalf, otherMin, otherMin + length, r, g, b, a); + } + } + + private void drawLinesOnHorizontal(Matrix4f pose, VertexConsumer buffer, GraphView view, double[] pos, float dHalf, + float crossLow, float crossHigh, int r, int g, int b, int a) { + for (double p : pos) { + if (Double.isNaN(p)) break; + if (p < min || p > max) continue; + + float fp = view.g2sX(p); + + float x0 = fp - dHalf; + float x1 = fp + dHalf; + GuiDraw.drawRectRaw(buffer, pose, x0, crossLow, x1, crossHigh, r, g, b, a); + } + } + + private void drawLinesOnVertical(Matrix4f pose, VertexConsumer buffer, GraphView view, double[] pos, float dHalf, + float crossLow, float crossHigh, int r, int g, int b, int a) { + for (double p : pos) { + if (Double.isNaN(p)) break; + if (p < min || p > max) continue; + + float fp = view.g2sY(p); + + float y0 = fp - dHalf; + float y1 = fp + dHalf; + GuiDraw.drawRectRaw(buffer, pose, crossLow, y0, crossHigh, y1, r, g, b, a); + } + } + + void drawLabels(GraphView view, GraphAxis other, GuiGraphics graphics) { + textRenderer.setHardWrapOnBorder(false); + if (axis.isHorizontal()) { + textRenderer.setScale(TICK_LABEL_SCALE); + textRenderer.setAlignment(Alignment.TopCenter, 100); + float y = view.g2sY(other.min) + TICK_LABEL_OFFSET; + for (int i = 0; i < this.majorTicks.length; i++) { + double pos = this.majorTicks[i]; + if (Double.isNaN(pos)) break; + if (pos < min || pos > max) continue; + textRenderer.setPos((int) (view.g2sX(pos) - 50), (int) y); + textRenderer.drawSimple(graphics, this.tickLabels[i].getText()); + } + } else { + textRenderer.setScale(TICK_LABEL_SCALE); + textRenderer.setAlignment(Alignment.CenterRight, this.maxLabelWidth, 20); + float x = view.g2sX(other.min) - TICK_LABEL_OFFSET - this.maxLabelWidth; + for (int i = 0; i < this.majorTicks.length; i++) { + double pos = this.majorTicks[i]; + if (Double.isNaN(pos)) break; + if (pos < min || pos > max) continue; + textRenderer.setPos((int) x, (int) (view.g2sY(pos) - 10)); + textRenderer.drawSimple(graphics, this.tickLabels[i].getText()); + } + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/GraphDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/GraphDrawable.java new file mode 100644 index 00000000000..3d0bcdeb1c5 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/GraphDrawable.java @@ -0,0 +1,283 @@ +package com.gregtechceu.gtceu.api.mui.drawable.graph; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.RenderType; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.VertexConsumer; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +public class GraphDrawable implements IDrawable { + + private final GraphView view = new GraphView(); + // private IDrawable background; + private int backgroundColor = Color.WHITE.main; + + private float majorTickThickness = 1f, majorTickLength = 3f, minorTickThickness = 0.5f, minorTickLength = 1.5f; + private float gridLineWidth = 0.5f; + private int gridLineColor = Color.withAlpha(Color.BLACK.main, 0.4f); + private float minorGridLineWidth = 0f; + private int minorGridLineColor = Color.withAlpha(Color.BLACK.main, 0.15f); + + @Getter + private final GraphAxis x = new GraphAxis(GuiAxis.X), y = new GraphAxis(GuiAxis.Y); + private final List plots = new ArrayList<>(); + + private boolean dirty = true; + + public void redraw() { + this.dirty = true; + for (Plot plot : this.plots) plot.redraw(); + } + + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + if (this.view.setScreen(x, y, x + width, y + height) | compute()) { + this.x.applyPadding(this.view); + this.y.applyPadding(this.view); + this.view.setGraph(this.x.min, this.y.min, this.x.max, this.y.max); + this.view.postResize(); + } + + var graphics = context.getGraphics(); + // background + if (this.backgroundColor != 0) { + GuiDraw.drawRect(graphics, this.view.sx0, this.view.sy0, this.view.getScreenWidth(), + this.view.getScreenHeight(), this.backgroundColor); + } + + RenderSystem.setShader(GameRenderer::getPositionColorShader); + var buffer = graphics.bufferSource().getBuffer(RenderType.guiOverlay()); + // grid lines + drawGrid(graphics, buffer); + + var stencil = context.getStencil(); + stencil.push((int) this.view.sx0, (int) this.view.sy0, (int) (this.view.getScreenWidth() + 1), + (int) (this.view.getScreenHeight() + 1)); + // plots + for (Plot plot : this.plots) { + plot.draw(graphics, this.view); + } + stencil.pop(); + // axis ticks + buffer = graphics.bufferSource().getBuffer(RenderType.guiOverlay()); + drawTicks(graphics, buffer); + + // GuiDraw.drawBorderOutsideLTRB(graphics, this.view.sx0, this.view.sy0, this.view.sx1, this.view.sy1, 0.5f, + // Color.BLACK.main); + this.x.drawLabels(this.view, this.y, graphics); + this.y.drawLabels(this.view, this.x, graphics); + } + + public void drawGrid(GuiGraphics graphics, VertexConsumer buffer) { + if (this.minorGridLineWidth > 0) { + int r = Color.getRed(this.minorGridLineColor); + int g = Color.getGreen(this.minorGridLineColor); + int b = Color.getBlue(this.minorGridLineColor); + int a = Color.getAlpha(this.minorGridLineColor); + this.x.drawGridLines(graphics.pose().last().pose(), buffer, this.view, this.y, false, + this.minorGridLineWidth, r, g, b, a); + } + if (this.gridLineWidth > 0) { + int r = Color.getRed(this.gridLineColor); + int g = Color.getGreen(this.gridLineColor); + int b = Color.getBlue(this.gridLineColor); + int a = Color.getAlpha(this.gridLineColor); + var pose = graphics.pose().last().pose(); + this.x.drawGridLines(pose, buffer, this.view, this.y, true, this.gridLineWidth, r, g, b, a); + this.y.drawGridLines(pose, buffer, this.view, this.x, true, this.gridLineWidth, r, g, b, a); + } + } + + public void drawTicks(GuiGraphics graphics, VertexConsumer buffer) { + var pose = graphics.pose().last().pose(); + this.x.drawTicks(pose, buffer, this.view, this.y, false, this.minorTickThickness, this.minorTickLength, 0, 0, 0, + 0xFF); + this.y.drawTicks(pose, buffer, this.view, this.x, false, this.minorTickThickness, this.minorTickLength, 0, 0, 0, + 0xFF); + this.x.drawTicks(pose, buffer, this.view, this.y, true, this.majorTickThickness, this.majorTickLength, 0, 0, 0, + 0xFF); + this.y.drawTicks(pose, buffer, this.view, this.x, true, this.majorTickThickness, this.majorTickLength, 0, 0, 0, + 0xFF); + } + + private boolean compute() { + if (!this.dirty) return false; + this.dirty = false; + this.x.compute(this.plots); + this.y.compute(this.plots); + int colorIndex = 0; + for (Plot plot : this.plots) { + if (plot.defaultColor) { + plot.color = Plot.DEFAULT_PLOT_COLORS[colorIndex]; + if (++colorIndex == Plot.DEFAULT_PLOT_COLORS.length) { + colorIndex = 0; + } + } + } + return true; + } + + public GraphDrawable autoXLim() { + this.x.autoLimits = true; + redraw(); + return this; + } + + public GraphDrawable autoYLim() { + this.y.autoLimits = true; + redraw(); + return this; + } + + public GraphDrawable xLim(float min, float max) { + this.x.min = min; + this.x.max = max; + this.x.autoLimits = false; + redraw(); + return this; + } + + public GraphDrawable yLim(float min, float max) { + this.y.min = min; + this.y.max = max; + this.y.autoLimits = false; + redraw(); + return this; + } + + public GraphDrawable majorTickStyle(float thickness, float length) { + this.majorTickThickness = thickness; + this.majorTickLength = length; + return this; + } + + public GraphDrawable minorTickStyle(float thickness, float length) { + this.minorTickThickness = thickness; + this.minorTickLength = length; + return this; + } + + public GraphDrawable xTickFinder(MajorTickFinder majorTickFinder, MinorTickFinder minorTickFinder) { + this.x.majorTickFinder = majorTickFinder; + this.x.minorTickFinder = minorTickFinder; + redraw(); + return this; + } + + public GraphDrawable yTickFinder(MajorTickFinder majorTickFinder, MinorTickFinder minorTickFinder) { + this.y.majorTickFinder = majorTickFinder; + this.y.minorTickFinder = minorTickFinder; + redraw(); + return this; + } + + public GraphDrawable xTickFinder(float majorMultiples, int minorTicksBetweenMajors) { + return xTickFinder(new AutoMajorTickFinder(majorMultiples), new AutoMinorTickFinder(minorTicksBetweenMajors)); + } + + public GraphDrawable yTickFinder(float majorMultiples, int minorTicksBetweenMajors) { + return yTickFinder(new AutoMajorTickFinder(majorMultiples), new AutoMinorTickFinder(minorTicksBetweenMajors)); + } + + public GraphDrawable backgroundColor(int color) { + if (color != 0 && Color.getAlpha(color) == 0) { + color = Color.withAlpha(color, 0xFF); + } + this.backgroundColor = color; + return this; + } + + public GraphDrawable plot(double[] x, double[] y) { + return plot(new Plot().data(x, y)); + } + + public GraphDrawable plot(double[] x, double[] y, int color) { + return plot(new Plot() + .data(x, y) + .color(color)); + } + + public GraphDrawable plot(double[] x, double[] y, float thickness) { + return plot(new Plot() + .data(x, y) + .thickness(thickness)); + } + + public GraphDrawable plot(double[] x, double[] y, float thickness, int color) { + return plot(new Plot() + .data(x, y) + .thickness(thickness) + .color(color)); + } + + public GraphDrawable plot(Plot plot) { + this.plots.add(plot); + plot.redraw(); + return this; + } + + public GraphDrawable majorGridStyle(float thickness, int color) { + this.gridLineWidth = thickness; + this.gridLineColor = color; + return this; + } + + public GraphDrawable minorGridStyle(float thickness, int color) { + this.minorGridLineWidth = thickness; + this.minorGridLineColor = color; + return this; + } + + public GraphDrawable disableMajorGrid() { + return majorGridLineThickness(0); + } + + public GraphDrawable disableMinorGrid() { + return minorGridLineThickness(0); + } + + public GraphDrawable enableMajorGrid() { + return majorGridLineThickness(0.5f); + } + + public GraphDrawable enableMinorGrid() { + return majorGridLineThickness(0.25f); + } + + public GraphDrawable majorGridLineThickness(float thickness) { + this.gridLineWidth = thickness; + return this; + } + + public GraphDrawable minorGridLineThickness(float thickness) { + this.minorGridLineWidth = thickness; + return this; + } + + public GraphDrawable majorGridLineColor(int color) { + this.gridLineColor = color; + return this; + } + + public GraphDrawable minorGridLineColor(int color) { + this.minorGridLineColor = color; + return this; + } + + public GraphDrawable graphAspectRatio(float aspectRatio) { + this.view.setAspectRatio(aspectRatio); + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/GraphView.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/GraphView.java new file mode 100644 index 00000000000..5ec4b5de883 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/GraphView.java @@ -0,0 +1,141 @@ +package com.gregtechceu.gtceu.api.mui.drawable.graph; + +import lombok.Getter; +import lombok.Setter; + +public class GraphView { + + @Setter + @Getter + float aspectRatio = 0; + // screen rectangle + float sx0, sy0, sx1, sy1; + // graph rectangle + double gx0, gy0, gx1, gy1; + + float zeroX, zeroY; + + void postResize() { + if (this.aspectRatio > 0) { + float w = sx1 - sx0, h = sy1 - sy0; + float properW = this.aspectRatio * h; + if (w > properW) { + float d = w - properW; + sx0 += d / 2; + sx1 -= d / 2; + } else if (properW > w) { + float properH = w / this.aspectRatio; + float d = h - properH; + sy0 += d / 2; + sy1 -= d / 2; + } + } + this.zeroX = g2sX(0); + this.zeroY = g2sY(0); + } + + boolean setScreen(float x0, float y0, float x1, float y1) { + if (x0 != this.sx0 || y0 != this.sy0 || x1 != this.sx1 || y1 != this.sy1) { + this.sx0 = x0; + this.sy0 = y0; + this.sx1 = x1; + this.sy1 = y1; + return true; + } + return false; + } + + void setGraph(double x0, double y0, double x1, double y1) { + this.gx0 = x0; + this.gy0 = y0; + this.gx1 = x1; + this.gy1 = y1; + this.zeroX = g2sX(0); + this.zeroY = g2sY(0); + } + + public float g2sX(double v) { + return (float) transform(v, gx0, gx1, sx0, sx1); + } + + public float g2sY(double v) { + // gy0 and gy1 inverted on purpose + // screen y0 is top, graph y0 is bottom + return (float) transform(v, gy1, gy0, sy0, sy1); + } + + public double g2sScaleX() { + return scale(gx0, gx1, sx0, sx1); + } + + public double g2sScaleY() { + return scale(gy1, gy0, sy0, sy1); + } + + public double s2gX(float v) { + return transform(v, sx0, sx1, gx0, gx1); + } + + public double s2gY(float v) { + // gy0 and gy1 inverted on purpose + // screen y0 is top, graph y0 is bottom + return transform(v, sy0, sy1, gy1, gy0); + } + + private double transform(double v, double fromMin, double fromMax, double toMin, double toMax) { + v = (v - fromMin) / (fromMax - fromMin); // reverse lerp + return toMin + (toMax - toMin) * v; + } + + private double scale(double fromMin, double fromMax, double toMin, double toMax) { + return (toMax - toMin) / (fromMax - fromMin); + } + + public double getGraphX0() { + return gx0; + } + + public double getGraphX1() { + return gx1; + } + + public double getGraphY0() { + return gy0; + } + + public double getGraphY1() { + return gy1; + } + + public float getScreenX0() { + return sx0; + } + + public float getScreenX1() { + return sx1; + } + + public float getScreenY0() { + return sy0; + } + + public float getScreenY1() { + return sy1; + } + + public float getScreenWidth() { + return sx1 - sx0; + } + + public float getScreenHeight() { + return sy1 - sy0; + } + + public double getGraphWidth() { + return gx1 - gx0; + } + + public double getGraphHeight() { + return gy1 - gy0; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/MajorTickFinder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/MajorTickFinder.java new file mode 100644 index 00000000000..8eaf332fc2c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/MajorTickFinder.java @@ -0,0 +1,9 @@ +package com.gregtechceu.gtceu.api.mui.drawable.graph; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public interface MajorTickFinder { + + double[] find(double min, double max, double[] ticks); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/MinorTickFinder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/MinorTickFinder.java new file mode 100644 index 00000000000..325b9a1ac0a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/MinorTickFinder.java @@ -0,0 +1,9 @@ +package com.gregtechceu.gtceu.api.mui.drawable.graph; + +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Experimental +public interface MinorTickFinder { + + double[] find(double min, double max, double[] majorTicks, double[] ticks); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/Plot.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/Plot.java new file mode 100644 index 00000000000..cd91d64ada9 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/graph/Plot.java @@ -0,0 +1,220 @@ +package com.gregtechceu.gtceu.api.mui.drawable.graph; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.utils.DAM; +import com.gregtechceu.gtceu.api.mui.utils.Interpolations; +import com.gregtechceu.gtceu.client.renderer.GTRenderTypes; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.util.Mth; + +import com.mojang.blaze3d.systems.RenderSystem; +import lombok.Getter; + +public class Plot { + + public static final int[] DEFAULT_PLOT_COLORS = { + Color.BLUE_ACCENT.main, + Color.ORANGE_ACCENT.darker(0), + Color.GREEN.main, + Color.RED.main, + Color.DEEP_PURPLE_ACCENT.main, + Color.BROWN.main, + Color.TEAL.main, + Color.LIME.main + }; + + double[] xs = DAM.EMPTY; + double[] ys = DAM.EMPTY; + @Getter + float thickness = 1f; + boolean defaultColor = true; + @Getter + int color; + + private float[] vertexBuffer; // screen coords need to be way less accurate than graph coords, so float is fine + private boolean dirty = true; + + public void redraw() { + this.dirty = true; + } + + private void redraw(GraphView view) { + long time = System.nanoTime(); + float dHalf = thickness * 0.5f; + + int n = xs.length * 4; + this.vertexBuffer = new float[n]; + int vertexIndex = 0; + + // first only calculate the start point vertices + // they are dependent on the first and second point + float x0 = view.g2sX(xs[0]); + float y0 = view.g2sY(ys[0]); + float x1 = view.g2sX(xs[1]); + float y1 = view.g2sY(ys[1]); + // last pos + float lx = x0; + float ly = y0; + + float dx = x1 - x0; + float dy = y1 - y0; + float len = Mth.sqrt(dx * dx + dy * dy); + if (len == 0) throw new IllegalArgumentException("Graph can't handle the same point back to back!"); + dx /= len; + dy /= len; + // perpendicular + float px = -dy; + float py = dx; + // last perpendicular thickness offset + float lpox = px * dHalf; + float lpoy = py * dHalf; + + vertexIndex = storePoints(vertexIndex, view, lx, ly, lpox, lpoy); + + // calculate all points except start and endpoint + // these depend on both their neighbors + for (int i = 1; i < xs.length - 1; i++) { + x0 = view.g2sX(xs[i]); + y0 = view.g2sY(ys[i]); + x1 = view.g2sX(xs[i + 1]); + y1 = view.g2sY(ys[i + 1]); + + dx = x1 - x0; + dy = y1 - y0; + len = Mth.sqrt(dx * dx + dy * dy); + if (len == 0) continue; + dx /= len; + dy /= len; + // perpendicular + px = -dy; + py = dx; + // perpendicular thickness offset + float pox = px * dHalf; + float poy = py * dHalf; + float ox, oy; + if (pox == lpox && poy == lpoy) { // linear + ox = pox; + oy = poy; + } else { + // get the average offset of this and the last point and of this and the next point + ox = Interpolations.lerp(lpox, pox, 0.5f); + oy = Interpolations.lerp(lpoy, poy, 0.5f); + // normalize it + len = Mth.sqrt(ox * ox + oy * oy); + ox /= len; + oy /= len; + // angle between now offset vector and last perpendicular offset vector + float cosAngle = (ox * lpox + oy * lpoy) / (1 * dHalf); + // calc hypotenuse and use it to calculate the actual length of the offset vector + float hypotenuse = this.thickness / cosAngle; + ox *= hypotenuse * 0.5f; + oy *= hypotenuse * 0.5f; + } + + vertexIndex = storePoints(vertexIndex, view, x0, y0, ox, oy); + + lx = x0; + ly = y0; + lpox = pox; + lpoy = poy; + } + + // finally calculate endpoint + // this depends on itself and the point before + int last = this.xs.length - 1; + x0 = lx; + y0 = ly; + x1 = view.g2sX(xs[last]); + y1 = view.g2sY(ys[last]); + + dx = x1 - x0; + dy = y1 - y0; + len = Mth.sqrt(dx * dx + dy * dy); + if (len == 0) throw new IllegalArgumentException("Graph can't handle the same point back to back!"); + dx /= len; + dy /= len; + // perpendicular + px = -dy; + py = dx; + // last perpendicular thickness offset + lpox = px * dHalf; + lpoy = py * dHalf; + + storePoints(vertexIndex, view, x1, y1, lpox, lpoy); + time = System.nanoTime() - time; + // GTCEu.LOGGER.error("Calculating vertices from {} data points took {}s", xs.length, + // FormattingUtil.formatNumberReadable(time)); + } + + private int storePoints(int index, GraphView view, float sx, float sy, float ox, float oy) { + this.vertexBuffer[index++] = sx - ox; + this.vertexBuffer[index++] = sy - oy; + this.vertexBuffer[index++] = sx + ox; + this.vertexBuffer[index++] = sy + oy; + return index; + } + + public void draw(GuiGraphics graphics, GraphView view) { + if (xs.length == 0) return; + if (xs.length == 1) { + GuiDraw.drawRect(graphics, view.g2sX(xs[0]) - thickness / 2, view.g2sY(ys[0]) - thickness / 2, thickness, + thickness, color); + return; + } + if (this.dirty) { + redraw(view); + this.dirty = false; + } + int r = Color.getRed(color); + int g = Color.getGreen(color); + int b = Color.getBlue(color); + int a = Color.getAlpha(color); + + RenderSystem.setShader(GameRenderer::getPositionColorShader); + var pose = graphics.pose().last().pose(); + var buffer = graphics.bufferSource().getBuffer(GTRenderTypes.guiTriangleStrip()); + long time = System.nanoTime(); + for (int i = 0; i < this.vertexBuffer.length; i += 2) { + buffer.vertex(pose, this.vertexBuffer[i], this.vertexBuffer[i + 1], 0).color(r, g, b, a).endVertex(); + time = System.nanoTime() - time; + // GTCEu.LOGGER.error("Drawing plot with {} points took {}s", xs.length, + // FormattingUtil.formatNumberReadable(time)); + } + } + + public double[] getX() { + return xs; + } + + public double[] getY() { + return ys; + } + + public double[] getData(GuiAxis axis) { + return axis.isHorizontal() ? this.xs : this.ys; + } + + public Plot data(double[] x, double[] y) { + if (x.length != y.length) throw new IllegalArgumentException("X and Y must have the same length!"); + this.xs = x; + this.ys = y; + redraw(); + return this; + } + + public Plot thickness(float thickness) { + this.thickness = thickness; + redraw(); + return this; + } + + public Plot color(int color) { + this.color = color; + this.defaultColor = color == 0; + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/AnimatedText.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/AnimatedText.java new file mode 100644 index 00000000000..680a68f245d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/AnimatedText.java @@ -0,0 +1,151 @@ +package com.gregtechceu.gtceu.api.mui.drawable.text; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.IntSupplier; + +public class AnimatedText extends StyledText { + + private MutableComponent full; + private String fullString; + private String currentString = ""; + private int currentIndex; + private int speed = 40; // ms per char + private long timeLastDraw; + private boolean forward = true; + + private boolean isAnimating = false; + + public AnimatedText(IKey key) { + super(key); + } + + @Override + public MutableComponent get() { + return Component.literal(this.currentString); + } + + public void reset() { + this.full = null; + } + + private void advance() { + if (!this.isAnimating || (this.forward && this.currentIndex >= this.fullString.length()) || + (!this.forward && this.currentIndex < 0)) + return; + long time = Util.getMillis(); + int amount = (int) ((time - this.timeLastDraw) / this.speed); + if (amount == 0) return; + if (this.forward) { + int max = Math.min(this.fullString.length() - 1, this.currentIndex + amount); + this.currentIndex = Math.max(this.currentIndex, 0); + for (int i = this.currentIndex; i < max; i++) { + char c = this.fullString.charAt(i); + if (c == ' ') { + max = Math.min(this.fullString.length() - 1, max + 1); + } + // noinspection StringConcatenationInLoop + this.currentString += c; + } + this.currentIndex = max; + } else { + int min = Math.max(0, this.currentIndex - amount); + this.currentIndex = Math.min(this.currentIndex, this.currentString.length() - 1); + for (int i = this.currentIndex; i >= min; i--) { + char c = this.fullString.charAt(i); + if (c == ' ') { + min = Math.max(0, min - 1); + } + this.currentString = this.currentString.substring(0, i); + } + this.currentIndex = min; + } + this.timeLastDraw += (long) amount * this.speed; + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + if (this.full == null || !this.full.equals(super.get())) { + if (this.isAnimating) { + this.full = super.get(); + this.fullString = this.full.getString(); + this.currentString = this.forward ? "" : this.fullString; + this.currentIndex = this.forward ? 0 : this.fullString.length() - 1; + this.timeLastDraw = Util.getMillis(); + } else { + this.currentString = this.forward ? "" : this.fullString; + } + } + advance(); + if (this.currentString.isEmpty()) return; + super.draw(context, x, y, width, height, widgetTheme); + } + + public AnimatedText startAnimation() { + this.isAnimating = true; + return this; + } + + public AnimatedText stopAnimation() { + this.isAnimating = false; + return this; + } + + public AnimatedText forward(boolean forward) { + this.forward = forward; + return this; + } + + @Override + public AnimatedText style(ChatFormatting formatting) { + return (AnimatedText) super.style(formatting); + } + + @Override + public AnimatedText alignment(Alignment alignment) { + return (AnimatedText) super.alignment(alignment); + } + + @Override + public AnimatedText color(int color) { + return color(() -> color); + } + + @Override + public AnimatedText color(@Nullable IntSupplier color) { + return (AnimatedText) super.color(color); + } + + @Override + public AnimatedText scale(float scale) { + return (AnimatedText) super.scale(scale); + } + + @Override + public AnimatedText shadow(@Nullable Boolean shadow) { + return (AnimatedText) super.shadow(shadow); + } + + /** + * How fast the characters appear + * + * @param speed in ms per character + */ + public AnimatedText speed(int speed) { + this.speed = speed; + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/BaseKey.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/BaseKey.java new file mode 100644 index 00000000000..6029ace8eeb --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/BaseKey.java @@ -0,0 +1,49 @@ +package com.gregtechceu.gtceu.api.mui.drawable.text; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.MutableComponent; + +import lombok.Getter; +import org.apache.commons.lang3.NotImplementedException; +import org.jetbrains.annotations.Nullable; + +public abstract class BaseKey implements IKey { + + @Getter + private @Nullable FormattingState formatting; + + @Override + public MutableComponent getFormatted(@Nullable FormattingState parentFormatting) { + return FontRenderHelper.format(this.formatting, parentFormatting, get()); + } + + @Override + public BaseKey style(@Nullable ChatFormatting formatting) { + if (this.formatting == null) { + this.formatting = new FormattingState(); + } + if (formatting == null) this.formatting.forceDefaultColor(); + else this.formatting.add(formatting, false); + return this; + } + + @Override + public IKey removeStyle() { + if (this.formatting != null) { + this.formatting.reset(); + } + return this; + } + + @Override + public String toString() { + return getFormatted().getString(); + } + + @Override + public int hashCode() { + throw new NotImplementedException("Implement hashCode() in subclasses"); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/ComposedLine.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/ComposedLine.java new file mode 100644 index 00000000000..43153a27c78 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/ComposedLine.java @@ -0,0 +1,109 @@ +package com.gregtechceu.gtceu.api.mui.drawable.text; + +import com.gregtechceu.gtceu.api.mui.base.IThemeApi; +import com.gregtechceu.gtceu.api.mui.base.drawable.IHoverable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IIcon; +import com.gregtechceu.gtceu.api.mui.base.drawable.ITextLine; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.client.gui.Font; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.util.FormattedCharSequence; + +import lombok.Getter; + +import java.util.List; + +public class ComposedLine implements ITextLine { + + private final List elements; + @Getter + private final int width; + private final int height; + + private float lastX, lastY; + + public ComposedLine(List elements, int width, int height) { + this.elements = elements; + this.width = width; + this.height = height; + } + + @Override + public int getHeight(Font font) { + return height == font.lineHeight ? height : height + 1; + } + + @Override + public void draw(GuiContext context, Font font, float x, float y, int color, boolean shadow, + int availableWidth, int availableHeight) { + this.lastX = x; + this.lastY = y; + for (Object o : this.elements) { + if (o instanceof String s) { + float drawY = getHeight(font) / 2f - font.lineHeight / 2f; + context.getGraphics().drawString(font, s, x, y + drawY, color, shadow); + x += font.width(s); + } else if (o instanceof FormattedText text) { + float drawY = getHeight(font) / 2f - font.lineHeight / 2f; + FormattedCharSequence charSequence = text instanceof Component component ? + component.getVisualOrderText() : Language.getInstance().getVisualOrder(text); + context.getGraphics().drawString(font, charSequence, x, y + drawY, color, shadow); + x += font.width(text); + } else if (o instanceof FormattedCharSequence s) { + float drawY = getHeight(font) / 2f - font.lineHeight / 2f; + context.getGraphics().drawString(font, s, x, y + drawY, color, shadow); + x += font.width(s); + } else if (o instanceof IIcon icon) { + float drawY = getHeight(font) / 2f - icon.getHeight() / 2f; + int w = icon.getWidth() > 0 ? icon.getWidth() : availableWidth; + icon.draw(context, (int) x, (int) (y + drawY), w, icon.getHeight(), + IThemeApi.get().getDefaultTheme().getFallback().getTheme()); + if (icon instanceof IHoverable hoverable) { + hoverable.setRenderedAt((int) x, (int) (y + drawY)); + } + x += w; + } + } + } + + @Override + public Object getHoveringElement(Font font, int x, int y) { + int h0 = getHeight(font); + if (y < lastY || y > lastY + h0) return null; // is not hovering vertically + if (x < lastX || x > lastX + getWidth()) return Boolean.FALSE; // is not hovering horizontally + float x0 = x - this.lastX; // origin to 0 + float x1 = 0; + float y0 = y - this.lastY; // origin to 0 + for (Object o : this.elements) { + float w, h; + if (o instanceof String s) { + w = font.width(s); + h = font.lineHeight; + } else if (o instanceof Component c) { + w = font.width(c); + h = font.lineHeight; + } else if (o instanceof FormattedCharSequence s) { + w = font.width(s); + h = font.lineHeight; + } else if (o instanceof IIcon icon) { + w = icon.getWidth(); + h = icon.getWidth(); + } else continue; + if (x0 > x1 && x0 < x1 + w) { + // is inside horizontally + if (h < h0) { + // is smaller than line height + int lower = (int) (h0 / 2f - h / 2); + int upper = (int) (h0 / 2f + h / 2 - 1f); + if (y0 < lower || y0 > upper) return Boolean.FALSE; // is outside vertically + } + return o; // found hovering + } + x1 += w; // move to next element + } + return null; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/CompoundKey.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/CompoundKey.java new file mode 100644 index 00000000000..3b023a7f02c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/CompoundKey.java @@ -0,0 +1,45 @@ +package com.gregtechceu.gtceu.api.mui.drawable.text; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; + +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; + +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +public class CompoundKey extends BaseKey { + + private static final IKey[] EMPTY = new IKey[0]; + + @Getter + private final IKey[] keys; + + public CompoundKey(IKey... keys) { + this.keys = keys == null || keys.length == 0 ? EMPTY : keys; + } + + @Override + public MutableComponent get() { + return toComponent(false, null); + } + + @Override + public MutableComponent getFormatted(@Nullable FormattingState parentFormatting) { + // formatting is prepended to each key + return toComponent(true, parentFormatting); + } + + private MutableComponent toComponent(boolean formatted, @Nullable FormattingState parentFormatting) { + MutableComponent builder = Component.empty(); + for (IKey key : this.keys) { + if (formatted) { + // merge parent formatting and this formatting to not lose info + builder.append(key.getFormatted(FormattingState.merge(parentFormatting, getFormatting()))); + } else { + builder.append(key.get()); + } + } + return builder; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/DynamicKey.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/DynamicKey.java new file mode 100644 index 00000000000..abb02fb4648 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/DynamicKey.java @@ -0,0 +1,41 @@ +package com.gregtechceu.gtceu.api.mui.drawable.text; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; + +import net.minecraft.network.chat.MutableComponent; + +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +public class DynamicKey extends BaseKey { + + private final Supplier supplier; + + public DynamicKey(Supplier supplier) { + // Objects.requireNonNull(supplier.get(), "IKey returns a null string!"); + this.supplier = supplier; + } + + @Override + public MutableComponent get() { + return toString(false, null); + } + + @Override + public MutableComponent getFormatted(@Nullable FormattingState parentFormatting) { + // formatting is prepended to each key + return toString(true, parentFormatting); + } + + private MutableComponent toString(boolean formatted, @Nullable FormattingState parentFormatting) { + IKey key = this.supplier.get(); + if (key == null) key = IKey.EMPTY; + if (formatted) { + // merge parent formatting and this formatting to no lose info + return key.getFormatted(FormattingState.merge(parentFormatting, getFormatting())); + } else { + return key.get(); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/FontRenderHelper.java b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/FontRenderHelper.java new file mode 100644 index 00000000000..07ad8d8e084 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/drawable/text/FontRenderHelper.java @@ -0,0 +1,238 @@ +package com.gregtechceu.gtceu.api.mui.drawable.text; + +import com.gregtechceu.gtceu.api.mui.base.MCHelper; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.Font; +import net.minecraft.network.chat.*; +import net.minecraft.util.FormattedCharSequence; + +import org.apache.commons.lang3.mutable.MutableBoolean; +import org.apache.commons.lang3.mutable.MutableFloat; +import org.apache.commons.lang3.mutable.MutableInt; +import org.apache.commons.lang3.mutable.MutableObject; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +public class FontRenderHelper { + + private static final int min = '0', max = 'r'; // min = 48, max = 114 + // array to access text formatting by character fast + private static final ChatFormatting[] formattingMap = new ChatFormatting[max - min + 1]; + + static { + for (ChatFormatting formatting : ChatFormatting.values()) { + char c = formatting.getChar(); + formattingMap[c - min] = formatting; + if (Character.isLetter(c)) { + formattingMap[Character.toUpperCase(c) - min] = formatting; + } + } + } + + /** + * Returns the formatting for a character with a fast array lookup. + * + * @param c formatting character + * @return formatting for character or null + */ + @Nullable + public static ChatFormatting getForCharacter(char c) { + if (c < min || c > max) return null; + return formattingMap[c - min]; + } + + public static void addAfter(ChatFormatting[] state, ChatFormatting formatting, boolean removeAllOnReset) { + if (formatting == ChatFormatting.RESET) { + if (removeAllOnReset) Arrays.fill(state, null); + state[0] = formatting; + return; + } + // remove reset + if (removeAllOnReset) state[6] = null; + if (formatting.isFormat()) { + state[formatting.ordinal() - 15] = formatting; + return; + } + // color + state[0] = formatting; + } + + public static MutableComponent format(@Nullable FormattingState state, @Nullable FormattingState parentState, + Component text) { + if (state == null) { + if (parentState == null) return text.copy(); + return parentState.prependText(ChatFormatting.RESET, null).append(text); + } + return state.prependText(ChatFormatting.RESET, parentState).append(text); + } + + public static MutableComponent formatArgs(Object[] args, @Nullable FormattingState parentState, String text, + boolean translate) { + if (args == null || args.length == 0) return translate ? Component.translatable(text) : Component.literal(text); + args = Arrays.copyOf(args, args.length); + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof IKey key) { + // parent format + key format + key text + parent format + args[i] = FormattingState.appendFormat(key.getFormatted(parentState) + .withStyle(ChatFormatting.RESET), parentState); + } + } + return translate ? Component.translatable(text, args) : Component.literal(String.format(text, args)); + } + + public static int getDefaultTextHeight() { + Font fr = MCHelper.getFont(); + return fr != null ? fr.lineHeight : 9; + } + + /** + * Calculates how many formatting characters there are at the given position of the string. + * + * @param s string + * @param start starting index + * @return amount of formatting characters at index + */ + public static int getFormatLength(String s, int start) { + int i = Math.max(0, start); + int l = 0; + for (; i < s.length(); i++) { + char c = s.charAt(i); + if (c == 167) { + if (i + 1 >= s.length()) return l; + if (getForCharacter(c) == null) return l; + l += 2; + i++; + } else { + return l; + } + } + return l; + } + + public static FormattedCharSequence splitAtMax(FormattedCharSequence input, float maxWidth) { + MutableFloat cur = new MutableFloat(); + // split the string at max width. + List output = new ArrayList<>(); + input.accept((pos, style, codePoint) -> { + if (cur.addAndGet(TextRenderer.getWidthProvider().getWidth(codePoint, style)) > maxWidth) { + return false; + } + output.add(new TextRenderer.FormattedChar(codePoint, style)); + return true; + }); + return fromChars(output); + } + + public static boolean isEmpty(FormattedCharSequence input) { + if (input == FormattedCharSequence.EMPTY) { + return true; + } + MutableBoolean value = new MutableBoolean(true); + input.accept((pos, style, codePoint) -> { + value.setFalse(); + return false; + }); + return value.isTrue(); + } + + public static Component getComponentFromCharSequence(FormattedCharSequence input) { + List parts = new ArrayList<>(); + StringBuilder value = new StringBuilder(); + MutableObject