Skip to content

Commit

Permalink
Rework block contexts
Browse files Browse the repository at this point in the history
  • Loading branch information
NichtStudioCode committed Oct 30, 2023
1 parent 34f4a24 commit ec5deaa
Show file tree
Hide file tree
Showing 33 changed files with 1,052 additions and 374 deletions.
45 changes: 36 additions & 9 deletions nova/src/main/kotlin/xyz/xenondevs/nova/api/ApiBlockManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
package xyz.xenondevs.nova.api

import org.bukkit.Location
import org.bukkit.entity.Entity
import org.bukkit.inventory.ItemStack
import xyz.xenondevs.nova.api.block.NovaBlockState
import xyz.xenondevs.nova.api.material.NovaMaterial
import xyz.xenondevs.nova.data.context.Context
import xyz.xenondevs.nova.data.context.intention.ContextIntentions.BlockBreak
import xyz.xenondevs.nova.data.context.intention.ContextIntentions.BlockPlace
import xyz.xenondevs.nova.data.context.param.ContextParamTypes
import xyz.xenondevs.nova.data.world.block.state.NovaTileEntityState
import xyz.xenondevs.nova.world.block.BlockManager
import xyz.xenondevs.nova.world.block.context.BlockBreakContext
import xyz.xenondevs.nova.world.block.context.BlockPlaceContext
import xyz.xenondevs.nova.world.pos
import java.util.*
import xyz.xenondevs.nova.api.block.BlockManager as IBlockManager
import xyz.xenondevs.nova.api.block.NovaBlock as INovaBlock

Expand All @@ -28,9 +32,14 @@ internal object ApiBlockManager : IBlockManager {
}

override fun placeBlock(location: Location, block: INovaBlock, source: Any?, playSound: Boolean) {
require(block is ApiBlockWrapper) { "material must be ApiBlockWrapper" }
val ctx = BlockPlaceContext.forAPI(location, block, source)
BlockManager.placeBlockState(block.block, ctx, playSound)
require(block is ApiBlockWrapper) { "block must be ApiBlockWrapper" }

val ctxBuilder = Context.intention(BlockPlace)
.param(ContextParamTypes.BLOCK_POS, location.pos)
.param(ContextParamTypes.BLOCK_TYPE_NOVA, block.block)
.param(ContextParamTypes.BLOCK_PLACE_EFFECTS, playSound)
setSourceParam(ctxBuilder, source)
BlockManager.placeBlockState(block.block, ctxBuilder.build())
}

