Write ingredients as plain itemstacks if networking

This commit is contained in:
DBotThePony 2023-08-19 00:56:03 +07:00
parent a0b0580bfa
commit 260be58951
Signed by: DBot
GPG Key ID: DCC23B5715498507
5 changed files with 81 additions and 78 deletions

View File

@ -11,6 +11,8 @@ import com.mojang.serialization.JsonOps
import net.minecraft.data.recipes.FinishedRecipe import net.minecraft.data.recipes.FinishedRecipe
import net.minecraft.network.FriendlyByteBuf import net.minecraft.network.FriendlyByteBuf
import net.minecraft.resources.ResourceLocation import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.crafting.Ingredient
import net.minecraft.world.item.crafting.Recipe import net.minecraft.world.item.crafting.Recipe
import net.minecraft.world.item.crafting.RecipeSerializer import net.minecraft.world.item.crafting.RecipeSerializer
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
@ -20,51 +22,26 @@ import ru.dbotthepony.mc.otm.core.util.writeBinaryJsonWithCodec
import kotlin.collections.ArrayDeque import kotlin.collections.ArrayDeque
import kotlin.concurrent.getOrSet import kotlin.concurrent.getOrSet
class Codec2RecipeSerializer<S : Recipe<*>> private constructor( class Codec2RecipeSerializer<S : Recipe<*>>(
val empty: S?, val empty: S?,
/**
* [ThreadLocal] because optimization mods can (and probably should) parallelize recipe deserialization,
* since RecipeSerializers are expected to be stateless. [Codec2RecipeSerializer], however, is stateful (threading PoV).
* To make it stateless, [ThreadLocal] is used.
*/
private val idStack: ThreadLocal<ArrayDeque<ResourceLocation>>,
codec: (Codec2RecipeSerializer<S>.Context) -> Codec<S>, codec: (Codec2RecipeSerializer<S>.Context) -> Codec<S>,
) : Codec<S>, RecipeSerializer<S> { ) : Codec<S>, RecipeSerializer<S> {
constructor(empty: S?, codec: (Codec2RecipeSerializer<S>.Context) -> Codec<S>) : this(empty, ThreadLocal(), codec)
constructor(supplier: (Codec2RecipeSerializer<S>.Context) -> Codec<S>) : this(null, supplier) constructor(supplier: (Codec2RecipeSerializer<S>.Context) -> Codec<S>) : this(null, supplier)
private val codec = codec.invoke(Context()) private class CurrentContext {
private val id: ArrayDeque<ResourceLocation> val idStack = ArrayDeque<ResourceLocation>()
get() = idStack.getOrSet { ArrayDeque() } var isNetwork = 0
inner class Context() {
val id: ResourceLocation
get() = checkNotNull(this@Codec2RecipeSerializer.id.lastOrNull()) { "Not currently deserializing recipe" }
fun <O : Recipe<*>> wrap(other: Codec2RecipeSerializer<O>): Codec<O> {
return object : Codec<O> {
override fun <T : Any> encode(input: O, ops: DynamicOps<T>, prefix: T): DataResult<T> {
try {
other.id.addLast(this@Context.id)
return other.encode(input, ops, prefix)
} finally {
other.id.removeLast()
}
}
override fun <T : Any> decode(ops: DynamicOps<T>, input: T): DataResult<Pair<O, T>> {
try {
other.id.addLast(this@Context.id)
return other.decode(ops, input)
} finally {
other.id.removeLast()
}
}
}
}
} }
inner class Context {
val id: ResourceLocation
get() = checkNotNull(context.idStack.lastOrNull()) { "Not currently deserializing recipe" }
val ingredients: Codec<Ingredient> get() = ActualIngredientCodec
}
private val codec = codec.invoke(Context())
override fun <T : Any> encode(input: S, ops: DynamicOps<T>, prefix: T): DataResult<T> { override fun <T : Any> encode(input: S, ops: DynamicOps<T>, prefix: T): DataResult<T> {
return codec.encode(input, ops, prefix) return codec.encode(input, ops, prefix)
} }
@ -74,41 +51,47 @@ class Codec2RecipeSerializer<S : Recipe<*>> private constructor(
} }
fun <O : Recipe<*>> xmap(to: (S) -> O, from: (O) -> S): Codec2RecipeSerializer<O> { fun <O : Recipe<*>> xmap(to: (S) -> O, from: (O) -> S): Codec2RecipeSerializer<O> {
return Codec2RecipeSerializer(empty?.let(to), idStack) { _ -> return Codec2RecipeSerializer(empty?.let(to)) { _ ->
codec.xmap(to, from) codec.xmap(to, from)
} }
} }
override fun fromJson(id: ResourceLocation, data: JsonObject): S { override fun fromJson(id: ResourceLocation, data: JsonObject): S {
try { try {
this.id.addLast(id) context.idStack.addLast(id)
return decode(JsonOps.INSTANCE, data).get().map( return decode(JsonOps.INSTANCE, data).get().map(
{ { it.first },
it.first { empty ?: throw JsonSyntaxException("Failed to deserialize recipe from JSON: ${it.message()}") }
},
{
empty ?: throw JsonSyntaxException("Failed to deserialize recipe from JSON: ${it.message()}")
}
) )
} finally { } finally {
this.id.removeLast() context.idStack.removeLast()
} }
} }
override fun fromNetwork(id: ResourceLocation, data: FriendlyByteBuf): S? { override fun fromNetwork(id: ResourceLocation, data: FriendlyByteBuf): S? {
try { try {
this.id.addLast(id) context.idStack.addLast(id)
context.isNetwork++
return data.readBinaryJsonWithCodecIndirect(this) return data.readBinaryJsonWithCodecIndirect(this)
.resultOrPartial { LOGGER.error("Failed to read recipe $id from network: $it") }.orElse(null) .resultOrPartial { LOGGER.error("Failed to read recipe $id from network: $it") }.orElse(null)
} finally { } finally {
this.id.removeLast() context.isNetwork--
context.idStack.removeLast()
} }
} }
override fun toNetwork(data: FriendlyByteBuf, recipe: S) { override fun toNetwork(data: FriendlyByteBuf, recipe: S) {
data.writeBinaryJsonWithCodec(this, recipe) try {
context.idStack.addLast(recipe.id)
context.isNetwork++
data.writeBinaryJsonWithCodec(this, recipe)
} finally {
context.isNetwork--
context.idStack.removeLast()
}
} }
fun toFinished(recipe: S): FinishedRecipe { fun toFinished(recipe: S): FinishedRecipe {
@ -146,7 +129,35 @@ class Codec2RecipeSerializer<S : Recipe<*>> private constructor(
} }
} }
private object ActualIngredientCodec : Codec<Ingredient> {
override fun <T : Any> encode(input: Ingredient, ops: DynamicOps<T>, prefix: T): DataResult<T> {
return if (context.isNetwork > 0) {
networkIngredientCodec.encode(input, ops, prefix)
} else {
IngredientCodec.encode(input, ops, prefix)
}
}
override fun <T : Any> decode(ops: DynamicOps<T>, input: T): DataResult<Pair<Ingredient, T>> {
return if (context.isNetwork > 0) {
networkIngredientCodec.decode(ops, input)
} else {
IngredientCodec.decode(ops, input)
}
}
}
companion object { companion object {
private val LOGGER = LogManager.getLogger() private val LOGGER = LogManager.getLogger()
private val networkIngredientCodec = Codec.list(ItemStack.CODEC).xmap({ Ingredient.of(it.stream()) }, { it.items.toMutableList() })
/**
* [ThreadLocal] because optimization mods can (and probably should) parallelize recipe deserialization,
* since RecipeSerializers are expected to be stateless. [Codec2RecipeSerializer], however, is stateful (threading PoV).
* To make it stateless, [ThreadLocal] is used.
*/
private val contextHolder = ThreadLocal<CurrentContext>()
private val context: CurrentContext
get() = contextHolder.getOrSet { CurrentContext() }
} }
} }

