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.network.FriendlyByteBuf
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.RecipeSerializer
import org.apache.logging.log4j.LogManager
@ -20,51 +22,26 @@ import ru.dbotthepony.mc.otm.core.util.writeBinaryJsonWithCodec
import kotlin.collections.ArrayDeque
import kotlin.concurrent.getOrSet
class Codec2RecipeSerializer<S : Recipe<*>> private constructor(
class Codec2RecipeSerializer<S : Recipe<*>>(
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<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)
private val codec = codec.invoke(Context())
private val id: ArrayDeque<ResourceLocation>
get() = idStack.getOrSet { ArrayDeque() }
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()
}
}
}
}
private class CurrentContext {
val idStack = ArrayDeque<ResourceLocation>()
var isNetwork = 0
}
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> {
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> {
return Codec2RecipeSerializer(empty?.let(to), idStack) { _ ->
return Codec2RecipeSerializer(empty?.let(to)) { _ ->
codec.xmap(to, from)
}
}
override fun fromJson(id: ResourceLocation, data: JsonObject): S {
try {
this.id.addLast(id)
context.idStack.addLast(id)
return decode(JsonOps.INSTANCE, data).get().map(
{
it.first
},
{
empty ?: throw JsonSyntaxException("Failed to deserialize recipe from JSON: ${it.message()}")
}
{ it.first },
{ empty ?: throw JsonSyntaxException("Failed to deserialize recipe from JSON: ${it.message()}") }
)
} finally {
this.id.removeLast()
context.idStack.removeLast()
}
}
override fun fromNetwork(id: ResourceLocation, data: FriendlyByteBuf): S? {
try {
this.id.addLast(id)
context.idStack.addLast(id)
context.isNetwork++
return data.readBinaryJsonWithCodecIndirect(this)
.resultOrPartial { LOGGER.error("Failed to read recipe $id from network: $it") }.orElse(null)
} finally {
this.id.removeLast()
context.isNetwork--
context.idStack.removeLast()
}
}
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 {
@ -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 {
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 ru.dbotthepony.mc.otm.core.collect.allEqual
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.stream
import ru.dbotthepony.mc.otm.recipe.IIngredientMatrix
import ru.dbotthepony.mc.otm.recipe.IngredientMatrix
import java.util.function.Supplier
object IngredientMatrixCodec : Codec<IIngredientMatrix> {
private val ingredientList = Codec.list(IngredientCodec)
class IngredientMatrixCodec(ingredientCodec: Codec<Ingredient>) : Codec<IIngredientMatrix> {
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> {
return DataResult.success(
ops.createList(
(0 until input.height).stream().map { row ->
ops.createList((0 until input.width).stream().map { column ->
JsonOps.INSTANCE.convertTo(ops, input[column, row].toJson())
})
}
)
return doubleIngredientList.encode(
(0 until input.height).iterator().map { row ->
(0 until input.width).iterator().map { column ->
input[column, row]
}.toList()
}.toList(),
ops,
prefix
)
}
@ -45,7 +47,7 @@ object IngredientMatrixCodec : Codec<IIngredientMatrix> {
.flatXmap(
{ 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()) }
), IngredientCodec).fieldOf("key").forGetter(Handwritten::key)
), ingredientCodec).fieldOf("key").forGetter(Handwritten::key)
).apply(it, ::Handwritten)
}

View File

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

View File

@ -113,12 +113,10 @@ class PainterRecipe(
}
companion object {
val SERIALIZER = Codec2RecipeSerializer<PainterRecipe>(
PainterRecipe(ResourceLocation(OverdriveThatMatters.MOD_ID, "empty"), Ingredient.EMPTY, ItemStack.EMPTY, setOf())
) { context ->
val SERIALIZER = Codec2RecipeSerializer<PainterRecipe> { context ->
RecordCodecBuilder.create {
it.group(
IngredientCodec.fieldOf("input").forGetter(PainterRecipe::input),
context.ingredients.fieldOf("input").forGetter(PainterRecipe::input),
ItemStack.CODEC.fieldOf("output").forGetter(PainterRecipe::output),
PredicatedCodecList<Map<DyeColor, Int>>(
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)
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 {
it.group(
IngredientCodec.fieldOf("input").forGetter(PlatePressRecipe::input),
IngredientCodec.fieldOf("output").forGetter(PlatePressRecipe::output),
context.ingredients.fieldOf("input").forGetter(PlatePressRecipe::input),
context.ingredients.fieldOf("output").forGetter(PlatePressRecipe::output),
Codec.INT.minRange(1).optionalFieldOf("count", 1).forGetter(PlatePressRecipe::count),
Codec.INT.minRange(0).optionalFieldOf("workTime", 200).forGetter(PlatePressRecipe::workTime),
FloatProvider.CODEC.optionalFieldOf("experience", ConstantFloat.ZERO).forGetter(PlatePressRecipe::experience)