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

Ability to add systems to the world after world creation #145

Merged
merged 8 commits into from
Jul 5, 2024

Conversation

RefuX
Copy link
Contributor

@RefuX RefuX commented Jul 2, 2024

I changed world.system to return a List instead on an Array, so while it didn't impact any tests, I guess its possible some users are expecting an Array?

I ran the benchmarks before and after changes and results are basically identical.

I added a constructor to IntervalSystem (to match old signature), as it was breaking some js/wasm tests otherwise.

Please review and give feedback.

@Quillraven
Copy link
Owner

Thank you! I will try to have a look by tomorrow and give you feedback :)

@Quillraven Quillraven added this to the 2.9 milestone Jul 2, 2024
@Quillraven Quillraven added the enhancement New feature or request label Jul 2, 2024
@Quillraven
Copy link
Owner

Had a quick check on my phone. In general looks good to me but need to have a more detailed look. Two things that I can already tell:

  • why is _systems and systems necessary? Also, why is it still a var instead of a val? I think internal val MutableList should be sufficient? The world config can simply add to the list instead of setting it directly. Fyi in an upcoming version of kotlin we will get a special backing field declaration to avoid the _systems trick that you are currently using. Just an fyi 😊
  • I think the special functionality that automatically adds a family hook needs to be updated. First, I think adding a system then removing it and then adding it again will call the hook logic twice. Second, if a system gets removed we also need to remove its family hook logic. Here I am not even sure how easy that will be.

@RefuX
Copy link
Contributor Author

RefuX commented Jul 3, 2024

Updated with review feedback 👍

Copy link
Owner

@Quillraven Quillraven left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if you need help with the setUpAggregatedFamilyHooks part. Other than that looks great with just some minor change requests.

Again, thank you for your time and support!

@@ -175,7 +175,7 @@ class WorldConfiguration(@PublishedApi internal val world: World) {
systemCfg?.invoke(it)
// assign world systems afterward to resize the systems array only once to the correct size
// instead of resizing every time a system gets added to the configuration
world.systems = it.systems.toTypedArray()
world.systems = it.systems
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't use an array anymore we can directly add the system to world.systems. I think a separate world configuration system list is no longer necessary. Also, the comment above is no longer valid and can be removed.

With that change you should be able to avoid _systems and systems in the world and also replace var with val.

*
* @throws FleksSystemAlreadyAddedException if the system was already added before.
*/
fun add(system: IntervalSystem, index: Int? = null) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would switch the argument because for normal list operations where you add an element at a specific index, the index parameter is first like in:

val numbers = mutableListOf("one", "five", "six")
numbers.add(1, "two")

To have a separate method without the index that simply adds at the end, I would go for a second method. To reduce code it could be something like this:

fun add(index: Int, system: IntervalSystem) {
  // do main logic here
}

fun add(system: IntervalSystem) = add(systems.size -1, system)

systems.reversed().forEach { it.onDispose() }
}

