Skip to content

Commit 3484551

Browse files
screretjurrejelle
andauthored
Clean up adjacency condition serialization (#4260)
Co-authored-by: Jurre Groenendijk <jurre@jilles.com>
1 parent 3dca9ef commit 3484551

32 files changed

Lines changed: 477 additions & 516 deletions

docs/content/Modpacks/Changes/v7.5.0.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,22 @@ title: "Version 7.5.0"
44

55

66
# Updating from `7.4.1` to `7.5.0`
7+
78
## MachineBuilder Generics
89
We have added a second Generic argument to our (Multiblock)MachineBuilder. This effectively means that anywhere where you used to store a partially finished `MachineBuilder<?>`, you now need to store a `MachineBuilder<?, ?>`. The same holds for `MultiblockMachineBuilder<?,?>`.
10+
11+
## RecipeCondition Generics
12+
We have added a Generic argument to `RecipeCondition` describing the condition.
13+
This means that your custom recipe conditions now need to extend `RecipeCondition<MyCondition>` (whereas before they just extended `RecipeCondition`).
14+
For example, if you have a custom recipe condition class like so: `public class ExampleCondition extends RecipeCondition`, it should now be `public class ExampleCondition extends RecipeCondition<ExampleCondition>`.
15+
You also need to adjust the generics of `getType()` and `createTemplate()` to match this change, like so:
16+
```patch
17+
- public RecipeConditionType<?> getType() {
18+
+ public RecipeConditionType<ExampleCondition> getType() {
19+
20+
- public RecipeCondition createTemplate() {
21+
+ public ExampleCondition createTemplate() {
22+
```
23+
924
## Machine & Cover Copy/Paste System
1025
A new system for copying machines using the Machine Memory Card has been added, see [this page](../Other-Topics/Cover-Machine-Copy-Paste-Support.md) if you want to add extra copy/paste behaviour to your own machines and covers.

docs/content/Modpacks/Examples/Custom-Recipe-Condition.md

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,50 @@ They are registered using
1515
@Mod(ExampleMod.MOD_ID)
1616
public class ExampleMod {
1717

18-
public ExampleMod(FMLJavaModLoadingContext context) {
19-
var bus = context.getModEventBus();
20-
bus.addGenericListener(RecipeConditionType.class, this::registerConditions);
21-
}
22-
18+
// in 1.20.1
2319
public static RecipeConditionType<ExampleCondition> EXAMPLE_CONDITION;
20+
21+
public ExampleMod() {
22+
IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus();
23+
modBus.addGenericListener(RecipeConditionType.class, this::registerConditions);
24+
}
25+
26+
public void registerConditions(GTCEuAPI.RegisterEvent<String, RecipeConditionType<?>> event) {
27+
EXAMPLE_CONDITION = GTRegistries.RECIPE_CONDITIONS.register("example_condition", // (1)
28+
new RecipeConditionType<>(ExampleCondition::new, ExampleCondition.CODEC));
29+
}
30+
// end 1.20.1
31+
32+
// in 1.21.1
33+
public static final RecipeConditionType<ExampleCondition> EXAMPLE_CONDITION = GTRegistries.register(GTRegistries.RECIPE_CONDITIONS,
34+
ResourceLocation.fromNamespaceAndPath(ExampleMod.MOD_ID, "example_condition"), // (2)
35+
new RecipeConditionType<>(ExampleCondition::new, ExampleCondition.CODEC));
36+
37+
public ExampleMod(IEventBus modBus, FMLModContainer container) {
38+
modBus.addListener(CommonInit::onRegister);
39+
bus.addListener(RecipeConditionType.class, this::registerConditions);
40+
}
2441

2542
public void registerConditions(GTCEuAPI.RegisterEvent<String, RecipeConditionType<?>> event) {
2643
EXAMPLE_CONDITION = GTRegistries.RECIPE_CONDITIONS.register("example_condition",
27-
new RecipeConditionType<>(
28-
ExampleCondition::new,
29-
ExampleCondition.CODEC
30-
)
31-
);
44+
new RecipeConditionType<>(ExampleCondition::new, ExampleCondition.CODEC));
3245
}
46+
// end 1.21.1
3347
}
3448
```
3549

50+
1. The 1.20.1 version doesn't require a namespace, so make sure you don't use the same ID as someone else!
51+
2. You may use a helper method akin to `GTCEu.id` for creating the ResourceLocation, but you **must** use your own namespace for it.
52+
3653
We will set up a condition that requires that the power buffer of the machine is above a certain Y level.
3754
```java
38-
public class ExampleCondition extends RecipeCondition {
55+
public class ExampleCondition extends RecipeCondition<ExampleCondition> {
3956

40-
public int height;
41-
42-
public static final Codec<ExampleCondition> CODEC = RecordCodecBuilder
43-
.create(instance -> RecipeCondition.isReverse(instance)
44-
.and(Codec.INT.fieldOf("height").forGetter(val -> val.height))
45-
.apply(instance, ExampleCondition::new));
57+
public static final Codec<ExampleCondition> CODEC = RecordCodecBuilder.create(instance -> RecipeCondition.isReverse(instance)
58+
.and(Codec.INT.fieldOf("height").forGetter(val -> val.height)
59+
).apply(instance, ExampleCondition::new));
4660

61+
public int height;
4762

4863
public ExampleCondition(boolean isReverse, int height) {
4964
this.isReverse = isReverse;
@@ -52,14 +67,14 @@ public class ExampleCondition extends RecipeCondition {
5267

5368
public ExampleCondition(int height) {
5469
this(false, height);
55-
}
70+
}
5671

5772
public ExampleCondition() {
5873
this(false, 0);
5974
}
6075

6176
@Override
62-
public RecipeConditionType<?> getType() {
77+
public RecipeConditionType<ExampleCondition> getType() {
6378
return ExampleMod.EXAMPLE_CONDITION;
6479
}
6580

@@ -74,7 +89,7 @@ public class ExampleCondition extends RecipeCondition {
7489
}
7590

7691
@Override
77-
public RecipeCondition createTemplate() {
92+
public ExampleCondition createTemplate() {
7893
return new ExampleCondition(0);
7994
}
8095
}
@@ -85,7 +100,7 @@ Lets step through this example. This will not be in order as it is in the file,
85100
Starting with:
86101
```java
87102
@Override
88-
public RecipeConditionType<?> getType() {
103+
public RecipeConditionType<ExampleCondition> getType() {
89104
return ExampleMod.EXAMPLE_CONDITION;
90105
}
91106

@@ -105,16 +120,12 @@ This part is quite simple, and just returns the type and tooltip for the conditi
105120
public ExampleCondition(int height) {
106121
this(false, height);
107122
}
108-
109-
public ExampleCondition() {
110-
this(false, 0);
111-
}
112123
```
113-
These are the constructors. We need the `isReverse`, as it is part of the overarching `RecipeCondition` type. `isReverse` means that if the condition is met, your recipe won't be run. Furthermore, a no-arg constructor is required for (de)serialization.
124+
These are the constructors. We need the `isReverse`, as it is part of the overarching `RecipeCondition` type. `isReverse` means that if the condition is met, your recipe won't be run. Furthermore, a constructor with all arguments is required for (de)serialization.
114125

115126
```java
116127
@Override
117-
public RecipeCondition createTemplate() {
128+
public ExampleCondition createTemplate() {
118129
return new ExampleCondition(0);
119130
}
120131
```
@@ -131,21 +142,20 @@ This creates the basic "template" that might be used for serialization. This sho
131142
This is the actual condition.
132143

133144
```java
145+
public static final Codec<ExampleCondition> CODEC = RecordCodecBuilder.create(instance -> RecipeCondition.isReverse(instance).and(
146+
Codec.INT.fieldOf("height").forGetter(val -> val.height)
147+
).apply(instance, ExampleCondition::new));
148+
134149
public int height;
135-
136-
public static final Codec<ExampleCondition> CODEC = RecordCodecBuilder
137-
.create(instance -> RecipeCondition.isReverse(instance)
138-
.and(Codec.INT.fieldOf("height").forGetter(val -> val.height))
139-
.apply(instance, ExampleCondition::new));
140150
```
141151

142152
The CODEC is how java knows how to serialize/deserialize your condition. This is needed for syncing between client/server, and storing it to json to load when the world loads.
143153
It consists of a few parts:
144154

145155
- `RecordCodecBuilder.create(instance -> ` means we will start a RecordCodecBuilder, or a builder that only consists of simple types.
146156
- `RecipeCondition.isReverse(instance)` is a helper codec that serializes the isReverse boolean of your codec.
147-
- `.and(` means this is the next field in the record.
148-
- `Codec.INT.fieldOf("height").forGetter(val -> val.height)` means we want to serialize an INT, we want to call it "height" in the json, and to get the value you call `.height`.
157+
- `.and(` allows adding additional fields to the codec.
158+
- `Codec.INT.fieldOf("height").forGetter(val -> val.height)` means we want to serialize an integer, we want to call it "height" in the JSON, and to get the value to serialize you use `ExampleCondition#height`.
149159
- `.apply(instance, ExampleCondition::new)` means when deserializing back to an object, you apply these steps to get the values (in this case `bool isReverse, int height`) and call the constructor with those arguments.
150160
In this case, this would call our `new ExampleCondition(isReverse, height)` constructor we have defined earlier.
151161

docs/content/Modpacks/Recipes/Recipe-Conditions.md

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,62 @@ title: Recipe Conditions
44

55
Recipe Conditions are recipe properties that can prevent a recipe from starting based on certain criteria, like for example Biome, Weather, Quest Completions, or self-made custom Conditions.
66

7-
These conditions can be used in both java and kubejs recipes. However, custom conditons can only be done in java. If you want to see how to make these, check out the [Custom Recipe Condition](../Examples/Custom-Recipe-Condition.md) example page.
7+
These conditions can be used in both Java and KubeJS recipes. However, custom conditions can only be done in Java addons. If you want to see how to make these, check out the [Custom Recipe Condition](../Examples/Custom-Recipe-Condition.md) example page.
88

99
!!! Note
1010
The condition is run after recipe matching and before recipe execution. If the recipe condition doesn't match, the machine will be suspended and won't be updated again until something in the inputs/outputs changes.
1111

1212
### Base Conditons
1313

1414
- Biome: `.biome("namespace:biome_id")`
15-
- Locks a recipe behind being inside a certain biome, works with any biome a pack has loaded. For example, you could use `minecraft:plains`.
15+
- Locks a recipe behind being inside a certain biome, works with any biome a pack has loaded.
16+
For example, you could do `.biome("minecraft:plains")`.
1617
- Dimension: `.dimension("namespace:dimension_id")`
17-
- Locks a recipe being behind a certain dimension, the gas collector is a good example of this. For example, you could use `minecraft:the_end`
18-
- Position_Y: `.posY(int min, int max)`
19-
- Locks a recipe behind a certain y level in-world. For example, you could use `.posY(120, 130)` to have a recipe require a machine to be in between y 120 and y 130.
18+
- Locks a recipe being behind a certain dimension, the gas collector is a good example of this.
19+
- For example, you could do `.dimension("minecraft:the_end")`
20+
- Y Position: `.posY(int min, int max)`
21+
- Locks a recipe behind a certain y level in-world.
22+
- For example, you could use `.posY(120, 130)` to have a recipe require a machine to be in between y 120 and y 130.
2023
- Rain: `.rain(float level)`
21-
- Locks a recipe behind a certain level of rain. For example, you could use `.rain(1.0)` to make a recipe need full rain.
22-
- Adjacent_Fluids: `adjacentFluids("minecraft:water","minecraft:lava")`
23-
- You can pass through any amount of fluids into the array. Moreover, any fluid passed into the array will make the recipe require a full source block touching the machine. We also have `adjacentFluidTag("forge:water", "forge:lava")`.
24-
- Adjacent_Blocks: `adjacentBlocks("minecraft:stone", "minecraft:iron_block")`
25-
- Much like the fluid condition, you can pass blocks into the array that lock the recipe behind needing the machine to touch these blocks. We also have `adjacentBlockTag("forge:stone", "forge:storage_blocks/iron")`.
24+
- Locks a recipe behind a certain level of rain.
25+
- For example, you could use `.rain(1.0)` to make a recipe need full rain.
26+
- Adjacent Fluids: `.adjacentFluids("namespace:fluid_id", ...)`
27+
- You can pass any amount of fluids into the array. Moreover, any fluid passed into the array will make the recipe require a full source block touching the machine.
28+
- For example, you could use `.adjacentFluids("minecraft:water", "minecraft:lava")` to make a recipe require BOTH a water source and a lava source next to the machine.
29+
- We also have `.adjacentFluidTag("forge:water", "forge:lava")`, which does the same, but allows fluid _tags_ to be used.
30+
- Adjacent Blocks: `.adjacentBlocks("namespace:block_id", ...)`
31+
- Much like the fluid condition, you can pass blocks into the array that lock the recipe behind needing the machine to touch these blocks.
32+
- For example, you could use `.adjacentBlocks("minecraft:stone", "minecraft:iron_block")` to make a recipe require a Stone block and a Block of Iron.
33+
- We also have `.adjacentBlockTag("forge:stone", "forge:storage_blocks/iron")`, which does the same, but allows block _tags_ to be used.
2634
- Thunder: `.thunder(float level)`
27-
- Locks a recipe behind a certain level of rain. For example, you could use `.thunder(1.0)` to make a recipe need a strong thunderstorm.
28-
- Vent: This condition is auto added to any steam single block, it blocks recipes from running if the vent is obstructed.
35+
- Locks a recipe behind a certain level of rain.
36+
- For example, you could use `.thunder(1.0)` to make a recipe need a strong thunderstorm.
37+
- Vent: This condition is automatically added to any recipes ran in a single block steam machine. It blocks recipes from running if the machine's vent is obstructed.
2938
- Cleanroom: `.cleanroom(CleanroomType.CLEANROOM)`
30-
- Locks a recipe to being inside a cleanroom. You can also use STERILE_CLEANROOM as well as your own custom cleanroom type.
31-
- Fusion_Start_EU: `.fusionStartEU(long eu)`
32-
- Locks a recipe behind the amount of stored power in a fusion machine. To use this, the machine must use the FusionReactorMachine class. For example, you could use `.fusionStartEU(600000)`
33-
- Station_Research: `.stationResearch(b => b.researchStack("namespace:item_id").EUt(long eu).CWUt(int minCWUPerTick, int TotalCWU))`
34-
- Locks a recipe behind having a certain research stack. For this condition to be properly seen, you will either need a base machine recipe type with the research ui component, or make your own. For example, you could do `.stationResearch(b => b.researchStack("gtceu:lv_motor").EUt(131000).CWUt(24, 12000))` which would lock a recipe behind needing a data orb with the lv motor research. It will also generate you a research station recipe.
35-
- Scanner_Research: `.scannerResearch(b => b.researchStack("namespace:item_id").EUt(long eu))`
36-
- Much like station research, this condition locks a recipe behind needing a research stack. However, in this case it will default to a data stick. For example, you could do `.scannerResearch(b => b.researchStack("gtceu:lv_motor").EUt(8192))`, which would make the recipe need a data stick with the lv motor research, and generates a scanner recipe.
37-
- Enviromental_Hazard: `.environmentalHazard(GTMedicalConditions.CARBON_MONOXIDE_POISONING)`
38-
- Locks a recipe into needing a certain environmental hazard to run. For now, carbon monoxide is the only one. An example of a machine using this condition is the air scrubber.
39-
- Daytime: `.daytime(boolean notNight)`
40-
- Locks recipe behind whether it is day or night. For example, you could do `.daytime(true)`, to make the recipe need it to be daytime.
39+
- Locks a recipe to being inside a cleanroom. You can also use `STERILE_CLEANROOM` as well as your own custom cleanroom type(s).
40+
- Fusion Start EU: `.fusionStartEU(long eu)`
41+
- Locks a recipe behind the amount of stored power in a fusion machine. To use this, the machine must use the FusionReactorMachine class.
42+
- For example, you could use `.fusionStartEU(600000)`
43+
- Station Research: `.stationResearch(b => b.researchStack("namespace:item_id").EUt(long eu).CWUt(int minCWUPerTick, int TotalCWU))`
44+
- Locks a recipe behind having a certain research stack. For this condition to be properly seen, you will either need a base machine recipe type with the research ui component, or make your own.
45+
- For example, you could do `.stationResearch(b => b.researchStack("gtceu:lv_motor").EUt(131000).CWUt(24, 12000))` which would lock a recipe behind needing a data orb with the lv motor research. It will also generate you a research station recipe.
46+
- Scanner Research: `.scannerResearch(b => b.researchStack("namespace:item_id").EUt(long eu))`
47+
- Much like station research, this condition locks a recipe behind needing a research stack. However, in this case it will default to a data stick.
48+
- For example, you could do `.scannerResearch(b => b.researchStack("gtceu:lv_motor").EUt(8192))`, which would make the recipe need a data stick with the lv motor research, and generates a scanner recipe.
49+
- Environmental Hazard: `.environmentalHazard("medical_condition_name")`
50+
- Locks a recipe into needing a certain environmental hazard to run. For now, `"carbon_monoxide_poisoning"` is the only one that's added to the world (by default). An example of a machine using this condition is the air scrubber.
51+
- For example, you could do `.environmentalHazard("carcinogen")` (if you have something that creates radiation, as if you don't, the recipe would never run.)
52+
- Daytime: `.daytime(boolean isNight)`
53+
- Locks recipe behind whether it is day or night.
54+
- For example, you could do `.daytime(true)` to make the recipe require nighttime to run.
4155

4256
### Mod Dependent Conditions
43-
- Ftb_Quests: `.ftbQuest(quest_id)`
44-
- Locks a recipe behind the owner of a machine completing a ftb quest. An example can't be easily given since every quest book is different.
45-
- Gamestage: `.gameStage(gameStage_id)`
57+
- FTB Quests: `.ftbQuest("quest_id")`
58+
- Locks a recipe behind the owner of a machine completing a quest with FTB Quests.
59+
- An example can't be easily given since every quest book is different.
60+
- Game Stages: `.gameStage("gamestage_id")`
4661
- Locks a recipe behind a certain game stage.
47-
- Heracles_Quests: `.heraclesQuest(quest_id)`
48-
- Locks a recipe behind the owner of a machine completing a heracles quest. An example can't be easily given since every quest book is different.
62+
- Odyssey Quests (Heracles): `.heraclesQuest("quest_id")`
63+
- Locks a recipe behind the owner of a machine completing a quest with Heracles.
64+
- An example can't be easily given since every quest book is different.
4965

src/main/java/com/gregtechceu/gtceu/api/codec/GTCodecUtils.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package com.gregtechceu.gtceu.api.codec;
22

3+
import com.gregtechceu.gtceu.utils.memoization.GTMemoizer;
4+
35
import net.minecraft.util.ExtraCodecs;
46

57
import com.mojang.datafixers.util.Either;
8+
import com.mojang.datafixers.util.Pair;
69
import com.mojang.serialization.Codec;
710
import com.mojang.serialization.DataResult;
11+
import com.mojang.serialization.DynamicOps;
812

913
import java.util.function.Function;
14+
import java.util.function.Supplier;
15+
16+
public final class GTCodecUtils {
1017

11-
public class GTCodecUtils {
18+
private GTCodecUtils() {}
1219

1320
public static final Codec<Long> NON_NEGATIVE_LONG = longRangeWithMessage(0, Long.MAX_VALUE,
1421
(val) -> "Value must be non-negative: " + val);
@@ -32,4 +39,28 @@ public static Codec<Long> longRange(long min, long max) {
3239
public static <T> T unboxEither(Either<T, T> either) {
3340
return either.map(Function.identity(), Function.identity());
3441
}
42+
43+
public static <T> Codec<Supplier<T>> lazyParsingCodec(Codec<T> delegate) {
44+
return new LazyParsingCodec<>(delegate);
45+
}
46+
47+
private record LazyParsingCodec<A>(Codec<A> codec) implements Codec<Supplier<A>> {
48+
49+
@Override
50+
public <T> DataResult<Pair<Supplier<A>, T>> decode(DynamicOps<T> ops, T input) {
51+
return DataResult.success(Pair.of(GTMemoizer.memoize(() -> deferredDecode(ops, input)), input));
52+
}
53+
54+
@Override
55+
public <T> DataResult<T> encode(Supplier<A> input, DynamicOps<T> ops, T prefix) {
56+
return input.get() == null ? DataResult.success(prefix) : this.codec.encode(input.get(), ops, prefix);
57+
}
58+
59+
private <T> A deferredDecode(DynamicOps<T> ops, T input) {
60+
return this.codec.decode(ops, input).get()
61+
.map(Pair::getFirst, partial -> {
62+
throw new IllegalStateException("Unable to parse deferred value: " + partial.message());
63+
});
64+
}
65+
}
3566
}

0 commit comments

Comments
 (0)