View File

@ -9,24 +9,26 @@ import com.mojang.serialization.codecs.RecordCodecBuilder
import net.minecraft.world.item.crafting.Ingredient import net.minecraft.world.item.crafting.Ingredient
import ru.dbotthepony.mc.otm.core.collect.allEqual import ru.dbotthepony.mc.otm.core.collect.allEqual
import ru.dbotthepony.mc.otm.core.collect.map import ru.dbotthepony.mc.otm.core.collect.map
import ru.dbotthepony.mc.otm.core.collect.toList
import ru.dbotthepony.mc.otm.core.collect.toStream import ru.dbotthepony.mc.otm.core.collect.toStream
import ru.dbotthepony.mc.otm.core.stream import ru.dbotthepony.mc.otm.core.stream
import ru.dbotthepony.mc.otm.recipe.IIngredientMatrix import ru.dbotthepony.mc.otm.recipe.IIngredientMatrix
import ru.dbotthepony.mc.otm.recipe.IngredientMatrix import ru.dbotthepony.mc.otm.recipe.IngredientMatrix
import java.util.function.Supplier import java.util.function.Supplier
object IngredientMatrixCodec : Codec<IIngredientMatrix> { class IngredientMatrixCodec(ingredientCodec: Codec<Ingredient>) : Codec<IIngredientMatrix> {
private val ingredientList = Codec.list(IngredientCodec) private val ingredientList = Codec.list(ingredientCodec)
private val doubleIngredientList = Codec.list(ingredientList)
override fun <T : Any> encode(input: IIngredientMatrix, ops: DynamicOps<T>, prefix: T): DataResult<T> { override fun <T : Any> encode(input: IIngredientMatrix, ops: DynamicOps<T>, prefix: T): DataResult<T> {
return DataResult.success( return doubleIngredientList.encode(
ops.createList( (0 until input.height).iterator().map { row ->
(0 until input.height).stream().map { row -> (0 until input.width).iterator().map { column ->
ops.createList((0 until input.width).stream().map { column -> input[column, row]
JsonOps.INSTANCE.convertTo(ops, input[column, row].toJson()) }.toList()
}) }.toList(),
} ops,
) prefix
) )
} }
@ -45,7 +47,7 @@ object IngredientMatrixCodec : Codec<IIngredientMatrix> {
.flatXmap( .flatXmap(
{ if (it.length == 1) DataResult.success(it[0]) else DataResult.error { "Ingredient key must be exactly 1 symbol in length, '$it' is invalid" } }, { if (it.length == 1) DataResult.success(it[0]) else DataResult.error { "Ingredient key must be exactly 1 symbol in length, '$it' is invalid" } },
{ DataResult.success(it.toString()) } { DataResult.success(it.toString()) }
), IngredientCodec).fieldOf("key").forGetter(Handwritten::key) ), ingredientCodec).fieldOf("key").forGetter(Handwritten::key)
).apply(it, ::Handwritten) ).apply(it, ::Handwritten)
} }