override fun placeBlock(location: Location, material: NovaMaterial, source: Any?, playSound: Boolean) {
Expand All @@ -39,13 +48,31 @@ internal object ApiBlockManager : IBlockManager {
}

override fun getDrops(location: Location, source: Any?, tool: ItemStack?): List<ItemStack>? {
val ctx = BlockBreakContext.forAPI(location, source, tool)
return BlockManager.getDrops(ctx)
val ctxBuilder = Context.intention(BlockBreak)
.param(ContextParamTypes.BLOCK_POS, location.pos)
.param(ContextParamTypes.TOOL_ITEM_STACK, tool)
setSourceParam(ctxBuilder, source)
return BlockManager.getDrops(ctxBuilder.build())
}

override fun removeBlock(location: Location, source: Any?, breakEffects: Boolean): Boolean {
val ctx = BlockBreakContext.forAPI(location, source, null)
return BlockManager.removeBlockState(ctx, breakEffects)
val ctxBuilder = Context.intention(BlockBreak)
.param(ContextParamTypes.BLOCK_POS, location.pos)
.param(ContextParamTypes.BLOCK_BREAK_EFFECTS, breakEffects)
setSourceParam(ctxBuilder, source)
return BlockManager.removeBlockState(ctxBuilder.build())
}

private fun setSourceParam(builder: Context.Builder<*>, source: Any?) {
if (source == null)
return

when (source) {
is Entity -> builder.param(ContextParamTypes.SOURCE_ENTITY, source)
is ApiTileEntityWrapper -> builder.param(ContextParamTypes.SOURCE_TILE_ENTITY, source.tileEntity)
is Location -> builder.param(ContextParamTypes.SOURCE_LOCATION, source)
is UUID -> builder.param(ContextParamTypes.SOURCE_UUID, source)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import xyz.xenondevs.nova.tileentity.TileEntity
import xyz.xenondevs.nova.api.tileentity.TileEntity as ITileEntity

@Suppress("DEPRECATION")
internal class ApiTileEntityWrapper(private val tileEntity: TileEntity) : ITileEntity {
internal class ApiTileEntityWrapper(val tileEntity: TileEntity) : ITileEntity {

@Deprecated("Use NovaBlock instead", replaceWith = ReplaceWith("block"))
override fun getMaterial(): NovaMaterial = LegacyMaterialWrapper(Either.right(tileEntity.block))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import xyz.xenondevs.nova.command.requiresPlayerPermission
import xyz.xenondevs.nova.command.sendFailure
import xyz.xenondevs.nova.command.sendSuccess
import xyz.xenondevs.nova.data.config.Configs
import xyz.xenondevs.nova.data.context.Context
import xyz.xenondevs.nova.data.context.intention.ContextIntentions
import xyz.xenondevs.nova.data.context.param.ContextParamTypes
import xyz.xenondevs.nova.data.recipe.RecipeManager
import xyz.xenondevs.nova.data.resources.ResourceGeneration
import xyz.xenondevs.nova.data.resources.builder.ResourcePackBuilder
Expand Down Expand Up @@ -53,7 +56,6 @@ import xyz.xenondevs.nova.util.item.takeUnlessEmpty
import xyz.xenondevs.nova.util.runAsyncTask
import xyz.xenondevs.nova.world.block.BlockManager
import xyz.xenondevs.nova.world.block.backingstate.BackingStateManager
import xyz.xenondevs.nova.world.block.context.BlockBreakContext
import xyz.xenondevs.nova.world.block.hitbox.HitboxManager
import xyz.xenondevs.nova.world.chunkPos
import xyz.xenondevs.nova.world.fakeentity.FakeEntityManager.MAX_RENDER_DISTANCE
Expand Down Expand Up @@ -405,7 +407,12 @@ internal object NovaCommand : Command("nova") {
val player = ctx.player
val chunks = player.location.chunk.getSurroundingChunks(ctx["range"], true)
val novaBlocks = chunks.flatMap { WorldDataManager.getBlockStates(it.pos).values.filterIsInstance<NovaBlockState>() }
novaBlocks.forEach { BlockManager.removeBlockState(BlockBreakContext(it.pos)) }
novaBlocks.forEach {
val breakCtx = Context.intention(ContextIntentions.BlockBreak)
.param(ContextParamTypes.BLOCK_POS, it.pos)
.build()
BlockManager.removeBlockState(breakCtx)
}

ctx.source.sendSuccess(Component.translatable(
"command.nova.remove_tile_entities.success",
Expand Down
204 changes: 204 additions & 0 deletions nova/src/main/kotlin/xyz/xenondevs/nova/data/context/Context.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
@file:Suppress("UNCHECKED_CAST")

package xyz.xenondevs.nova.data.context

import xyz.xenondevs.commons.collections.filterValuesNotNullTo
import xyz.xenondevs.commons.reflection.call
import xyz.xenondevs.nova.data.context.intention.ContextIntention
import xyz.xenondevs.nova.data.context.param.ContextParamType
import xyz.xenondevs.nova.data.context.param.DefaultingContextParamType

class Context<I : ContextIntention> private constructor(
private val intention: I,
private val explicitParams: Map<ContextParamType<*>, Any>,
private val resolvedParams: Map<ContextParamType<*>, Any>,
) {

/**
* Returns the value of the given [paramType] or null if it is not present.
*/
operator fun <V : Any> get(paramType: ContextParamType<V>): V? {
return getParam(paramType)
}

/**
* Returns the value of the given [paramType].
*/
operator fun <V : Any> get(paramType: DefaultingContextParamType<V>): V {
return getParam(paramType) ?: paramType.defaultValue
}

/**
* Returns the value of the given [paramType] or throws an exception if it is not present.
*
* @throws IllegalStateException If the given [paramType] is an optional parameter that is not present.
* @throws IllegalArgumentException If the given [paramType] is not allowed under this context's intention.
*/
fun <V : Any> getOrThrow(paramType: ContextParamType<V>): V {
val value = getParam(paramType)

if (value != null)
return value

if (paramType is DefaultingContextParamType)
return paramType.defaultValue

throwParamNotPresent(paramType)
}

private fun <V : Any> getParam(paramType: ContextParamType<V>): V? {
return (explicitParams[paramType] ?: resolvedParams[paramType]) as V?
}


/**
* Checks whether the given [paramType] is present in this context.
*
* @throws IllegalArgumentException When the given [paramType] is not allowed under this context's intention.
*/
fun has(paramType: ContextParamType<*>): Boolean {
if (paramType in explicitParams || paramType in resolvedParams)
return true

if (paramType !in intention.all)
throw IllegalArgumentException("A context of intention $intention will never contain parameter ${paramType.id}")

return false
}

/**
* Checks whether the given [paramType] is explicitly specified in this context.
*
* @throws IllegalArgumentException When the given [paramType] is not allowed under this context's intention.
*/
fun hasExplicitly(paramType: ContextParamType<*>): Boolean {
if (paramType in explicitParams)
return true

if (paramType !in intention.all)
throw IllegalArgumentException("A context of intention $intention will never contain parameter ${paramType.id}")

return false
}

private fun throwParamNotPresent(paramType: ContextParamType<*>): Nothing {
if (paramType in intention.all)
throw IllegalStateException("Context parameter ${paramType.id} is not present")
else throw IllegalArgumentException("Context parameter ${paramType.id} is not allowed")
}

companion object {

/**
* Creates a new context builder for the given [intention].
*/
fun <I : ContextIntention> intention(intention: I): Builder<I> {
return Builder(intention)
}

}

class Builder<I : ContextIntention> internal constructor(private val intention: I) {

/**
* The parameters that are explicitly set.
*/
private val explicitParams = HashMap<ContextParamType<*>, Any>()

/**
* The parameters that are loaded through autofillers. The value is null if the param could not be loaded
* through fallbacks.
*/
private val resolvedParams = HashMap<ContextParamType<*>, Any?>()

/**
* Sets the given [paramType] to the given [value].
*/
fun <V : Any> param(paramType: ContextParamType<V>, value: V?): Builder<I> {
if (paramType !in intention.all)
throw IllegalArgumentException("Context parameter ${paramType.id} is not allowed under intention $intention")

if (value == null) {
explicitParams.remove(paramType)
} else {
// check requirements
for (requirement in paramType.requirements) {
if (!requirement.validator(value))
throw IllegalArgumentException("Context value: $value for parameter type: ${paramType.id} is invalid: ${requirement.errorGenerator(value)}")
}

explicitParams[paramType] = value
}

return this
}

/**
* Builds the context.
*/
fun build(autofill: Boolean = true): Context<I> {
if (autofill)
resolveParams()

// verify presence of all required params
for (requiredParam in intention.required) {
if (requiredParam !in explicitParams)
throw IllegalStateException("Required context parameter ${requiredParam.id} is not present")
}

return Context(
intention,
HashMap(explicitParams),
resolvedParams.filterValuesNotNullTo(HashMap())
)
}

private fun resolveParams() {
for (paramType in intention.all) {
resolveParam(paramType)
}
}

private fun hasParam(paramType: ContextParamType<*>): Boolean =
paramType in explicitParams || paramType in resolvedParams

private fun <V : Any> getParam(paramType: ContextParamType<V>): V? =
explicitParams[paramType] as V? ?: resolvedParams[paramType] as V?

private fun <V : Any> resolveParam(paramType: ContextParamType<V>): V? {
if (hasParam(paramType))
return getParam(paramType)

// preemptively set this to null to prevent recursive call chains
resolvedParams[paramType] = null

// try to resolve value through autofillers
val autofillers = paramType.autofillers
var value: V? = null
autofiller@ for ((requiredParamTypes, fillerFunction) in autofillers) {
// load params required by autofiller
val requiredParamValues = arrayOfNulls<Any>(requiredParamTypes.size)
for ((i, requiredParamType) in requiredParamTypes.withIndex()) {
val requiredParamValue = resolveParam(requiredParamType)
?: continue@autofiller // try next autofiller
requiredParamValues[i] = requiredParamValue
}

// run autofiller function
value = fillerFunction.call(*requiredParamValues)

if (value != null && paramType.requirements.all { it.validator(value!!) })
break
}

// otherwise, use default value if present
if (value == null && paramType is DefaultingContextParamType)
value = paramType.defaultValue

resolvedParams[paramType] = value
return value
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package xyz.xenondevs.nova.data.context.intention

import xyz.xenondevs.nova.data.context.param.ContextParamType

/**
* Represents an intention for what a context is used for.
*
* @param required The required parameters for this intention.
* @param optional The optional parameters for this intention.
* @param all All parameters for this intention.
*/
abstract class ContextIntention(
val required: Set<ContextParamType<*>>,
val optional: Set<ContextParamType<*>>,
val all: Set<ContextParamType<*>>
) {

/**
* Creates an intention with the given [required] and [optional] parameters.
*/
constructor(required: Collection<ContextParamType<*>>, optional: Collection<ContextParamType<*>>) :
this(required.toHashSet(), optional.toHashSet(), (required + optional).toHashSet())

}
Loading

0 comments on commit ec5deaa

Please sign in to comment.