Skip to content

Commit 3687ea9

Browse files
authored
Merge pull request #519 from pylonmc/pylon-recipe-processor
Add PylonRecipeProcessor
2 parents d03a8eb + 50fe767 commit 3687ea9

File tree

7 files changed

+216
-29
lines changed

7 files changed

+216
-29
lines changed

pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/PylonCore.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ object PylonCore : JavaPlugin(), PylonAddon {
125125
Bukkit.getPluginManager().registerEvents(PylonGuiBlock, this)
126126
Bukkit.getPluginManager().registerEvents(PylonEntityHolderBlock, this)
127127
Bukkit.getPluginManager().registerEvents(PylonSimpleMultiblock, this)
128+
Bukkit.getPluginManager().registerEvents(PylonRecipeProcessor, this)
128129
Bukkit.getPluginManager().registerEvents(PylonFluidBufferBlock, this)
129130
Bukkit.getPluginManager().registerEvents(PylonFluidTank, this)
130131
Bukkit.getPluginManager().registerEvents(PylonRecipeListener, this)

pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/BlockStorage.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ object BlockStorage : Listener {
257257

258258
BlockTextureEngine.insert(block)
259259
PylonBlockPlaceEvent(blockPosition.block, block, context).callEvent()
260+
block.postInitialise()
260261

261262
return block
262263
}

pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/PylonBlock.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ open class PylonBlock internal constructor(val block: Block) {
131131
*/
132132
protected open fun postLoad() {}
133133

134+
/**
135+
* Called after both the create constructor and the load constructor.
136+
*
137+
* Use this to initialise stuff which must always be initialised, like creating logistics
138+
* groups (see [io.github.pylonmc.pylon.core.block.base.PylonLogisticBlock]).
139+
*
140+
* Called before [postLoad], after [io.github.pylonmc.pylon.core.event.PylonBlockPlaceEvent],
141+
* after [PylonBlockDeserializeEvent], and
142+
* before [io.github.pylonmc.pylon.core.event.PylonBlockLoadEvent]
143+
*/
144+
open fun postInitialise() {}
145+
134146
/**
135147
* Used to initialize [blockTextureEntity], if you need to modify the entity post-initialization,
136148
* use [updateBlockTexture].
@@ -387,6 +399,7 @@ open class PylonBlock internal constructor(val block: Block) {
387399
val block = schema.load(position.block, pdc)
388400

389401
PylonBlockDeserializeEvent(block.block, block, pdc).callEvent()
402+
block.postInitialise()
390403
block.postLoad()
391404
return block
392405
} catch (t: Throwable) {

pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/block/base/PylonLogisticBlock.kt

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
package io.github.pylonmc.pylon.core.block.base
22

33
import io.github.pylonmc.pylon.core.event.PylonBlockBreakEvent
4-
import io.github.pylonmc.pylon.core.logistics.LogisticSlot
5-
import io.github.pylonmc.pylon.core.logistics.LogisticSlotType
6-
import io.github.pylonmc.pylon.core.event.PylonBlockLoadEvent
7-
import io.github.pylonmc.pylon.core.event.PylonBlockPlaceEvent
84
import io.github.pylonmc.pylon.core.event.PylonBlockUnloadEvent
95
import io.github.pylonmc.pylon.core.logistics.LogisticGroup
6+
import io.github.pylonmc.pylon.core.logistics.LogisticSlot
7+
import io.github.pylonmc.pylon.core.logistics.LogisticSlotType
108
import io.github.pylonmc.pylon.core.logistics.VirtualInventoryLogisticSlot
119
import org.bukkit.block.Block
1210
import org.bukkit.event.EventHandler
13-
import org.bukkit.event.EventPriority
1411
import org.bukkit.event.Listener
1512
import org.jetbrains.annotations.ApiStatus
1613
import xyz.xenondevs.invui.inventory.VirtualInventory
17-
import java.util.IdentityHashMap
14+
import java.util.*
1815

1916
/**
2017
* A block which can have items removed or added via a logistics system.
@@ -33,13 +30,6 @@ interface PylonLogisticBlock {
3330
*/
3431
val block: Block
3532

36-
/**
37-
* Sets up all the logistic slot groups. This is where you should
38-
* call [createLogisticGroup] to create all the logistic slot
39-
* groups you want.
40-
*/
41-
fun setupLogisticGroups()
42-
4333
fun createLogisticGroup(groupName: String, group: LogisticGroup) {
4434
val logisticBlockData = (logisticBlocks.getOrPut(this) { mutableMapOf() })
4535
check(!logisticBlockData.contains(groupName)) { "The slot group $groupName already exists" }
@@ -74,22 +64,6 @@ interface PylonLogisticBlock {
7464

7565
private val logisticBlocks = IdentityHashMap<PylonLogisticBlock, MutableMap<String, LogisticGroup>>()
7666

77-
@EventHandler
78-
private fun onPlace(event: PylonBlockPlaceEvent) {
79-
val block = event.pylonBlock
80-
if (block is PylonLogisticBlock) {
81-
block.setupLogisticGroups()
82-
}
83-
}
84-
85-
@EventHandler
86-
private fun onLoad(event: PylonBlockLoadEvent) {
87-
val block = event.pylonBlock
88-
if (block is PylonLogisticBlock) {
89-
block.setupLogisticGroups()
90-
}
91-
}
92-
9367
@EventHandler
9468
private fun onBreak(event: PylonBlockBreakEvent) {
9569
val block = event.pylonBlock
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package io.github.pylonmc.pylon.core.block.base
2+
3+
import io.github.pylonmc.pylon.core.datatypes.PylonSerializers
4+
import io.github.pylonmc.pylon.core.event.PylonBlockDeserializeEvent
5+
import io.github.pylonmc.pylon.core.event.PylonBlockLoadEvent
6+
import io.github.pylonmc.pylon.core.event.PylonBlockSerializeEvent
7+
import io.github.pylonmc.pylon.core.event.PylonBlockUnloadEvent
8+
import io.github.pylonmc.pylon.core.recipe.PylonRecipe
9+
import io.github.pylonmc.pylon.core.recipe.RecipeType
10+
import io.github.pylonmc.pylon.core.util.gui.ProgressItem
11+
import io.github.pylonmc.pylon.core.util.pylonKey
12+
import org.bukkit.event.EventHandler
13+
import org.bukkit.event.Listener
14+
import org.jetbrains.annotations.ApiStatus
15+
import java.util.IdentityHashMap
16+
17+
/**
18+
* An interface that stores and progresses a recipe.
19+
*
20+
* This does not actually handle the recipe inputs and outputs. Instead, it simply
21+
* tracks a recipe that is being processed and how much time is left on it, ticking
22+
* automatically.
23+
*
24+
* This interface overrides [PylonTickingBlock.tick], meaning the rate at which the
25+
* recipe ticks is determined by [PylonTickingBlock.setTickInterval].
26+
*/
27+
interface PylonRecipeProcessor<T: PylonRecipe> : PylonTickingBlock {
28+
29+
data class RecipeProcessorData(
30+
var recipeType: RecipeType<*>?,
31+
var currentRecipe: PylonRecipe?,
32+
var totalRecipeTicks: Int?,
33+
var recipeTicksRemaining: Int?,
34+
var progressItem: ProgressItem?,
35+
)
36+
37+
private val recipeProcessorData: RecipeProcessorData
38+
get() = recipeProcessorBlocks.getOrPut(this) { RecipeProcessorData(null, null, null, null, null)}
39+
40+
val currentRecipe: T?
41+
// cast should always be safe due to type restriction when starting recipe
42+
get() = recipeProcessorData.currentRecipe as T?
43+
44+
val recipeTicksRemaining: Int?
45+
get() = recipeProcessorData.recipeTicksRemaining
46+
47+
/**
48+
* Set the progress item that should be updated as the recipe progresses. Optional.
49+
*
50+
* Set once in your place constructor.
51+
*/
52+
@ApiStatus.NonExtendable
53+
fun setRecipeType(type: RecipeType<T>) {
54+
recipeProcessorData.recipeType = type
55+
}
56+
57+
/**
58+
* Set the progress item that should be updated as the recipe progresses. Optional.
59+
*
60+
* Does not persist; you must call this whenever the block is initialised (e.g.
61+
* in [io.github.pylonmc.pylon.core.block.PylonBlock.postInitialise])
62+
*/
63+
@ApiStatus.NonExtendable
64+
fun setProgressItem(item: ProgressItem) {
65+
recipeProcessorData.progressItem = item
66+
}
67+
68+
/**
69+
* Starts a new recipe with duration [ticks], with [ticks] being the number of server
70+
* ticks the recipe will take.
71+
*/
72+
fun startRecipe(recipe: T, ticks: Int) {
73+
recipeProcessorData.currentRecipe = recipe
74+
recipeProcessorData.totalRecipeTicks = ticks
75+
recipeProcessorData.recipeTicksRemaining = ticks
76+
recipeProcessorData.progressItem?.setTotalTimeTicks(ticks)
77+
recipeProcessorData.progressItem?.setRemainingTimeTicks(ticks)
78+
}
79+
80+
fun onRecipeFinished(recipe: T)
81+
82+
override fun tick(deltaSeconds: Double) {
83+
val data = recipeProcessorData
84+
85+
if (data.currentRecipe != null && data.recipeTicksRemaining != null) {
86+
data.progressItem?.setRemainingTimeTicks(data.recipeTicksRemaining!!)
87+
88+
// tick recipe
89+
if (data.recipeTicksRemaining!! > 0) {
90+
data.recipeTicksRemaining = data.recipeTicksRemaining!! - tickInterval
91+
return
92+
}
93+
94+
// finish recipe
95+
onRecipeFinished(data.currentRecipe as T)
96+
data.currentRecipe = null
97+
data.totalRecipeTicks = null
98+
data.recipeTicksRemaining = null
99+
data.progressItem?.totalTime = null
100+
// cast should always be safe due to type restriction when starting recipe
101+
return
102+
}
103+
}
104+
105+
companion object : Listener {
106+
107+
private val recipeProcessorKey = pylonKey("recipe_processor_data")
108+
109+
private val recipeProcessorBlocks = IdentityHashMap<PylonRecipeProcessor<*>, RecipeProcessorData>()
110+
111+
@EventHandler
112+
private fun onDeserialize(event: PylonBlockDeserializeEvent) {
113+
val block = event.pylonBlock
114+
if (block is PylonRecipeProcessor<*>) {
115+
val data = event.pdc.get(recipeProcessorKey, PylonSerializers.RECIPE_PROCESSOR_DATA)
116+
?: error("Recipe processor data not found for ${block.key}")
117+
recipeProcessorBlocks[block] = data
118+
}
119+
}
120+
121+
@EventHandler
122+
private fun onLoad(event: PylonBlockLoadEvent) {
123+
// This separate listener is needed because when [PylonBlockDeserializeEvent] fires, then the
124+
// block may not have been fully initialised yet (e.g. postInitialise may not have been called)
125+
// which means progressItem may not have been set yet
126+
val block = event.pylonBlock
127+
if (block is PylonRecipeProcessor<*>) {
128+
val data = recipeProcessorBlocks[block]!!
129+
data.progressItem?.setTotalTimeTicks(data.totalRecipeTicks)
130+
data.recipeTicksRemaining?.let { data.progressItem?.setRemainingTimeTicks(it) }
131+
}
132+
}
133+
134+
@EventHandler
135+
private fun onSerialize(event: PylonBlockSerializeEvent) {
136+
val block = event.pylonBlock
137+
if (block is PylonRecipeProcessor<*>) {
138+
event.pdc.set(recipeProcessorKey, PylonSerializers.RECIPE_PROCESSOR_DATA, recipeProcessorBlocks[block]!!)
139+
}
140+
}
141+
142+
@EventHandler
143+
private fun onUnload(event: PylonBlockUnloadEvent) {
144+
val block = event.pylonBlock
145+
if (block is PylonRecipeProcessor<*>) {
146+
recipeProcessorBlocks.remove(block)
147+
}
148+
}
149+
}
150+
}

pylon-core/src/main/kotlin/io/github/pylonmc/pylon/core/datatypes/PylonSerializers.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ object PylonSerializers {
117117
@JvmSynthetic
118118
internal val FLUID_TANK_DATA = FluidTankDataPersistentDataType
119119

120+
@JvmSynthetic
121+
internal val RECIPE_PROCESSOR_DATA = RecipeProcessorDataPersistentDataType
122+
120123
@JvmSynthetic
121124
internal val SIMPLE_MULTIBLOCK_DATA = SimpleMultiblockDataPersistentDataType
122125

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.github.pylonmc.pylon.core.datatypes
2+
3+
import io.github.pylonmc.pylon.core.block.base.PylonRecipeProcessor
4+
import io.github.pylonmc.pylon.core.registry.PylonRegistry
5+
import io.github.pylonmc.pylon.core.util.pylonKey
6+
import io.github.pylonmc.pylon.core.util.setNullable
7+
import org.bukkit.persistence.PersistentDataAdapterContext
8+
import org.bukkit.persistence.PersistentDataContainer
9+
import org.bukkit.persistence.PersistentDataType
10+
11+
internal object RecipeProcessorDataPersistentDataType : PersistentDataType<PersistentDataContainer, PylonRecipeProcessor.RecipeProcessorData> {
12+
13+
private val RECIPE_TYPE_KEY = pylonKey("recipe_type")
14+
private val CURRENT_RECIPE_KEY = pylonKey("current_recipe")
15+
private val TOTAL_RECIPE_TICKS_KEY = pylonKey("total_recipe_ticks")
16+
private val RECIPE_TICKS_REMAINING_KEY = pylonKey("recipe_ticks_remaining")
17+
18+
private val RECIPE_TYPE_TYPE = PylonSerializers.KEYED.keyedTypeFrom { key -> PylonRegistry.RECIPE_TYPES.getOrThrow(key) }
19+
20+
override fun getPrimitiveType(): Class<PersistentDataContainer> = PersistentDataContainer::class.java
21+
22+
override fun getComplexType(): Class<PylonRecipeProcessor.RecipeProcessorData> = PylonRecipeProcessor.RecipeProcessorData::class.java
23+
24+
override fun fromPrimitive(primitive: PersistentDataContainer, context: PersistentDataAdapterContext): PylonRecipeProcessor.RecipeProcessorData {
25+
val recipeType = primitive.get(RECIPE_TYPE_KEY, RECIPE_TYPE_TYPE)!!
26+
val recipePDT = PylonSerializers.KEYED.keyedTypeFrom { recipeType.getRecipeOrThrow(it) }
27+
return PylonRecipeProcessor.RecipeProcessorData(
28+
recipeType,
29+
primitive.get(CURRENT_RECIPE_KEY, recipePDT),
30+
primitive.get(TOTAL_RECIPE_TICKS_KEY, PylonSerializers.INTEGER),
31+
primitive.get(RECIPE_TICKS_REMAINING_KEY, PylonSerializers.INTEGER),
32+
null
33+
)
34+
}
35+
36+
override fun toPrimitive(complex: PylonRecipeProcessor.RecipeProcessorData, context: PersistentDataAdapterContext): PersistentDataContainer {
37+
val pdc = context.newPersistentDataContainer()
38+
val recipePDT = PylonSerializers.KEYED.keyedTypeFrom { complex.recipeType!!.getRecipeOrThrow(it) }
39+
pdc.setNullable(RECIPE_TYPE_KEY, RECIPE_TYPE_TYPE, complex.recipeType)
40+
pdc.setNullable(CURRENT_RECIPE_KEY, recipePDT, complex.currentRecipe)
41+
pdc.setNullable(TOTAL_RECIPE_TICKS_KEY, PylonSerializers.INTEGER, complex.totalRecipeTicks)
42+
pdc.setNullable(RECIPE_TICKS_REMAINING_KEY, PylonSerializers.INTEGER, complex.recipeTicksRemaining)
43+
return pdc
44+
}
45+
}

0 commit comments

Comments
 (0)