internal fun setUpAggregatedFamilyHooks(systems: List<IntervalSystem>) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this functionality is not properly working anymore with the new change. It was a very special feature requested by some other user. To explain it:

  • In Fleks you can have family hooks to react when an entity gets added/removed from a family
  • To make life easier we added a FamilyOnAdd and FamilyOnRemove interface that can be added to an Iteratingsystem and then Fleks will automatically register those hooks to the family
  • Since there is just one hook on a family we then simply combined all system hook functions into a single one and register that special function as a hook to the family
  • One additional point that makes it a little complex is that you can also register your own family hook in the world configuration already (that's the ownHook in the code below. The final hook is then the family original hook + all systems' hooks

Before, this was just necessary once because systems could not be modified after world creation. Now, imo we need to do that all the time when a system gets added or removed.

However, it is not necessary to iterate over all systems all the time and create the hooks again. My idea is:

  • When adding/removing a system check if it implements the FamilyOnAdd or FamilyOnRemove interface
  • If it does then call this method or a new one which does the following:
    • get all systems that have the same family in their current order
    • update the family hook with the already existing logic
    • This will add/remove the newly added/removed system's hooks

However, there is one issue with the ownHook of the family. I think we don't have that anymore and it is currently impossible to retrieve it from somewhere. Needs to be checked but I am pretty sure that that's the case, unfortunately :(

For that I see two solutions:

  • store the own hook of the family, that gets created in the world configuration in a separate place somewhere where we can then use it for the logic above (e.g. directly in the family class)
    • The downside is that we introduce an additional field per family which increases memory consumption but I guess it is not huge
    • This could maybe be solved more efficiently if we e.g. use a ownHookCache in the world directly. When we retrieve a family`s ownHook for the first time we could read this cache. If there is an entry then use it and if there is no entry then add it. That way we don't add an additional field to every family.
  • instead of just having one hook on a family just make it a list or an array. That way we don't need to override the hook at all and just add/remove system hooks.
    • This is however inconsistent with the other hooks in Fleks

@@ -326,8 +278,55 @@ class World internal constructor(
/**
* Returns the world's systems.
*/
var systems = emptyArray<IntervalSystem>()
internal set
private var _systems = mutableListOf<IntervalSystem>()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot remember anymore but I think mutableListOf creates a LinkedHashList or something. I don't know if the iteration over systems is then with an iterator or not.

We need to check that because otherwise we create a lot of iterator instances and trigger the garbage collector.

If possible, we should maybe just use an ArrayList here. I think that has the least overhead and iteration should be a simple one without iterator.

@Quillraven
Copy link
Owner

Updated with review feedback 👍

haha unlucky timing, I just did the detailed review in parallel. Will have another look this afternoon :)

@RefuX
Copy link
Contributor Author

RefuX commented Jul 3, 2024

Resolved the own hook conundrum using your first approach. Let me know if you like how it looks.

@Quillraven
Copy link
Owner

Quillraven commented Jul 3, 2024

Looks better now but the hook stuff is still not correct. It is a little bit confusing because it is a single hook from a family point of view but actually multiple things can be called. Here is a scenario that describes it better:

  • you can add a family hook during world configuration. Let's call that worldCfgHook. It is simply a function that takes an entity and does something
  • you can then also add a FamilyOnAdd interface to system A that iterates over the same family of the previous step. Let's call that systemAHook.
  • let's do the same thing again for system B and call it systemBHook

The special setUpAggregatedFamilyHooks function is then combining all three hooks into a single function. The new function executes during an add "trigger" in following order:

  • worldCfgHook
  • systemAHook
  • systemBHook

For removal it is the same but in reversed order.

If you now remove system A of the world then the hook of the family remains but the new execution will be:

  • worldCfgHook
  • systemBHook

systemAHook logic needs to be removed. When adding system A again, it needs to become part of the single hook function again depending on where the system was added.

I understand if this is a little complex for someone who does not know about that special use-case and all the details ;) I think this is also not covered by any of the test cases because otherwise you would already have failing tests.

I will have some time on the weekend and can help you with that. But of course, if you feel motivated to do it on your own then also feel free to do so :)


Everything else looks fine from my side now and can be merged. We are just missing the proper logic for this special aggregate hook stuff.

@Quillraven
Copy link
Owner

@RefuX:

had a little bit of time this morning. In theory the code should work but needs to be verified in a proper test case. Please have a look as well:

  1. I removed the ownAddHook and ownRemoveHook from the family again
  2. WorldConfiguration is then directly setting again the addHook / removeHook
  3. I split the setUpAggregatedFamilyHooks method into two methods. The first one is initAggregatedFamilyHooks which gets called after world configuration (it is basically the same as it was before). The second one is a new updateAggregatedFamilyHooks method that only updates the add/remove hook of a single family. It gets called when a system is added/removed and the system has those special interfaces.
  4. I introduced two "lazy caches" to remember the original world configuration hooks. I think this should be the best option from a memory consumption point of view.

The two lazy caches. I hope mutableMapOf supports null values:

    /**
     * Map of add [FamilyHook] out of the [WorldConfiguration].
     * Only used if there are also aggregated system hooks for the family to remember
     * its original world configuration hook (see [initAggregatedFamilyHooks] and [updateAggregatedFamilyHooks]).
     */
    private val worldCfgFamilyAddHooks = mutableMapOf<Family, FamilyHook?>()

    /**
     * Map of remove [FamilyHook] out of the [WorldConfiguration].
     * Only used if there are also aggregated system hooks for the family to remember
     * its original world configuration hook (see [initAggregatedFamilyHooks] and [updateAggregatedFamilyHooks]).
     */
    private val worldCfgFamilyRemoveHooks = mutableMapOf<Family, FamilyHook?>()

Updated system add:

    /**
     * Adds a new system to the world.
     *
     * @param index The position at which the system should be inserted in the list of systems. If null, the system is added at the end of the list.
     * This parameter is optional and defaults to null.
     * @param system The system to be added to the world. This should be an instance of a class that extends IntervalSystem.
     *
     * @throws FleksSystemAlreadyAddedException if the system was already added before.
     */
    fun add(index: Int, system: IntervalSystem) {
        if (systems.any { it::class == system::class }) {
            throw FleksSystemAlreadyAddedException(system::class)
        }
        if (system is IteratingSystem && (system is FamilyOnAdd || system is FamilyOnRemove)) {
            updateAggregatedFamilyHooks(system.family)
        }

        mutableSystems.add(index, system)
    }

Updated system remove:

    /**
     * Removes the specified system from the world.
     *
     * @param system The system to be removed from the world. This should be an instance of a class that extends IntervalSystem.
     * @return True if the system was successfully removed, false otherwise.
     */
    fun remove(system: IntervalSystem) {
        mutableSystems.remove(system)
        if (system is IteratingSystem && (system is FamilyOnAdd || system is FamilyOnRemove)) {
            updateAggregatedFamilyHooks(system.family)
        }
    }

The updated init aggregated hooks and update aggregated hooks:

    /**
     * Extend [Family.addHook] and [Family.removeHook] for all
     * [systems][IteratingSystem] that implement [FamilyOnAdd] and/or [FamilyOnRemove].
     */
    internal fun initAggregatedFamilyHooks() {
        // validate systems against illegal interfaces
        systems.forEach { system ->
            // FamilyOnAdd and FamilyOnRemove interfaces are only meant to be used by IteratingSystem
            if (system !is IteratingSystem) {
                if (system is FamilyOnAdd) {
                    throw FleksWrongSystemInterfaceException(system::class, FamilyOnAdd::class)
                }
                if (system is FamilyOnRemove) {
                    throw FleksWrongSystemInterfaceException(system::class, FamilyOnRemove::class)
                }
            }
        }

        // register family hooks for IteratingSystem.FamilyOnAdd containing systems
        systems
            .mapNotNull { if (it is IteratingSystem && it is FamilyOnAdd) it else null }
            .groupBy { it.family }
            .forEach { entry ->
                val (family, systemList) = entry
                val ownHook = worldCfgFamilyAddHooks.getOrPut(family) { family.addHook }
                family.addHook = if (ownHook != null) { entity ->
                    ownHook(this, entity)
                    systemList.forEach { it.onAddEntity(entity) }
                } else { entity ->
                    systemList.forEach { it.onAddEntity(entity) }
                }
            }

        // register family hooks for IteratingSystem.FamilyOnRemove containing systems
        systems
            .mapNotNull { if (it is IteratingSystem && it is FamilyOnRemove) it else null }
            .groupBy { it.family }
            .forEach { entry ->
                val (family, systemList) = entry
                val ownHook = worldCfgFamilyRemoveHooks.getOrPut(family) { family.removeHook as FamilyHook }
                family.removeHook = if (ownHook != null) { entity ->
                    systemList.forEachReverse { it.onRemoveEntity(entity) }
                    ownHook(this, entity)
                } else { entity ->
                    systemList.forEachReverse { it.onRemoveEntity(entity) }
                }
            }
    }

    /**
     * Update [Family.addHook] and [Family.removeHook] for all
     * [systems][IteratingSystem] that implement [FamilyOnAdd] and/or [FamilyOnRemove]
     * and iterate over the given [family].
     */
    private fun updateAggregatedFamilyHooks(family: Family) {
        // system validation like in initAggregatedFamilyHooks is not necessary
        // because it is already validated before (in initAggregatedFamilyHooks and in add/remove system)

        // update family add hook by adding systems' onAddEntity calls after its original world cfg hook
        val ownAddHook = worldCfgFamilyAddHooks.getOrPut(family) { family.addHook }
        val addSystems = systems.filter { it is IteratingSystem && it is FamilyOnAdd && it.family == family }
        family.addHook = if (ownAddHook != null) { entity ->
            ownAddHook(this, entity)
            addSystems.forEach { (it as FamilyOnAdd).onAddEntity(entity) }
        } else { entity ->
            addSystems.forEach { (it as FamilyOnAdd).onAddEntity(entity) }
        }

        // update family remove hook by adding systems' onRemoveEntity calls before its original world cfg hook
        val ownRemoveHook = worldCfgFamilyRemoveHooks.getOrPut(family) { family.removeHook }
        val removeSystems = systems.filter { it is IteratingSystem && it is FamilyOnRemove && it.family == family }
        family.removeHook = if (ownRemoveHook != null) { entity ->
            removeSystems.forEach { (it as FamilyOnRemove).onRemoveEntity(entity) }
            ownRemoveHook(this, entity)
        } else { entity ->
            removeSystems.forEach { (it as FamilyOnRemove).onRemoveEntity(entity) }
        }
    }

Complete family.kt file
package com.github.quillraven.fleks

import com.github.quillraven.fleks.collection.*

/**
 * Type alias for an optional hook function for a [Family].
 * Such a function runs within a [World] and takes the [Entity] as an argument.
 */
typealias FamilyHook = World.(Entity) -> Unit

/**
 * A class to define the configuration of a [Family]. A [family][Family] contains of three parts:
 *
 * - **allOf**: an [entity][Entity] must have all specified [components][Component] to be part of the [family][Family].
 * - **noneOf**: an [entity][Entity] must not have any of the specified [components][Component] to be part of the [family][Family].
 * - **anyOf**: an [entity][Entity] must have at least one of the specified [components][Component] to be part of the [family][Family].
 *
 * It is not mandatory to specify all three parts but **at least one** part must be provided.
 */
data class FamilyDefinition(
    internal var allOf: BitArray? = null,
    internal var noneOf: BitArray? = null,
    internal var anyOf: BitArray? = null,
) {

    /**
     * Any [entity][Entity] must have all given [types] to be part of the [family][Family].
     */
    fun all(vararg types: UniqueId<*>): FamilyDefinition {
        allOf = BitArray(types.size).also { bits ->
            types.forEach { bits.set(it.id) }
        }
        return this
    }

    /**
     * Any [entity][Entity] must not have any of the given [types] to be part of the [family][Family].
     */
    fun none(vararg types: UniqueId<*>): FamilyDefinition {
        noneOf = BitArray(types.size).also { bits ->
            types.forEach { bits.set(it.id) }
        }
        return this
    }

    /**
     * Any [entity][Entity] must have at least one of the given [types] to be part of the [family][Family].
     */
    fun any(vararg types: UniqueId<*>): FamilyDefinition {
        anyOf = BitArray(types.size).also { bits ->
            types.forEach { bits.set(it.id) }
        }
        return this
    }

    /**
     * Returns true if and only if [allOf], [noneOf] and [anyOf] are either null or empty.
     */
    internal fun isEmpty(): Boolean {
        return allOf.isNullOrEmpty() && noneOf.isNullOrEmpty() && anyOf.isNullOrEmpty()
    }
}

/**
 * A family of [entities][Entity]. It stores [entities][Entity] that have a specific configuration of components.
 * A configuration is defined via the a [FamilyDefinition].
 * Each [component][Component] is assigned to a unique index via its [ComponentType].
 * That index is set in the [allOf], [noneOf] or [anyOf][] [BitArray].
 *
 * A family gets notified when an [entity][Entity] is added, updated or removed of the [world][World].
 *
 * Every [IteratingSystem] is linked to exactly one family but a family can also exist outside of systems.
 * It gets created via the [World.family] function.
 */
data class Family(
    internal val allOf: BitArray? = null,
    internal val noneOf: BitArray? = null,
    internal val anyOf: BitArray? = null,
    private val world: World,
    @PublishedApi
    internal val entityService: EntityService = world.entityService,
) : EntityComponentContext(world.componentService) {

    /**
     * An optional [FamilyHook] that gets called whenever an [entity][Entity] enters the family.
     */
    internal var addHook: FamilyHook? = null

    /**
     * An optional [FamilyHook] that gets called whenever an [entity][Entity] leaves the family.
     */
    internal var removeHook: FamilyHook? = null

    /**
     * Returns the [entities][Entity] that belong to this family.
     */
    private val activeEntities = bag<Entity>(world.capacity)
    private var countEntities = 0

    /**
     * Returns true if an iteration of this family is currently in process.
     */
    @PublishedApi
    internal var isIterating = false

    // This bag is added for better iteration performance.
    @PublishedApi
    internal val mutableEntities = MutableEntityBag()
        get() {
            if (isDirty && !isIterating) {
                // no iteration in process -> update entities if necessary
                isDirty = false
                field.clearEnsuringCapacity(activeEntities.size)
                activeEntities.forEach { field += it }
            }
            return field
        }

    /**
     * Returns the [entities][Entity] that belong to this family.
     * Be aware that the underlying [EntityBag] collection is not always up to date.
     * The collection is not updated while a family iteration is in progress. It
     * gets automatically updated whenever it is accessed and no iteration is currently
     * in progress.
     */
    val entities: EntityBag
        get() = mutableEntities

    /**
     * Returns the number of [entities][Entity] that belong to this family.
     */
    val numEntities: Int
        get() = countEntities

    /**
     * Returns true if and only if this [Family] does not contain any entity.
     */
    val isEmpty: Boolean
        get() = countEntities == 0

    /**
     * Returns true if and only if this [Family] contains at least one entity.
     */
    val isNotEmpty: Boolean
        get() = countEntities > 0

    /**
     * Flag to indicate if there are changes in the [activeEntities].
     * If it is true then the [mutableEntities] will get updated the next time it is accessed.
     */
    private var isDirty = false

    /**
     * Returns true if the specified [compMask] matches the family's component configuration.
     *
     * @param compMask the component configuration of an [entity][Entity].
     */
    internal operator fun contains(compMask: BitArray): Boolean {
        return (allOf == null || compMask.contains(allOf))
            && (noneOf == null || !compMask.intersects(noneOf))
            && (anyOf == null || compMask.intersects(anyOf))
    }

    /**
     * Returns true if and only if the given [entity] is part of the family.
     */
    operator fun contains(entity: Entity): Boolean = activeEntities.hasValueAtIndex(entity.id)

    /**
     * Updates this family if needed and runs the given [action] for all [entities][Entity].
     *
     * **Important note**: There is a potential risk when iterating over entities and one of those entities
     * gets removed. Removing the entity immediately and cleaning up its components could
     * cause problems because if you access a component which is mandatory for the family, you will get
     * a FleksNoSuchComponentException. To avoid that you could check if an entity really has the component
     * before accessing it but that is redundant in context of a family.
     *
     * To avoid these kinds of issues, entity removals are delayed until the end of the iteration. This also means
     * that a removed entity of this family will still be part of the [action] for the current iteration.
     */
    inline fun forEach(crossinline action: Family.(Entity) -> Unit) {
        // Access entities before 'forEach' call to properly update them.
        // Check mutableEntities getter for more details.
        val entitiesForIteration = mutableEntities

        if (!entityService.delayRemoval) {
            entityService.delayRemoval = true
            isIterating = true
            entitiesForIteration.forEach { action(it) }
            isIterating = false
            entityService.cleanupDelays()
        } else {
            val origIterating = isIterating
            isIterating = true
            entitiesForIteration.forEach { this.action(it) }
            isIterating = origIterating
        }
    }

    /**
     * Updates this family if needed and returns its first [Entity].
     * @throws [NoSuchElementException] if the family has no entities.
     */
    fun first(): Entity = mutableEntities.first()

    /**
     * Updates this family if needed and returns its first [Entity] or null if the family has no entities.
     */
    fun firstOrNull(): Entity? = mutableEntities.firstOrNull()

    /**
     * Sorts the [entities][Entity] of this family by the given [comparator].
     */
    fun sort(comparator: EntityComparator) = mutableEntities.sort(comparator)

    /**
     * Adds the [entity] to the family and sets the [isDirty] flag if and only
     * if the entity's [compMask] is matching the family configuration.
     */
    @PublishedApi
    internal fun onEntityAdded(entity: Entity, compMask: BitArray) {
        if (compMask in this) {
            isDirty = true
            if (activeEntities.hasNoValueAtIndex(entity.id)) countEntities++
            activeEntities[entity.id] = entity
            addHook?.invoke(world, entity)
        }
    }

    /**
     * Checks if the [entity] is part of the family by analyzing the entity's components.
     * The [compMask] is a [BitArray] that indicates which components the [entity] currently has.
     *
     * The [entity] gets either added to the [activeEntities] or removed and [isDirty] is set when needed.
     */
    @PublishedApi
    internal fun onEntityCfgChanged(entity: Entity, compMask: BitArray) {
        val entityInFamily = compMask in this
        val currentEntity = activeEntities.getOrNull(entity.id)
        if (entityInFamily && currentEntity == null) {
            // new entity gets added
            isDirty = true
            countEntities++
            activeEntities[entity.id] = entity
            addHook?.invoke(world, entity)
        } else if (!entityInFamily && currentEntity != null) {
            // existing entity gets removed
            isDirty = true
            countEntities--
            activeEntities.removeAt(entity.id)
            removeHook?.invoke(world, entity)
        }
    }

    /**
     * Removes the [entity] of the family and sets the [isDirty] flag if and only
     * if the [entity] is already in the family.
     */
    internal fun onEntityRemoved(entity: Entity) {
        if (activeEntities.hasValueAtIndex(entity.id)) {
            // existing entity gets removed
            isDirty = true
            activeEntities.removeAt(entity.id)
            countEntities--
            removeHook?.invoke(world, entity)
        }
    }

    override fun toString(): String {
        return "Family(allOf=$allOf, noneOf=$noneOf, anyOf=$anyOf, numEntities=$numEntities)"
    }
}
Complete world.kt file
package com.github.quillraven.fleks

import com.github.quillraven.fleks.World.Companion.CURRENT_WORLD
import com.github.quillraven.fleks.collection.EntityBag
import com.github.quillraven.fleks.collection.MutableEntityBag
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlin.native.concurrent.ThreadLocal
import kotlin.reflect.KClass

/**
 * DSL marker for the [WorldConfiguration].
 */
@DslMarker
annotation class WorldCfgMarker

/**
 * Wrapper class for injectables of the [WorldConfiguration].
 * It is used to identify unused injectables after [world][World] creation.
 */
data class Injectable(val injObj: Any, var used: Boolean = false)

/**
 * A DSL class to configure [Injectable] of a [WorldConfiguration].
 */
@WorldCfgMarker
class InjectableConfiguration(private val world: World) {

    /**
     * Adds the specified [dependency] under the given [name] which
     * can then be injected via [World.inject].
     *
     * @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before.
     */
    fun <T : Any> add(name: String, dependency: T) {
        if (name in world.injectables) {
            throw FleksInjectableAlreadyAddedException(name)
        }

        world.injectables[name] = Injectable(dependency)
    }

    /**
     * Adds the specified [dependency] via its [simpleName][KClass.simpleName],
     * or via its [toString][KClass.toString] if it has no name.
     * It can then be injected via [World.inject].
     *
     * @throws [FleksInjectableAlreadyAddedException] if the dependency was already added before.
     */
    inline fun <reified T : Any> add(dependency: T) = add(T::class.simpleName ?: T::class.toString(), dependency)
}

/**
 * A DSL class to configure [IntervalSystem] of a [WorldConfiguration].
 */
@WorldCfgMarker
class SystemConfiguration(
    private val systems: MutableList<IntervalSystem>
) {
    /**
     * Adds the [system] to the [world][World].
     * The order in which systems are added is the order in which they will be executed when calling [World.update].
     *
     * @throws [FleksSystemAlreadyAddedException] if the system was already added before.
     */
    fun add(system: IntervalSystem) {
        if (systems.any { it::class == system::class }) {
            throw FleksSystemAlreadyAddedException(system::class)
        }
        systems += system
    }
}

/**
 * A DSL class to configure [FamilyHook] for specific [families][Family].
 */
@WorldCfgMarker
class FamilyConfiguration(
    @PublishedApi
    internal val world: World,
) {

    /**
     * Sets the add [hook][Family.addHook] for the given [family].
     * This hook gets called whenever an [entity][Entity] enters the [family].
     */
    fun onAdd(
        family: Family,
        hook: FamilyHook
    ) {
        if (family.addHook != null) {
            throw FleksHookAlreadyAddedException("addHook", "Family $family")
        }
        family.addHook = hook
    }

    /**
     * Sets the remove [hook][Family.removeHook] for the given [family].
     * This hook gets called whenever an [entity][Entity] leaves the [family].
     */
    fun onRemove(
        family: Family,
        hook: FamilyHook
    ) {
        if (family.removeHook != null) {
            throw FleksHookAlreadyAddedException("removeHook", "Family $family")
        }
        family.removeHook = hook
    }
}

/**
 * A configuration for an entity [world][World] to define the systems, dependencies to be injected,
 * [component][Component]- and [family][Family] hooks.
 *
 * @param world the [World] to be configured.
 */
@WorldCfgMarker
class WorldConfiguration(@PublishedApi internal val world: World) {

    private var injectableCfg: (InjectableConfiguration.() -> Unit)? = null
    private var familyCfg: (FamilyConfiguration.() -> Unit)? = null
    private var systemCfg: (SystemConfiguration.() -> Unit)? = null

    fun injectables(cfg: InjectableConfiguration.() -> Unit) {
        injectableCfg = cfg
    }

    fun families(cfg: FamilyConfiguration.() -> Unit) {
        familyCfg = cfg
    }

    fun systems(cfg: SystemConfiguration.() -> Unit) {
        systemCfg = cfg
    }

    /**
     * Sets the add entity [hook][EntityService.addHook].
     * This hook gets called whenever an [entity][Entity] gets created and
     * after its [components][Component] are assigned and [families][Family] are updated.
     */
    fun onAddEntity(hook: EntityHook) {
        world.setEntityAddHook(hook)
    }

    /**
     * Sets the remove entity [hook][EntityService.removeHook].
     * This hook gets called whenever an [entity][Entity] gets removed and
     * before its [components][Component] are removed and [families][Family] are updated.
     */
    fun onRemoveEntity(hook: EntityHook) {
        world.setEntityRemoveHook(hook)
    }

    /**
     * Sets the [EntityProvider] for the [EntityService] by calling the [factory] function
     * within the context of a [World]. Per default the [DefaultEntityProvider] is used.
     */
    fun entityProvider(factory: World.() -> EntityProvider) {
        world.entityService.entityProvider = world.run(factory)
    }

    /**
     * Configures the world in following sequence:
     * - injectables
     * - family
     * - system
     *
     * The order is important to correctly trigger [FamilyHook]s and [EntityHook]s.
     */
    fun configure() {
        injectableCfg?.invoke(InjectableConfiguration(world))
        familyCfg?.invoke(FamilyConfiguration(world))
        SystemConfiguration(world.mutableSystems).also {
            systemCfg?.invoke(it)
        }

        if (world.numEntities > 0) {
            throw FleksWorldModificationDuringConfigurationException()
        }

        world.initAggregatedFamilyHooks()

        world.systems.forEach { it.onInit() }
    }

}

/**
 * Creates a new [world][World] with the given [cfg][WorldConfiguration].
 *
 * @param entityCapacity initial maximum entity capacity.
 * Will be used internally when a [world][World] is created to set the initial
 * size of some collections and to avoid slow resizing calls.
 *
 * @param cfg the [configuration][WorldConfiguration] of the world containing the [systems][IntervalSystem],
 * [injectables][Injectable] and [FamilyHook]s.
 */
fun configureWorld(entityCapacity: Int = 512, cfg: WorldConfiguration.() -> Unit): World {
    val newWorld = World(entityCapacity)
    CURRENT_WORLD = newWorld

    try {
        WorldConfiguration(newWorld).apply(cfg).configure()
    } finally {
        CURRENT_WORLD = null
    }

    return newWorld
}

/**
 * Snapshot for an [entity][Entity] that contains its [components][Component] and [tags][EntityTag].
 */
@Serializable
data class Snapshot(
    val components: List<Component<out @Contextual Any>>,
    val tags: List<UniqueId<out @Contextual Any>>,
)

/**
 * Utility function to manually create a [Snapshot].
 */
@Suppress("UNCHECKED_CAST")
fun wildcardSnapshotOf(components: List<Component<*>>, tags: List<UniqueId<*>>): Snapshot {
    return Snapshot(components as List<Component<out Any>>, tags as List<UniqueId<out Any>>)
}

/**
 * A world to handle [entities][Entity] and [systems][IntervalSystem].
 *
 * @param entityCapacity the initial maximum capacity of entities.
 */
class World internal constructor(
    entityCapacity: Int,
) : EntityComponentContext(ComponentService()) {
    @PublishedApi
    internal val injectables = mutableMapOf<String, Injectable>()

    /**
     * Returns the time that is passed to [update][World.update].
     * It represents the time in seconds between two frames.
     */
    var deltaTime = 0f
        private set

    @PublishedApi
    internal val entityService = EntityService(this, entityCapacity)

    /**
     * List of all [families][Family] of the world that are created either via
     * an [IteratingSystem] or via the world's [family] function to
     * avoid creating duplicates.
     */
    @PublishedApi
    internal var allFamilies = emptyArray<Family>()

    /**
     * Returns the amount of active entities.
     */
    val numEntities: Int
        get() = entityService.numEntities

    /**
     * Returns the maximum capacity of active entities.
     */
    val capacity: Int
        get() = entityService.capacity

    /**
     * Returns the world's systems.
     */
    internal val mutableSystems = arrayListOf<IntervalSystem>()
    val systems: List<IntervalSystem>
        get() = mutableSystems

    /**
     * Map of add [FamilyHook] out of the [WorldConfiguration].
     * Only used if there are also aggregated system hooks for the family to remember
     * its original world configuration hook (see [initAggregatedFamilyHooks] and [updateAggregatedFamilyHooks]).
     */
    private val worldCfgFamilyAddHooks = mutableMapOf<Family, FamilyHook?>()

    /**
     * Map of remove [FamilyHook] out of the [WorldConfiguration].
     * Only used if there are also aggregated system hooks for the family to remember
     * its original world configuration hook (see [initAggregatedFamilyHooks] and [updateAggregatedFamilyHooks]).
     */
    private val worldCfgFamilyRemoveHooks = mutableMapOf<Family, FamilyHook?>()

    /**
     * Adds a new system to the world.
     *
     * @param index The position at which the system should be inserted in the list of systems. If null, the system is added at the end of the list.
     * This parameter is optional and defaults to null.
     * @param system The system to be added to the world. This should be an instance of a class that extends IntervalSystem.
     *
     * @throws FleksSystemAlreadyAddedException if the system was already added before.
     */
    fun add(index: Int, system: IntervalSystem) {
        if (systems.any { it::class == system::class }) {
            throw FleksSystemAlreadyAddedException(system::class)
        }
        if (system is IteratingSystem && (system is FamilyOnAdd || system is FamilyOnRemove)) {
            updateAggregatedFamilyHooks(system.family)
        }

        mutableSystems.add(index, system)
    }

    /**
     * Adds a new system to the world.
     *
     * @param system The system to be added to the world. This should be an instance of a class that extends IntervalSystem.
     */
    fun add(system: IntervalSystem) = add(systems.size, system)

    /**
     * Removes the specified system from the world.
     *
     * @param system The system to be removed from the world. This should be an instance of a class that extends IntervalSystem.
     * @return True if the system was successfully removed, false otherwise.
     */
    fun remove(system: IntervalSystem) {
        mutableSystems.remove(system)
        if (system is IteratingSystem && (system is FamilyOnAdd || system is FamilyOnRemove)) {
            updateAggregatedFamilyHooks(system.family)
        }
    }

    /**
     * Adds a new system to the world using the '+=' operator.
     *
     * @param system The system to be added to the world. This should be an instance of a class that extends IntervalSystem.
     *
     * @throws FleksSystemAlreadyAddedException if the system was already added before.
     */
    operator fun plusAssign(system: IntervalSystem) = add(system)

    /**
     * Removes the specified system from the world using the '-=' operator.
     *
     * @param system The system to be removed from the world. This should be an instance of a class that extends IntervalSystem.
     */
    operator fun minusAssign(system: IntervalSystem) {
        remove(system)
    }

    /**
     * Cache of used [EntityTag] instances. Needed for snapshot functionality.
     */
    @PublishedApi
    internal val tagCache = mutableMapOf<Int, UniqueId<*>>()

    init {
        /**
         * Maybe because of design flaws, the world reference of the ComponentService must be
         * set in the world's constructor because the parent class (=EntityComponentContext) already
         * requires a ComponentService, and it is not possible to pass "this" reference directly.
         *
         * That's why it is happening here to set it as soon as possible.
         */
        componentService.world = this
    }

    /**
     * Returns an already registered injectable of the given [name] and marks it as used.
     *
     * @throws FleksNoSuchInjectableException if there is no injectable registered for [name].
     */
    inline fun <reified T> inject(name: String = T::class.simpleName ?: T::class.toString()): T {
        val injectable = injectables[name] ?: throw FleksNoSuchInjectableException(name)
        injectable.used = true
        return injectable.injObj as T
    }

    /**
     * Returns a new map of unused [injectables][Injectable]. An injectable gets set to 'used'
     * when it gets injected at least once via a call to [inject].
     */
    fun unusedInjectables(): Map<String, Any> =
        injectables.filterValues { !it.used }.mapValues { it.value.injObj }

    /**
     * Returns a new [EntityBag] instance containing all [entities][Entity] of the world.
     *
     * Do not call this operation each frame, as it can be expensive depending on the amount
     * of entities in your world.
     *
     * For frequent entity operations on specific entities, use [families][Family].
     */
    fun asEntityBag(): EntityBag {
        val result = MutableEntityBag(numEntities)
        entityService.forEach {
            result += it
        }
        return result
    }

    /**
     * Adds a new [entity][Entity] to the world using the given [configuration][EntityCreateContext].
     *
     * **Attention** Make sure that you only modify the entity of the current scope.
     * Otherwise, you will get wrong behavior for families. E.g. don't do this:
     *
     * ```
     * entity {
     *     // modifying the current entity is allowed ✅
     *     it += Position()
     *     // don't modify other entities ❌
     *     someOtherEntity += Position()
     * }
     * ```
     */
    inline fun entity(configuration: EntityCreateContext.(Entity) -> Unit = {}): Entity {
        return entityService.create(configuration)
    }

    /**
     * Returns true if and only if the [entity] is not removed and is part of the [World].
     */
    operator fun contains(entity: Entity) = entityService.contains(entity)

    /**
     * Removes the given [entity] from the world. The [entity] will be recycled and reused for
     * future calls to [World.entity].
     */
    operator fun minusAssign(entity: Entity) {
        entityService -= entity
    }

    /**
     * Removes all [entities][Entity] from the world. The entities will be recycled and reused for
     * future calls to [World.entity].
     * If [clearRecycled] is true then the recycled entities are cleared and the ids for newly
     * created entities start at 0 again.
     */
    fun removeAll(clearRecycled: Boolean = false) {
        entityService.removeAll(clearRecycled)
    }

    /**
     * Performs the given [action] on each active [entity][Entity].
     */
    fun forEach(action: World.(Entity) -> Unit) {
        entityService.forEach(action)
    }

    /**
     * Returns the specified [system][IntervalSystem].
     *
     * @throws [FleksNoSuchSystemException] if there is no such system.
     */
    inline fun <reified T : IntervalSystem> system(): T {
        systems.forEach { system ->
            if (system is T) {
                return system
            }
        }
        throw FleksNoSuchSystemException(T::class)
    }

    /**
     * Sets the [hook] as an [EntityService.addHook].
     *
     * @throws FleksHookAlreadyAddedException if the [EntityService] already has an add hook set.
     */
    @PublishedApi
    internal fun setEntityAddHook(hook: EntityHook) {
        if (entityService.addHook != null) {
            throw FleksHookAlreadyAddedException("addHook", "Entity")
        }
        entityService.addHook = hook
    }

    /**
     * Sets the [hook] as an [EntityService.removeHook].
     *
     * @throws FleksHookAlreadyAddedException if the [EntityService] already has a remove hook set.
     */
    @PublishedApi
    internal fun setEntityRemoveHook(hook: EntityHook) {
        if (entityService.removeHook != null) {
            throw FleksHookAlreadyAddedException("removeHook", "Entity")
        }
        entityService.removeHook = hook
    }

    /**
     * Creates a new [Family] for the given [cfg][FamilyDefinition].
     *
     * This function internally either creates or reuses an already existing [family][Family].
     * In case a new [family][Family] gets created it will be initialized with any already existing [entity][Entity]
     * that matches its configuration.
     * Therefore, this might have a performance impact on the first call if there are a lot of entities in the world.
     *
     * As a best practice families should be created as early as possible, ideally during world creation.
     * Also, store the result of this function instead of calling this function multiple times with the same arguments.
     *
     * @throws [FleksFamilyException] if the [FamilyDefinition] is null or empty.
     */
    fun family(cfg: FamilyDefinition.() -> Unit): Family = family(FamilyDefinition().apply(cfg))

    /**
     * Creates a new [Family] for the given [definition][FamilyDefinition].
     *
     * This function internally either creates or reuses an already existing [family][Family].
     * In case a new [family][Family] gets created it will be initialized with any already existing [entity][Entity]
     * that matches its configuration.
     * Therefore, this might have a performance impact on the first call if there are a lot of entities in the world.
     *
     * As a best practice families should be created as early as possible, ideally during world creation.
     * Also, store the result of this function instead of calling this function multiple times with the same arguments.
     *
     * @throws [FleksFamilyException] if the [FamilyDefinition] is null or empty.
     */
    @PublishedApi
    internal fun family(definition: FamilyDefinition): Family {
        if (definition.isEmpty()) {
            throw FleksFamilyException(definition)
        }

        val (defAll, defNone, defAny) = definition

        var family = allFamilies.find { it.allOf == defAll && it.noneOf == defNone && it.anyOf == defAny }
        if (family == null) {
            family = Family(defAll, defNone, defAny, this)
            allFamilies += family
            // initialize a newly created family by notifying it for any already existing entity
            // world.allFamilies.forEach { it.onEntityCfgChanged(entity, compMask) }
            entityService.forEach { family.onEntityCfgChanged(it, entityService.compMasks[it.id]) }
        }
        return family
    }

    /**
     * Returns a map that contains all [entities][Entity] and their components of this world.
     * The keys of the map are the entities.
     * The values are a list of components that a specific entity has. If the entity
     * does not have any components then the value is an empty list.
     */
    fun snapshot(): Map<Entity, Snapshot> {
        val result = mutableMapOf<Entity, Snapshot>()

        entityService.forEach { result[it] = snapshotOf(it) }

        return result
    }

    /**
     * Returns a list that contains all components of the given [entity] of this world.
     * If the entity does not have any components then an empty list is returned.
     */
    @Suppress("UNCHECKED_CAST")
    fun snapshotOf(entity: Entity): Snapshot {
        val comps = mutableListOf<Component<*>>()
        val tags = mutableListOf<UniqueId<*>>()

        if (entity in entityService) {
            entityService.compMasks[entity.id].forEachSetBit { cmpId ->
                val holder = componentService.holderByIndexOrNull(cmpId)
                if (holder == null) {
                    // tag instead of component
                    tags += tagCache[cmpId] ?: throw FleksSnapshotException("Tag with id $cmpId was never assigned")
                } else {
                    comps += holder[entity]
                }
            }
        }

        return Snapshot(comps as List<Component<out Any>>, tags as List<UniqueId<out Any>>)
    }

    /**
     * Loads the given [snapshot] of the world. This will first clear any existing
     * entity of the world. After that it will load all provided entities and components.
     * This will also execute [FamilyHook].
     *
     * @throws FleksSnapshotException if a family iteration is currently in process.
     */
    fun loadSnapshot(snapshot: Map<Entity, Snapshot>) {
        if (entityService.delayRemoval) {
            throw FleksSnapshotException("Snapshots cannot be loaded while a family iteration is in process")
        }

        // remove any existing entity and clean up recycled ids
        removeAll(true)
        if (snapshot.isEmpty()) {
            // snapshot is empty -> nothing to load
            return
        }

        val versionLookup = snapshot.keys.associateBy { it.id }

        // Set next entity id to the maximum provided id + 1.
        // All ids before that will be either created or added to the recycled
        // ids to guarantee that the provided snapshot entity ids match the newly created ones.
        with(entityService) {
            val maxId = snapshot.keys.maxOf { it.id }
            repeat(maxId + 1) {
                val entity = Entity(it, version = (versionLookup[it]?.version ?: 0u) - 1u)
                this.recycle(entity)
                val entitySnapshot = snapshot[versionLookup[it]]
                if (entitySnapshot != null) {
                    // snapshot for entity is provided -> create it
                    // note that the id for the entity will be the recycled id from above
                    this.configure(this.create { }, entitySnapshot)
                }
            }
        }
    }

    /**
     * Loads the given [entity] and its [snapshot][Snapshot].
     * If the entity does not exist yet, it will be created.
     * If the entity already exists it will be updated with the given components.
     *
     * @throws FleksSnapshotException if a family iteration is currently in process.
     */
    fun loadSnapshotOf(entity: Entity, snapshot: Snapshot) {
        if (entityService.delayRemoval) {
            throw FleksSnapshotException("Snapshots cannot be loaded while a family iteration is in process")
        }

        if (entity !in entityService) {
            // entity not part of service yet -> create it
            entityService.create(entity.id) { }
        }

        // load components for entity
        entityService.configure(entity, snapshot)
    }

    /**
     * Updates all [enabled][IntervalSystem.enabled] [systems][IntervalSystem] of the world
     * using the given [deltaTime].
     */
    fun update(deltaTime: Float) {
        this.deltaTime = deltaTime
        systems.forEach { system ->
            if (system.enabled) {
                system.onUpdate()
            }
        }
    }

    /**
     * Removes all [entities][Entity] of the world and calls the
     * [onDispose][IntervalSystem.onDispose] function of each system.
     */
    fun dispose() {
        entityService.removeAll()
        systems.reversed().forEach { it.onDispose() }
    }

    /**
     * Extend [Family.addHook] and [Family.removeHook] for all
     * [systems][IteratingSystem] that implement [FamilyOnAdd] and/or [FamilyOnRemove].
     */
    internal fun initAggregatedFamilyHooks() {
        // validate systems against illegal interfaces
        systems.forEach { system ->
            // FamilyOnAdd and FamilyOnRemove interfaces are only meant to be used by IteratingSystem
            if (system !is IteratingSystem) {
                if (system is FamilyOnAdd) {
                    throw FleksWrongSystemInterfaceException(system::class, FamilyOnAdd::class)
                }
                if (system is FamilyOnRemove) {
                    throw FleksWrongSystemInterfaceException(system::class, FamilyOnRemove::class)
                }
            }
        }

        // register family hooks for IteratingSystem.FamilyOnAdd containing systems
        systems
            .mapNotNull { if (it is IteratingSystem && it is FamilyOnAdd) it else null }
            .groupBy { it.family }
            .forEach { entry ->
                val (family, systemList) = entry
                val ownHook = worldCfgFamilyAddHooks.getOrPut(family) { family.addHook }
                family.addHook = if (ownHook != null) { entity ->
                    ownHook(this, entity)
                    systemList.forEach { it.onAddEntity(entity) }
                } else { entity ->
                    systemList.forEach { it.onAddEntity(entity) }
                }
            }

        // register family hooks for IteratingSystem.FamilyOnRemove containing systems
        systems
            .mapNotNull { if (it is IteratingSystem && it is FamilyOnRemove) it else null }
            .groupBy { it.family }
            .forEach { entry ->
                val (family, systemList) = entry
                val ownHook = worldCfgFamilyRemoveHooks.getOrPut(family) { family.removeHook as FamilyHook }
                family.removeHook = if (ownHook != null) { entity ->
                    systemList.forEachReverse { it.onRemoveEntity(entity) }
                    ownHook(this, entity)
                } else { entity ->
                    systemList.forEachReverse { it.onRemoveEntity(entity) }
                }
            }
    }

    /**
     * Update [Family.addHook] and [Family.removeHook] for all
     * [systems][IteratingSystem] that implement [FamilyOnAdd] and/or [FamilyOnRemove]
     * and iterate over the given [family].
     */
    private fun updateAggregatedFamilyHooks(family: Family) {
        // system validation like in initAggregatedFamilyHooks is not necessary
        // because it is already validated before (in initAggregatedFamilyHooks and in add/remove system)

        // update family add hook by adding systems' onAddEntity calls after its original world cfg hook
        val ownAddHook = worldCfgFamilyAddHooks.getOrPut(family) { family.addHook }
        val addSystems = systems.filter { it is IteratingSystem && it is FamilyOnAdd && it.family == family }
        family.addHook = if (ownAddHook != null) { entity ->
            ownAddHook(this, entity)
            addSystems.forEach { (it as FamilyOnAdd).onAddEntity(entity) }
        } else { entity ->
            addSystems.forEach { (it as FamilyOnAdd).onAddEntity(entity) }
        }

        // update family remove hook by adding systems' onRemoveEntity calls before its original world cfg hook
        val ownRemoveHook = worldCfgFamilyRemoveHooks.getOrPut(family) { family.removeHook }
        val removeSystems = systems.filter { it is IteratingSystem && it is FamilyOnRemove && it.family == family }
        family.removeHook = if (ownRemoveHook != null) { entity ->
            removeSystems.forEach { (it as FamilyOnRemove).onRemoveEntity(entity) }
            ownRemoveHook(this, entity)
        } else { entity ->
            removeSystems.forEach { (it as FamilyOnRemove).onRemoveEntity(entity) }
        }
    }

    @ThreadLocal
    companion object {
        @PublishedApi
        internal var CURRENT_WORLD: World? = null

        /**
         * Returns an already registered injectable of the given [name] and marks it as used.
         *
         * @throws FleksNoSuchInjectableException if there is no injectable registered for [name].
         * @throws FleksWrongConfigurationUsageException if called outside a [WorldConfiguration] scope.
         */
        inline fun <reified T> inject(name: String = T::class.simpleName ?: T::class.toString()): T =
            CURRENT_WORLD?.inject(name) ?: throw FleksWrongConfigurationUsageException()

        /**
         * Creates a new [Family] for the given [cfg][FamilyDefinition].
         *
         * This function internally either creates or reuses an already existing [family][Family].
         * In case a new [family][Family] gets created it will be initialized with any already existing [entity][Entity]
         * that matches its configuration.
         * Therefore, this might have a performance impact on the first call if there are a lot of entities in the world.
         *
         * As a best practice families should be created as early as possible, ideally during world creation.
         * Also, store the result of this function instead of calling this function multiple times with the same arguments.
         *
         * @throws [FleksFamilyException] if the [FamilyDefinition] is null or empty.
         * @throws FleksWrongConfigurationUsageException if called outside a [WorldConfiguration] scope.
         */
        fun family(cfg: FamilyDefinition.() -> Unit): Family =
            CURRENT_WORLD?.family(cfg) ?: throw FleksWrongConfigurationUsageException()
    }
}

private inline fun <T> List<T>.forEachReverse(action: (T) -> Unit) {
    val lastIndex = this.lastIndex
    for (i in lastIndex downTo 0) {
        action(this[i])
    }
}

@RefuX
Copy link
Contributor Author

RefuX commented Jul 4, 2024

Great, I was just about to start looking at this and was writing some test cases 👍

@Quillraven
Copy link
Owner

looks good now - thanks a lot for adding the new test cases to also verify this special hook behavior <3

Btw, is it okay for you if this feature is currently only available in SNAPSHOT version? Or do you need a stable release for it?

@RefuX
Copy link
Contributor Author

RefuX commented Jul 4, 2024

Btw, is it okay for you if this feature is currently only available in SNAPSHOT version? Or do you need a stable release for it?

I'm currently running off a local jar, so I'm not fussed 😎

@RefuX RefuX closed this Jul 4, 2024
@RefuX
Copy link
Contributor Author

RefuX commented Jul 4, 2024

It's warning me about unmerged commits, so maybe I can't close this yet.

@RefuX RefuX reopened this Jul 4, 2024
@Quillraven
Copy link
Owner

Quillraven commented Jul 4, 2024

Yeah, I did not merge it yet, just approved it :) Will merge it tomorrow.

One more thing that I noticed now: it seems like systems.forEach is now using an iterator in the generated Bytecode which is not ideal because that floods the garbage collector with useless objects in our scenario.

Can you please update that to use a for loop with indices inside world.update?

@Quillraven Quillraven merged commit b1b72e7 into Quillraven:master Jul 5, 2024
4 checks passed
Quillraven added a commit that referenced this pull request Jul 5, 2024
@Quillraven
Copy link
Owner

@RefuX: should be ready in SNAPSHOT version in a few minutes. Thanks again for your contribution, especially for adding the test cases to cover the special hook behavior!

@RefuX
Copy link
Contributor Author

RefuX commented Jul 5, 2024

Great, thanks so much, using the SNAPSHOT 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants