Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(addon): Actions #104

Merged
merged 15 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ eclipse
kotlin-js-store/

geary-benchmarks/.results
/truckconfig.json
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ dependencies {
}
```

### Version catalog entries

```toml
[versions]
geary = "x.y.z"

[libraries]
geary-core = { module = "com.mineinabyss:geary-autoscan", version.ref = "geary" }
geary-actions = { module = "com.mineinabyss:geary-actions", version.ref = "geary" }
geary-autoscan = { module = "com.mineinabyss:geary-autoscan", version.ref = "geary" }
geary-prefabs = { module = "com.mineinabyss:geary-prefabs", version.ref = "geary" }
geary-serialization = { module = "com.mineinabyss:geary-serialization", version.ref = "geary" }
geary-uuid = { module = "com.mineinabyss:geary-uuid", version.ref = "geary" }
```

## Roadmap

As the project matures, our primary goal is to make it useful to more people. Here are a handful of features we hope to achieve:
Expand Down
31 changes: 31 additions & 0 deletions addons/geary-actions/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
plugins {
id(idofrontLibs.plugins.mia.kotlin.multiplatform.get().pluginId)
id(idofrontLibs.plugins.mia.publication.get().pluginId)
alias(idofrontLibs.plugins.kotlinx.serialization)
}

kotlin {
sourceSets {
val commonMain by getting {
dependencies {
implementation(project(":geary-core"))
implementation(project(":geary-serialization"))

implementation(libs.uuid)
implementation(idofrontLibs.idofront.di)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation(idofrontLibs.kotlinx.coroutines.test)
implementation(idofrontLibs.kotlinx.serialization.kaml)
implementation(idofrontLibs.kotest.assertions)
implementation(idofrontLibs.kotest.property)
implementation(idofrontLibs.idofront.di)
implementation(project(":geary-core"))
implementation(project(":geary-serialization"))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mineinabyss.geary.actions

import kotlin.jvm.JvmName

interface Action {
/** Should this action create a copy of [ActionGroupContext] to run or not? */
val useSubcontext: Boolean get() = true

fun ActionGroupContext.execute(): Any?

}

fun Action.execute(context: ActionGroupContext) = context.execute()
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.mineinabyss.geary.actions

import com.mineinabyss.geary.actions.actions.EmitEventAction
import com.mineinabyss.geary.actions.actions.EnsureAction
import com.mineinabyss.geary.actions.event_binds.*
import com.mineinabyss.geary.actions.expressions.Expression
import com.mineinabyss.geary.modules.geary
import com.mineinabyss.geary.serialization.serializers.InnerSerializer
import com.mineinabyss.geary.serialization.serializers.PolymorphicListAsMapSerializer
import com.mineinabyss.geary.serialization.serializers.SerializedComponents
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer

class ActionEntry(
val action: Action,
val conditions: List<EnsureAction>?,
val register: String?,
val onFail: ActionGroup?,
val loop: Expression<List<Any>>?,
val environment: Map<String, Expression<Any>>?,
)

@Serializable(with = ActionGroup.Serializer::class)
class ActionGroup(
val actions: List<ActionEntry>,
) : Action {
override fun ActionGroupContext.execute() {
actions.forEach { entry ->
val context = if (entry.action.useSubcontext)
entry.environment?.let { env -> this.plus(env.mapValues { eval(it.value) }) } ?: this
else this
try {
if (entry.loop != null) {
entry.loop.evaluate(context).forEach { loopEntry ->
val subcontext = context.copy()
subcontext.register("it", loopEntry)
executeEntry(subcontext, entry)
}
} else
executeEntry(context, entry)
} catch (e: ActionsCancelledException) {
entry.onFail?.execute(context)
return
}
}
}

private fun executeEntry(context: ActionGroupContext, entry: ActionEntry) {
entry.conditions?.forEach { condition ->
condition.execute(context)
}

val returned = entry.action.execute(context)

if (entry.register != null)
context.register(entry.register, returned)
}

class Serializer : InnerSerializer<List<SerializedComponents>, ActionGroup>(
serialName = "geary:action_group",
inner = ListSerializer(
PolymorphicListAsMapSerializer.ofComponents(
PolymorphicListAsMapSerializer.Config(
customKeys = mapOf(
"when" to { ActionWhen.serializer() },
"register" to { ActionRegister.serializer() },
"onFail" to { ActionOnFail.serializer() },
"loop" to { ActionLoop.serializer() }
)
)
)
),
inverseTransform = { TODO() },
transform = {
val actions = it.mapNotNull { components ->
var action: Action? = null
var condition: List<EnsureAction>? = null
var register: String? = null
var loop: Expression<List<Any>>? = null
var onFail: ActionGroup? = null
var environment: Map<String, Expression<Any>>? = null
components.forEach { comp ->
when {
comp is ActionWhen -> condition = comp.conditions
comp is ActionRegister -> register = comp.register
comp is ActionOnFail -> onFail = comp.action
comp is ActionLoop -> loop =
Expression.parseExpression(comp.expression, serializersModule) as Expression<List<Any>>

comp is ActionEnvironment -> environment = comp.environment
action != null -> geary.logger.w { "Multiple actions defined in one block!" }
else -> action = EmitEventAction.wrapIfNotAction(comp)
}
}
if (action == null) return@mapNotNull null
ActionEntry(
action = action!!,
conditions = condition,
register = register,
onFail = onFail,
loop = loop,
environment = environment
)
}
ActionGroup(actions)
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.mineinabyss.geary.actions

import com.mineinabyss.geary.actions.expressions.Expression
import com.mineinabyss.geary.datatypes.GearyEntity

class ActionGroupContext() {
constructor(entity: GearyEntity) : this() {
this.entity = entity
}

var entity: GearyEntity?
get() = environment["entity"] as? GearyEntity
set(value) {
environment["entity"] = value
}

val environment: MutableMap<String, Any?> = mutableMapOf()

fun <T> eval(expression: Expression<T>): T = expression.evaluate(this)

fun register(name: String, value: Any?) {
environment[name] = value
}

fun copy(): ActionGroupContext {
val newContext = ActionGroupContext()
newContext.environment.putAll(environment)
return newContext
}

fun plus(newEnvironment: Map<String, Any?>): ActionGroupContext {
val newContext = copy()
newContext.environment.putAll(newEnvironment)
return newContext
}

companion object
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.mineinabyss.geary.actions

class ActionsCancelledException : Exception()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.mineinabyss.geary.actions

interface Condition {
fun ActionGroupContext.execute(): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.mineinabyss.geary.actions

import com.mineinabyss.geary.actions.event_binds.bindEntityObservers
import com.mineinabyss.geary.actions.event_binds.parsePassive
import com.mineinabyss.geary.addons.dsl.GearyAddonWithDefault
import com.mineinabyss.geary.modules.geary

class GearyActions {
companion object : GearyAddonWithDefault<GearyActions> {
override fun default() = GearyActions()

override fun GearyActions.install() {
geary.run {
bindEntityObservers()
parsePassive()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.mineinabyss.geary.actions.actions

import com.mineinabyss.geary.actions.Action
import com.mineinabyss.geary.actions.ActionGroupContext
import com.mineinabyss.geary.actions.expressions.EntityExpression
import com.mineinabyss.geary.serialization.serializers.InnerSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable(with = BecomeAction.Serializer::class)
@SerialName("geary:become")
class BecomeAction(
val become: EntityExpression,
) : Action {
override val useSubcontext: Boolean = false

override fun ActionGroupContext.execute() {
entity = become.evaluate(this)
}

object Serializer : InnerSerializer<EntityExpression, BecomeAction>(
serialName = "geary:become",
inner = EntityExpression.serializer(),
inverseTransform = { it.become },
transform = { BecomeAction(it) }
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.mineinabyss.geary.actions.actions

import com.mineinabyss.geary.actions.Action
import com.mineinabyss.geary.actions.ActionGroupContext
import com.mineinabyss.geary.datatypes.ComponentId
import com.mineinabyss.geary.helpers.componentId

class EmitEventAction(
val eventId: ComponentId,
val data: Any?,
) : Action {
override fun ActionGroupContext.execute() {
entity?.emit(event = eventId, data = data)
}

companion object {
fun from(data: Any) = EmitEventAction(componentId(data::class), data)

fun wrapIfNotAction(data: Any) = if (data is Action) data else from(data)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.mineinabyss.geary.actions.actions

import com.mineinabyss.geary.actions.*
import com.mineinabyss.geary.helpers.componentId
import com.mineinabyss.geary.serialization.serializers.InnerSerializer
import com.mineinabyss.geary.serialization.serializers.PolymorphicListAsMapSerializer
import com.mineinabyss.geary.serialization.serializers.SerializedComponents
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

@Serializable(with = EnsureAction.Serializer::class)
class EnsureAction(
val conditions: SerializedComponents,
) : Action {
@Transient
private val flat = conditions.map { componentId(it::class) to it }

override fun ActionGroupContext.execute() {
flat.forEach { (id, data) ->
when (data) {
is Condition -> with(data) {
if (!execute()) {
throw ActionsCancelledException()
}
}

else -> entity?.emit(id, data) //TODO use geary condition system if we get one
}
}
}

fun conditionsMet(context: ActionGroupContext): Boolean {
try {
execute(context)
} catch (e: ActionsCancelledException) {
return false
}
return true
}

object Serializer : InnerSerializer<SerializedComponents, EnsureAction>(
serialName = "geary:ensure",
inner = PolymorphicListAsMapSerializer.ofComponents(),
inverseTransform = { it.conditions },
transform = { EnsureAction(it) }
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.mineinabyss.geary.actions.actions

import com.mineinabyss.geary.actions.Action
import com.mineinabyss.geary.actions.ActionGroupContext
import com.mineinabyss.geary.actions.expressions.Expression
import com.mineinabyss.geary.actions.expressions.InlineExpressionSerializer
import com.mineinabyss.geary.serialization.serializers.InnerSerializer
import kotlinx.serialization.Serializable

@Serializable(with = EvalAction.Serializer::class)
class EvalAction(
val expression: Expression<*>,
) : Action {
override fun ActionGroupContext.execute() =
expression.evaluate(this)

object Serializer : InnerSerializer<Expression<*>, EvalAction>(
serialName = "geary:eval",
inner = InlineExpressionSerializer,
inverseTransform = { it.expression },
transform = { EvalAction(it) }
)
}
Loading
Loading