Skip to content

Commit d8be87a

Browse files
authored
Merge pull request #234 from pylonmc/feature/recipe-config
Recipe config
2 parents b671ff5 + 7fa3abe commit d8be87a

51 files changed

Lines changed: 1365 additions & 490 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pylon-core/build.gradle.kts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ dependencies {
2727

2828
runtimeOnly(project(":nms"))
2929

30-
implementation("info.debatty:java-string-similarity:2.0.0")
31-
3230
paperLibraryApi("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
3331

3432
compileOnly("io.papermc.paper:paper-api:1.21.8-R0.1-SNAPSHOT")
@@ -39,6 +37,7 @@ dependencies {
3937
// see https://github.com/NichtStudioCode/InvUI/blob/main/inventoryaccess/inventory-access/src/main/java/xyz/xenondevs/inventoryaccess/version/InventoryAccessRevision.java
4038
paperLibrary("xyz.xenondevs.invui:inventory-access-r24:1.46:remapped-mojang")
4139
paperLibraryApi("xyz.xenondevs.invui:invui-kotlin:1.46")
40+
implementation("info.debatty:java-string-similarity:2.0.0")
4241

4342
testImplementation(kotlin("test"))
4443
testImplementation("com.willowtreeapps.assertk:assertk:0.28.1")
@@ -63,7 +62,7 @@ kotlin {
6362
jvmToolchain(21)
6463
compilerOptions {
6564
javaParameters = true
66-
freeCompilerArgs = listOf("-Xjvm-default=all")
65+
freeCompilerArgs = listOf("-Xjvm-default=all", "-Xwhen-guards")
6766
}
6867
}
6968

@@ -105,6 +104,8 @@ tasks.shadowJar {
105104
mergeServiceFiles()
106105

107106
exclude("kotlin/**")
107+
exclude("org/intellij/lang/annotations/**")
108+
exclude("org/jetbrains/annotations/**")
108109

109110
archiveBaseName = "pylon-core"
110111
archiveClassifier = null

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
package io.github.pylonmc.pylon.core
44

5+
import com.github.shynixn.mccoroutine.bukkit.launch
6+
import com.github.shynixn.mccoroutine.bukkit.ticks
57
import io.github.pylonmc.pylon.core.addon.PylonAddon
68
import io.github.pylonmc.pylon.core.block.*
79
import io.github.pylonmc.pylon.core.block.base.*
810
import io.github.pylonmc.pylon.core.block.waila.Waila
911
import io.github.pylonmc.pylon.core.command.ROOT_COMMAND
1012
import io.github.pylonmc.pylon.core.command.ROOT_COMMAND_PY_ALIAS
13+
import io.github.pylonmc.pylon.core.config.Config
14+
import io.github.pylonmc.pylon.core.config.ConfigSection
1115
import io.github.pylonmc.pylon.core.content.debug.DebugWaxedWeatheredCutCopperStairs
1216
import io.github.pylonmc.pylon.core.content.fluid.*
1317
import io.github.pylonmc.pylon.core.content.guide.PylonGuide
@@ -20,12 +24,17 @@ import io.github.pylonmc.pylon.core.item.PylonItem
2024
import io.github.pylonmc.pylon.core.item.PylonItemListener
2125
import io.github.pylonmc.pylon.core.item.research.Research
2226
import io.github.pylonmc.pylon.core.mobdrop.MobDropListener
27+
import io.github.pylonmc.pylon.core.recipe.ConfigurableRecipeType
2328
import io.github.pylonmc.pylon.core.recipe.DisplayRecipeType
2429
import io.github.pylonmc.pylon.core.recipe.PylonRecipeListener
2530
import io.github.pylonmc.pylon.core.recipe.RecipeType
31+
import io.github.pylonmc.pylon.core.registry.PylonRegistry
2632
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents
33+
import kotlinx.coroutines.delay
2734
import org.bukkit.Bukkit
2835
import org.bukkit.Material
36+
import org.bukkit.NamespacedKey
37+
import org.bukkit.configuration.file.YamlConfiguration
2938
import org.bukkit.entity.BlockDisplay
3039
import org.bukkit.entity.Interaction
3140
import org.bukkit.entity.ItemDisplay
@@ -34,6 +43,7 @@ import org.bukkit.permissions.PermissionDefault
3443
import org.bukkit.plugin.java.JavaPlugin
3544
import xyz.xenondevs.invui.InvUI
3645
import java.util.Locale
46+
import kotlin.io.path.*
3747

3848
object PylonCore : JavaPlugin(), PylonAddon {
3949

@@ -44,6 +54,8 @@ object PylonCore : JavaPlugin(), PylonAddon {
4454

4555
Bukkit.getPluginManager().registerEvents(PylonTranslator, this)
4656
Bukkit.getPluginManager().registerEvents(PylonAddon, this)
57+
58+
// Anything that listens for addon registration must be above this line
4759
registerWithPylon()
4860

4961
Bukkit.getPluginManager().registerEvents(BlockStorage, this)
@@ -104,6 +116,38 @@ object PylonCore : JavaPlugin(), PylonAddon {
104116

105117
DisplayRecipeType.register()
106118
RecipeType.addVanillaRecipes()
119+
120+
launch {
121+
delay(1.ticks)
122+
loadRecipes()
123+
}
124+
}
125+
126+
private fun loadRecipes() {
127+
logger.info("Loading recipes...")
128+
for (type in PylonRegistry.RECIPE_TYPES) {
129+
if (type !is ConfigurableRecipeType) continue
130+
for (addon in PylonRegistry.ADDONS) {
131+
val configStream = addon.javaPlugin.getResource(type.filePath) ?: continue
132+
val config = configStream.reader().use { ConfigSection(YamlConfiguration.loadConfiguration(it)) }
133+
type.loadFromConfig(config)
134+
}
135+
}
136+
137+
val recipesDir = dataPath.resolve("recipes")
138+
if (recipesDir.exists()) {
139+
for (recipeDir in recipesDir.listDirectoryEntries()) {
140+
if (!recipeDir.isDirectory()) continue
141+
val namespace = recipeDir.nameWithoutExtension
142+
for (recipe in recipeDir.listDirectoryEntries()) {
143+
if (!recipe.isRegularFile() || recipe.extension != "yml") continue
144+
val key = NamespacedKey(namespace, recipe.nameWithoutExtension)
145+
val type = PylonRegistry.RECIPE_TYPES[key] as? ConfigurableRecipeType ?: continue
146+
type.loadFromConfig(Config(recipe))
147+
}
148+
}
149+
}
150+
logger.info("Finished loading recipes")
107151
}
108152

109153
override fun onDisable() {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.bukkit.event.EventHandler
1919
import org.bukkit.event.Listener
2020
import org.bukkit.event.server.PluginDisableEvent
2121
import org.bukkit.plugin.java.JavaPlugin
22+
import org.jetbrains.annotations.ApiStatus
2223
import java.util.Locale
2324

2425
/**
@@ -64,6 +65,7 @@ interface PylonAddon : Keyed {
6465
/**
6566
* Must be called as the first thing in your plugin's `onEnable`
6667
*/
68+
@ApiStatus.NonExtendable
6769
fun registerWithPylon() {
6870
PylonRegistry.ADDONS.register(this)
6971
if (!suppressAddonNameWarning) {

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.mojang.brigadier.arguments.IntegerArgumentType
77
import com.mojang.brigadier.arguments.LongArgumentType
88
import com.mojang.brigadier.context.CommandContext
99
import io.github.pylonmc.pylon.core.PylonCore
10+
import io.github.pylonmc.pylon.core.addon.PylonAddon
1011
import io.github.pylonmc.pylon.core.block.BlockStorage
1112
import io.github.pylonmc.pylon.core.block.PylonBlockSchema
1213
import io.github.pylonmc.pylon.core.block.waila.Waila.Companion.wailaEnabled
@@ -21,6 +22,8 @@ import io.github.pylonmc.pylon.core.item.research.Research.Companion.researches
2122
import io.github.pylonmc.pylon.core.item.research.addResearch
2223
import io.github.pylonmc.pylon.core.item.research.hasResearch
2324
import io.github.pylonmc.pylon.core.item.research.removeResearch
25+
import io.github.pylonmc.pylon.core.recipe.ConfigurableRecipeType
26+
import io.github.pylonmc.pylon.core.recipe.RecipeType
2427
import io.github.pylonmc.pylon.core.registry.PylonRegistry
2528
import io.github.pylonmc.pylon.core.test.GameTestConfig
2629
import io.github.pylonmc.pylon.core.util.getArgument
@@ -381,6 +384,29 @@ private val research = buildCommand("research") {
381384
then(researchPoints)
382385
}
383386

387+
private val exposeRecipeConfig = buildCommand("exposerecipeconfig") {
388+
argument("addon", RegistryCommandArgument(PylonRegistry.ADDONS)) {
389+
argument("recipe", RegistryCommandArgument(PylonRegistry.RECIPE_TYPES)) {
390+
permission("pylon.command.exposerecipeconfig")
391+
executes { sender ->
392+
val addon = getArgument<PylonAddon>("addon")
393+
val recipeType = getArgument<RecipeType<*>>("recipe")
394+
if (recipeType !is ConfigurableRecipeType) {
395+
sender.sendMessage(Component.translatable("pylon.pyloncore.message.command.exposerecipe.not-configurable"))
396+
return@executes
397+
}
398+
sender.sendMessage(
399+
Component.translatable(
400+
"pylon.pyloncore.message.command.exposerecipe.warning",
401+
PylonArgument.of("file", "plugins/PylonCore/${recipeType.filePath}")
402+
)
403+
)
404+
addon.mergeGlobalConfig(recipeType.filePath, recipeType.filePath)
405+
}
406+
}
407+
}
408+
}
409+
384410
@JvmSynthetic
385411
internal val ROOT_COMMAND = buildCommand("pylon") {
386412
permission("pylon.command.guide")
@@ -396,6 +422,7 @@ internal val ROOT_COMMAND = buildCommand("pylon") {
396422
then(waila)
397423
then(gametest)
398424
then(research)
425+
then(exposeRecipeConfig)
399426
}
400427

401428
@JvmSynthetic
Lines changed: 29 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
package io.github.pylonmc.pylon.core.config
22

3-
import com.google.common.base.CaseFormat
4-
import io.github.pylonmc.pylon.core.fluid.PylonFluid
5-
import io.github.pylonmc.pylon.core.registry.PylonRegistry
6-
import io.github.pylonmc.pylon.core.util.itemFromName
7-
import org.bukkit.Material
8-
import org.bukkit.NamespacedKey
3+
import io.github.pylonmc.pylon.core.config.adapter.ConfigAdapter
94
import org.bukkit.configuration.ConfigurationSection
10-
import org.bukkit.inventory.ItemStack
11-
import kotlin.properties.ReadWriteProperty
12-
import kotlin.reflect.KProperty
5+
import java.lang.reflect.ParameterizedType
6+
import java.lang.reflect.Type
7+
import java.util.WeakHashMap
138

149
open class ConfigSection(val internalSection: ConfigurationSection) {
1510

16-
private val cache: MutableMap<String, Any?> = mutableMapOf()
11+
private val cache: MutableMap<String, Any?> = WeakHashMap()
1712

1813
val keys: Set<String>
1914
get() = internalSection.getKeys(false)
@@ -44,83 +39,41 @@ open class ConfigSection(val internalSection: ConfigurationSection) {
4439
fun getSectionOrThrow(key: String): ConfigSection =
4540
getSection(key) ?: throw KeyNotFoundException(internalSection.currentPath, key)
4641

47-
fun getFluid(key: String): PylonFluid? {
48-
val cached = cache[key]
49-
if (cached != null) {
50-
if (cached !is PylonFluid) {
51-
return null
52-
}
53-
return cached
54-
}
55-
56-
val name = get<String>(key) ?: return null
57-
val fluid = PylonRegistry.FLUIDS[
58-
NamespacedKey.fromString(name) ?: error("'$name' is not a namespaced key")
59-
]
60-
cache[key] = fluid
61-
return fluid
42+
/**
43+
* Returns null either if the key does not exist or if the value cannot be converted to the desired type.
44+
*/
45+
fun <T> get(key: String, adapter: ConfigAdapter<T>): T? {
46+
return runCatching { getOrThrow(key, adapter) }.getOrNull()
6247
}
6348

64-
fun getFluidOrThrow(key: String): PylonFluid {
65-
val name = getOrThrow<String>(key)
66-
return PylonRegistry.FLUIDS[
67-
NamespacedKey.fromString(name) ?: error("'$name' is not a namespaced key")
68-
] ?: error("No such fluid '$name'")
49+
fun <T> get(key: String, adapter: ConfigAdapter<T>, defaultValue: T): T {
50+
return get(key, adapter) ?: defaultValue
6951
}
7052

71-
fun getMaterial(key: String): Material? {
72-
val cached = cache[key]
73-
if (cached != null) {
74-
if (cached !is Material) {
75-
return null
53+
fun <T> getOrThrow(key: String, adapter: ConfigAdapter<T>): T {
54+
val value = cache.getOrPut(key) {
55+
val value = internalSection.get(key) ?: throw KeyNotFoundException(internalSection.currentPath, key)
56+
try {
57+
adapter.convert(value)
58+
} catch (e: Exception) {
59+
throw IllegalArgumentException(
60+
"Failed to convert value '$value' to type ${adapter.type} for key '$key' in section '${internalSection.currentPath}'",
61+
e
62+
)
7663
}
77-
return cached
7864
}
7965

80-
val name = get<String>(key) ?: return null
81-
val material = Material.getMaterial(name.uppercase())
82-
cache[key] = material
83-
return material
84-
}
85-
86-
fun getMaterialOrThrow(key: String): Material {
87-
val name = getOrThrow<String>(key)
88-
return Material.getMaterial(name.uppercase()) ?: error("No such material '$name'")
89-
}
90-
91-
fun getItem(key: String): ItemStack?
92-
= get(key, String::class.java)?.let { itemFromName(it) }
93-
94-
fun getItemOrThrow(key: String): ItemStack
95-
= itemFromName(getOrThrow(key, String::class.java))
96-
?: error("No such item $key")
97-
98-
fun <T> get(key: String, type: Class<out T>): T? {
99-
val cached = cache[key]
100-
if (cached != null) {
101-
if (!type.isInstance(cached)) {
102-
return null
103-
}
104-
@Suppress("UNCHECKED_CAST")
105-
return cached as T?
66+
fun getClass(type: Type): Class<*> = when (type) {
67+
is Class<*> -> type
68+
is ParameterizedType -> getClass(type.rawType)
69+
else -> throw IllegalArgumentException("Unsupported type: $type")
10670
}
10771

108-
val value = internalSection.get(key) ?: return null
109-
cache[key] = value
110-
return type.cast(value)
72+
@Suppress("UNCHECKED_CAST")
73+
val clazz = getClass(adapter.type) as Class<T>
74+
return clazz.cast(value)
11175
}
11276

113-
inline fun <reified T> get(key: String): T? = get(key, T::class.java)
114-
115-
fun <T> getOrThrow(key: String, type: Class<out T>): T =
116-
get(key, type) ?: throw KeyNotFoundException(internalSection.currentPath, key)
117-
118-
inline fun <reified T> getOrThrow(key: String): T = getOrThrow(key, T::class.java)
119-
120-
fun <T> get(key: String, type: Class<out T>, default: T): T = get(key, type) ?: default
121-
122-
inline fun <reified T> get(key: String, default: T): T = get(key, T::class.java, default)
123-
12477
fun <T> set(key: String, value: T) {
12578
internalSection.set(key, value)
12679
}
@@ -141,23 +94,6 @@ open class ConfigSection(val internalSection: ConfigurationSection) {
14194
}
14295
}
14396

144-
@JvmSynthetic
145-
inline operator fun <reified T> provideDelegate(
146-
@Suppress("unused") thisRef: Any,
147-
property: KProperty<*>,
148-
): ReadWriteProperty<Any, T> {
149-
val key = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, property.name)
150-
return object : ReadWriteProperty<Any, T> {
151-
override fun getValue(thisRef: Any, property: KProperty<*>): T {
152-
return getOrThrow(key)
153-
}
154-
155-
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
156-
set(key, value)
157-
}
158-
}
159-
}
160-
16197
class KeyNotFoundException(path: String?, key: String) :
16298
Exception(if (!path.isNullOrEmpty()) "Config key not found: $path.$key" else "Config key not found: $key")
16399
}

0 commit comments

Comments
 (0)