View File

@ -143,18 +143,10 @@ open class MatterEntanglerRecipe(
} }
companion object { companion object {
val SERIALIZER = Codec2RecipeSerializer<MatterEntanglerRecipe>( val SERIALIZER = Codec2RecipeSerializer<MatterEntanglerRecipe> { context ->
MatterEntanglerRecipe(
ResourceLocation(OverdriveThatMatters.MOD_ID, "null"),
IIngredientMatrix.Companion,
Decimal.ZERO,
0.0,
ItemStack.EMPTY,
)
) { context ->
RecordCodecBuilder.create { RecordCodecBuilder.create {
it.group( it.group(
IngredientMatrixCodec.fieldOf("ingredients").forGetter(MatterEntanglerRecipe::ingredients), IngredientMatrixCodec(context.ingredients).fieldOf("ingredients").forGetter(MatterEntanglerRecipe::ingredients),
DecimalCodec.minRange(Decimal.ZERO).fieldOf("matter").forGetter(MatterEntanglerRecipe::matter), DecimalCodec.minRange(Decimal.ZERO).fieldOf("matter").forGetter(MatterEntanglerRecipe::matter),
Codec.DOUBLE.minRange(0.0).fieldOf("ticks").forGetter(MatterEntanglerRecipe::ticks), Codec.DOUBLE.minRange(0.0).fieldOf("ticks").forGetter(MatterEntanglerRecipe::ticks),
ItemStack.CODEC.fieldOf("result").forGetter(MatterEntanglerRecipe::result), ItemStack.CODEC.fieldOf("result").forGetter(MatterEntanglerRecipe::result),

View File

@ -113,12 +113,10 @@ class PainterRecipe(
} }
companion object { companion object {
val SERIALIZER = Codec2RecipeSerializer<PainterRecipe>( val SERIALIZER = Codec2RecipeSerializer<PainterRecipe> { context ->
PainterRecipe(ResourceLocation(OverdriveThatMatters.MOD_ID, "empty"), Ingredient.EMPTY, ItemStack.EMPTY, setOf())
) { context ->
RecordCodecBuilder.create { RecordCodecBuilder.create {
it.group( it.group(
IngredientCodec.fieldOf("input").forGetter(PainterRecipe::input), context.ingredients.fieldOf("input").forGetter(PainterRecipe::input),
ItemStack.CODEC.fieldOf("output").forGetter(PainterRecipe::output), ItemStack.CODEC.fieldOf("output").forGetter(PainterRecipe::output),
PredicatedCodecList<Map<DyeColor, Int>>( PredicatedCodecList<Map<DyeColor, Int>>(
DyeColor.CODEC.xmap({ mapOf(it to 1) }, { it.keys.first() }) to Predicate { it.keys.size == 1 && it.values.first() == 1 }, DyeColor.CODEC.xmap({ mapOf(it to 1) }, { it.keys.first() }) to Predicate { it.keys.size == 1 && it.values.first() == 1 },

View File

@ -78,11 +78,11 @@ class PlatePressRecipe(
fun toFinished() = SERIALIZER.toFinished(this) fun toFinished() = SERIALIZER.toFinished(this)
companion object { companion object {
val SERIALIZER = Codec2RecipeSerializer<PlatePressRecipe>(PlatePressRecipe(ResourceLocation(OverdriveThatMatters.MOD_ID, "empty"), Ingredient.EMPTY, Ingredient.EMPTY, 1)) { context -> val SERIALIZER = Codec2RecipeSerializer<PlatePressRecipe> { context ->
RecordCodecBuilder.create { RecordCodecBuilder.create {
it.group( it.group(
IngredientCodec.fieldOf("input").forGetter(PlatePressRecipe::input), context.ingredients.fieldOf("input").forGetter(PlatePressRecipe::input),
IngredientCodec.fieldOf("output").forGetter(PlatePressRecipe::output), context.ingredients.fieldOf("output").forGetter(PlatePressRecipe::output),
Codec.INT.minRange(1).optionalFieldOf("count", 1).forGetter(PlatePressRecipe::count), Codec.INT.minRange(1).optionalFieldOf("count", 1).forGetter(PlatePressRecipe::count),
Codec.INT.minRange(0).optionalFieldOf("workTime", 200).forGetter(PlatePressRecipe::workTime), Codec.INT.minRange(0).optionalFieldOf("workTime", 200).forGetter(PlatePressRecipe::workTime),
FloatProvider.CODEC.optionalFieldOf("experience", ConstantFloat.ZERO).forGetter(PlatePressRecipe::experience) FloatProvider.CODEC.optionalFieldOf("experience", ConstantFloat.ZERO).forGetter(PlatePressRecipe::experience)