CCCPaste

11da25d7698d6

export type SystemPhase = "pre" | "update" | "post"

export type ComponentTable = { [number]: T }

type ComponentStore = { [string]: ComponentTable }
type ComponentEntityLists = { [string]: { number } }
type ComponentIndexMaps = { [string]: { [number]: number } }
type EntityComponentSets = { [number]: { [string]: boolean } }

export type SystemFn = (world: World, dt: number) -> ()

export type WorldStats = {
	entityCount: number,
	componentCounts: { [string]: number },
	systemCounts: {
		pre: number,
		update: number,
		post: number,
	},
	lastStepDt: number,
	totalSteps: number,
}

export type WorldOptions = {
	initialEntityCapacity: number?,
	initialComponentEntityCapacity: number?,
	scratchSize: number?,
	debugProfiling: boolean?,
	debugAssertions: boolean?,
}

export type DebugOptions = {
	profiling: boolean?,
	assertions: boolean?,
}

type Archetype = {
	id: number,
	signatureKey: string,
	components: { [string]: boolean },
	entities: { number },
	entityIndex: { [number]: number },
}

type QueryPlan = {
	includeSet: { [string]: boolean },
	excludeSet: { [string]: boolean }?,
	archetypes: { Archetype },
}

export type World = {
	_nextEntityId: number,
	_freeEntityIds: { number },

	_entityCapacity: number,
	_componentEntityCapacity: number,
	_scratchSize: number,

	_alive: { [number]: boolean },

	_components: ComponentStore,
	_componentEntities: ComponentEntityLists,
	_componentIndex: ComponentIndexMaps,

	_entityComponents: EntityComponentSets,
	_componentScratch: { string },

	_archetypes: { [number]: Archetype },
	_archetypeBySignature: { [string]: Archetype },
	_entityArchetype: { [number]: number },
	_nextArchetypeId: number,

	_queries: { [string]: QueryPlan },

	_systemsPre: { SystemFn },
	_systemsUpdate: { SystemFn },
	_systemsPost: { SystemFn },
	_hasSystems: boolean,

	_shardSystemsPre: { [number]: { SystemFn } },
	_shardSystemsUpdate: { [number]: { SystemFn } },
	_shardSystemsPost: { [number]: { SystemFn } },

	_debugProfiling: boolean,
	_debugAssertions: boolean,

	_lastStepDt: number,
	_totalSteps: number,

	_inStepDepth: number,

	_queueKinds: { number },
	_queueEntityIds: { number },
	_queueComponentNames: { string? },
	_queueComponentData: { any },

	Spawn: (self: World) -> number,
	Despawn: (self: World, entityId: number) -> (),

	HasEntity: (self: World, entityId: number) -> boolean,

	GetComponentTable: (self: World, name: string) -> ComponentTable,
	AddComponent: (self: World, entityId: number, name: string, data: T) -> (),
	GetComponent: (self: World, entityId: number, name: string) -> T?,
	RemoveComponent: (self: World, entityId: number, name: string) -> (),

	RegisterSystem: (self: World, phase: SystemPhase, system: SystemFn) -> (),
	RegisterSystemShard: (self: World, phase: SystemPhase, shardId: number, system: SystemFn) -> (),

	Step: (self: World, dt: number) -> (),
	StepShard: (self: World, dt: number, shardId: number) -> (),

	ForEach: (self: World, name: string, fn: (entityId: number, data: T) -> ()) -> (),
	ForEach2: (
		self: World,
		nameA: string,
		nameB: string,
		fn: (entityId: number, dataA: A, dataB: B) -> ()
	) -> (),
	ForEachQuery: (self: World, include: { string }, exclude: { string }?, fn: (entityId: number) -> ()) -> (),

	Clear: (self: World) -> (),

	SetDebugOptions: (self: World, options: DebugOptions) -> (),
	GetStats: (self: World) -> WorldStats,

	QueueDespawn: (self: World, entityId: number) -> (),
	QueueAddComponent: (self: World, entityId: number, name: string, data: T) -> (),
	QueueRemoveComponent: (self: World, entityId: number, name: string) -> (),
}

type ECSWorldModule = {
	new: (options: WorldOptions?) -> World,
}

local ECSWorld = {} :: ECSWorldModule

local DEFAULT_ENTITY_CAPACITY = 256
local DEFAULT_COMPONENT_ENTITY_CAPACITY = 64
local DEFAULT_SCRATCH_SIZE = 16

local QUEUE_KIND_DESPAWN = 1
local QUEUE_KIND_ADD = 2
local QUEUE_KIND_REMOVE = 3

local debugLib = debug :: any

local profileBegin: ((string) -> ())? = nil
local profileEnd: (() -> ())? = nil

if debugLib and debugLib.profilebegin and debugLib.profileend then
	profileBegin = debugLib.profilebegin :: (string) -> ()
	profileEnd = debugLib.profileend :: () -> ()
end

local TablePool = {}

TablePool._pool = {} :: { { any } }

function TablePool.acquire(minCapacity: number): { any }
	local pool = TablePool._pool
	local count = #pool
	if count > 0 then
		local t = pool[count]
		pool[count] = nil
		return t
	end
	if minCapacity > 0 then
		local t = table.create(minCapacity)
		return t
	end
	return {}
end

function TablePool.release(t: { any })
	table.clear(t)
	local pool = TablePool._pool
	pool[#pool + 1] = t
end

local function runSystems(list: { SystemFn }, world: World, dt: number)
	local count = #list
	if count == 0 then
		return
	end

	for i = 1, count do
		list[i](world, dt)
	end
end

local function getComponentTable(self: World, name: string): ComponentTable
	local components = self._components
	local existing = components[name]
	if existing ~= nil then
		return existing :: ComponentTable
	end

	local newTbl = {} :: ComponentTable
	components[name] = newTbl

	return newTbl
end

local function ensureComponentMeta(self: World, name: string)
	local entitiesByComponent = self._componentEntities
	local indexByComponent = self._componentIndex

	if entitiesByComponent[name] == nil then
		local list = table.create(self._componentEntityCapacity)
		table.clear(list)
		entitiesByComponent[name] = list :: { number }
	end

	if indexByComponent[name] == nil then
		indexByComponent[name] = {}
	end
end

local function computeSignature(compSet: { [string]: boolean }?): string?
	if compSet == nil then
		return nil
	end

	local count = 0
	for _ in compSet do
		count += 1
	end

	if count == 0 then
		return nil
	end

	local scratch = TablePool.acquire(count)
	local i = 1
	for name in compSet do
		scratch[i] = name
		i += 1
	end

	table.sort(scratch, function(a, b)
		return (a :: string) < (b :: string)
	end)

	local key = table.concat(scratch :: { string }, "\0", 1, count)
	TablePool.release(scratch)

	return key
end

local function archetypeMatchesQuery(
	archetype: Archetype,
	includeSet: { [string]: boolean },
	excludeSet: { [string]: boolean }?
): boolean
	for name in includeSet do
		if not archetype.components[name] then
			return false
		end
	end

	if excludeSet ~= nil then
		for name in excludeSet do
			if archetype.components[name] then
				return false
			end
		end
	end

	return true
end

local function getOrCreateArchetype(self: World, signatureKey: string, compSet: { [string]: boolean }): Archetype
	local bySignature = self._archetypeBySignature
	local existing = bySignature[signatureKey]
	if existing ~= nil then
		return existing
	end

	local id = self._nextArchetypeId + 1
	self._nextArchetypeId = id

	local componentsCopy: { [string]: boolean } = {}
	for name in compSet do
		componentsCopy[name] = true
	end

	local archetype: Archetype = {
		id = id,
		signatureKey = signatureKey,
		components = componentsCopy,
		entities = {},
		entityIndex = {},
	}

	self._archetypes[id] = archetype
	bySignature[signatureKey] = archetype

	local queries = self._queries
	for _, plan in queries do
		if archetypeMatchesQuery(archetype, plan.includeSet, plan.excludeSet) then
			local list = plan.archetypes
			list[#list + 1] = archetype
		end
	end

	return archetype
end

local function updateEntityArchetype(self: World, entityId: number, compSet: { [string]: boolean }?)
	local signatureKey = computeSignature(compSet)
	local entityArchetype = self._entityArchetype
	local oldId = entityArchetype[entityId]

	if signatureKey == nil then
		if oldId ~= nil then
			local oldArch = self._archetypes[oldId]
			if oldArch ~= nil then
				local entities = oldArch.entities
				local index = oldArch.entityIndex
				local idx = index[entityId]
				if idx ~= nil then
					local lastIndex = #entities
					local lastId = entities[lastIndex]
					entities[lastIndex] = nil
					if idx ~= lastIndex then
						entities[idx] = lastId
						index[lastId] = idx
					end
					index[entityId] = nil
				end
			end
			entityArchetype[entityId] = nil
		end
		return
	end

	local archetype = getOrCreateArchetype(self, signatureKey, compSet :: { [string]: boolean })
	if oldId == archetype.id then
		return
	end

	if oldId ~= nil then
		local oldArch = self._archetypes[oldId]
		if oldArch ~= nil then
			local entities = oldArch.entities
			local index = oldArch.entityIndex
			local idx = index[entityId]
			if idx ~= nil then
				local lastIndex = #entities
				local lastId = entities[lastIndex]
				entities[lastIndex] = nil
				if idx ~= lastIndex then
					entities[idx] = lastId
					index[lastId] = idx
				end
				index[entityId] = nil
			end
		end
	end

	local entities = archetype.entities
	local index = archetype.entityIndex
	if index[entityId] == nil then
		local newIndex = #entities + 1
		entities[newIndex] = entityId
		index[entityId] = newIndex
	end

	entityArchetype[entityId] = archetype.id
end

local function detachComponent(self: World, entityId: number, name: string)
	local components = self._components
	local dataAny = components[name]
	if dataAny == nil then
		return
	end

	local data = dataAny :: ComponentTable
	if data[entityId] == nil then
		return
	end

	data[entityId] = nil

	local entitiesByComponent = self._componentEntities
	local indexByComponent = self._componentIndex

	local entityList = entitiesByComponent[name]
	local indexMap = indexByComponent[name]

	if entityList ~= nil and indexMap ~= nil then
		local idx = indexMap[entityId]
		if idx ~= nil then
			local lastIndex = #entityList
			local lastId = entityList[lastIndex]
			entityList[lastIndex] = nil

			if idx ~= lastIndex then
				entityList[idx] = lastId
				indexMap[lastId] = idx
			end

			indexMap[entityId] = nil
		end
	end

	local entityComponents = self._entityComponents
	local compSet = entityComponents[entityId]
	if compSet ~= nil then
		compSet[name] = nil
		if next(compSet) == nil then
			entityComponents[entityId] = nil
			updateEntityArchetype(self, entityId, nil)
		else
			updateEntityArchetype(self, entityId, compSet)
		end
	end
end

local function spawnEntity(self: World): number
	local free = self._freeEntityIds
	local freeCount = #free

	local id: number
	if freeCount > 0 then
		id = free[freeCount]
		free[freeCount] = nil
	else
		local nextId = self._nextEntityId + 1
		self._nextEntityId = nextId
		id = nextId

		if nextId > self._entityCapacity then
			self._entityCapacity = nextId
		end
	end

	self._alive[id] = true
	return id
end

local function hasEntity(self: World, entityId: number): boolean
	return self._alive[entityId] == true
end

local function addComponentImmediate(self: World, entityId: number, name: string, data: T)
	if not self._alive[entityId] then
		if self._debugAssertions then
			error("ECSWorld.AddComponent: entity is not alive")
		end
		return
	end

	local tbl = getComponentTable(self, name) :: ComponentTable
	tbl[entityId] = data

	ensureComponentMeta(self, name)

	local entityList = self._componentEntities[name] :: { number }
	local indexMap = self._componentIndex[name] :: { [number]: number }

	local entityComponents = self._entityComponents
	local compSet = entityComponents[entityId]
	if compSet == nil then
		local newSet: { [string]: boolean } = {}
		entityComponents[entityId] = newSet
		compSet = newSet
	end
	compSet[name] = true

	if indexMap[entityId] == nil then
		local newIndex = #entityList + 1
		entityList[newIndex] = entityId
		indexMap[entityId] = newIndex
	end

	updateEntityArchetype(self, entityId, compSet)
end

local function removeComponentImmediate(self: World, entityId: number, name: string)
	detachComponent(self, entityId, name)
end

local function despawnEntityImmediate(self: World, entityId: number)
	if not self._alive[entityId] then
		if self._debugAssertions then
			error("ECSWorld.Despawn: entity is not alive")
		end
		return
	end

	self._alive[entityId] = nil

	local entityComponents = self._entityComponents
	local compSet = entityComponents[entityId]
	if compSet ~= nil then
		local scratch = self._componentScratch
		local count = 0

		for name in compSet do
			count += 1
			scratch[count] = name
		end

		for i = 1, count do
			local compName = scratch[i]
			scratch[i] = nil
			detachComponent(self, entityId, compName)
		end
	end

	local free = self._freeEntityIds
	free[#free + 1] = entityId
end

local function getComponent(self: World, entityId: number, name: string): T?
	local tbl = self._components[name]
	if tbl == nil then
		return nil
	end
	return (tbl :: ComponentTable)[entityId]
end

local function enqueueOp(self: World, kind: number, entityId: number, compName: string?, data: any)
	local kinds = self._queueKinds
	local index = #kinds + 1
	kinds[index] = kind
	self._queueEntityIds[index] = entityId
	self._queueComponentNames[index] = compName
	self._queueComponentData[index] = data
end

local function flushQueue(self: World)
	local kinds = self._queueKinds
	local count = #kinds
	if count == 0 then
		return
	end

	local entities = self._queueEntityIds
	local names = self._queueComponentNames
	local data = self._queueComponentData

	for i = 1, count do
		local kind = kinds[i]
		local entityId = entities[i]

		if kind == QUEUE_KIND_DESPAWN then
			despawnEntityImmediate(self, entityId)
		elseif kind == QUEUE_KIND_ADD then
			local name = names[i]
			if name ~= nil then
				addComponentImmediate(self, entityId, name, data[i])
			end
		elseif kind == QUEUE_KIND_REMOVE then
			local name = names[i]
			if name ~= nil then
				removeComponentImmediate(self, entityId, name)
			end
		end

		kinds[i] = 0
		entities[i] = 0
		names[i] = nil
		data[i] = nil
	end

	table.clear(kinds)
	table.clear(entities)
	table.clear(names)
	table.clear(data)
end

local function addComponent(self: World, entityId: number, name: string, data: T)
	if self._inStepDepth > 0 then
		enqueueOp(self, QUEUE_KIND_ADD, entityId, name, data)
	else
		addComponentImmediate(self, entityId, name, data)
	end
end

local function removeComponent(self: World, entityId: number, name: string)
	if self._inStepDepth > 0 then
		enqueueOp(self, QUEUE_KIND_REMOVE, entityId, name, nil)
	else
		removeComponentImmediate(self, entityId, name)
	end
end

local function despawnEntity(self: World, entityId: number)
	if self._inStepDepth > 0 then
		enqueueOp(self, QUEUE_KIND_DESPAWN, entityId, nil, nil)
	else
		despawnEntityImmediate(self, entityId)
	end
end

local function queueDespawn(self: World, entityId: number)
	enqueueOp(self, QUEUE_KIND_DESPAWN, entityId, nil, nil)
end

local function queueAddComponent(self: World, entityId: number, name: string, data: T)
	enqueueOp(self, QUEUE_KIND_ADD, entityId, name, data)
end

local function queueRemoveComponent(self: World, entityId: number, name: string)
	enqueueOp(self, QUEUE_KIND_REMOVE, entityId, name, nil)
end

local function registerSystem(self: World, phase: SystemPhase, system: SystemFn)
	if phase == "pre" then
		local list = self._systemsPre
		list[#list + 1] = system
	elseif phase == "update" then
		local list = self._systemsUpdate
		list[#list + 1] = system
	elseif phase == "post" then
		local list = self._systemsPost
		list[#list + 1] = system
	else
		error(("ECSWorld.RegisterSystem: invalid phase '%s'"):format(tostring(phase)))
	end

	self._hasSystems = true
end

local function registerSystemShard(self: World, phase: SystemPhase, shardId: number, system: SystemFn)
	if shardId <= 0 then
		if self._debugAssertions then
			error("ECSWorld.RegisterSystemShard: shardId must be > 0")
		end
		return
	end

	if phase == "pre" then
		local byShard = self._shardSystemsPre
		local list = byShard[shardId]
		if list == nil then
			list = {}
			byShard[shardId] = list
		end
		list[#list + 1] = system
	elseif phase == "update" then
		local byShard = self._shardSystemsUpdate
		local list = byShard[shardId]
		if list == nil then
			list = {}
			byShard[shardId] = list
		end
		list[#list + 1] = system
	elseif phase == "post" then
		local byShard = self._shardSystemsPost
		local list = byShard[shardId]
		if list == nil then
			list = {}
			byShard[shardId] = list
		end
		list[#list + 1] = system
	else
		error(("ECSWorld.RegisterSystemShard: invalid phase '%s'"):format(tostring(phase)))
	end
end

local function step(self: World, dt: number)
	if dt <= 0 or not self._hasSystems then
		return
	end

	self._lastStepDt = dt
	self._totalSteps += 1

	self._inStepDepth += 1

	local profilingEnabled = self._debugProfiling and profileBegin ~= nil and profileEnd ~= nil

	if profilingEnabled then
		local pb = profileBegin :: (string) -> ()
		local pe = profileEnd :: () -> ()

		pb("ECSWorld::Step(pre)")
		runSystems(self._systemsPre, self, dt)
		pe()

		pb("ECSWorld::Step(update)")
		runSystems(self._systemsUpdate, self, dt)
		pe()

		pb("ECSWorld::Step(post)")
		runSystems(self._systemsPost, self, dt)
		pe()
	else
		runSystems(self._systemsPre, self, dt)
		runSystems(self._systemsUpdate, self, dt)
		runSystems(self._systemsPost, self, dt)
	end

	flushQueue(self)
	self._inStepDepth -= 1
end

local function stepShard(self: World, dt: number, shardId: number)
	if dt <= 0 then
		return
	end

	local pre = self._shardSystemsPre[shardId]
	local update = self._shardSystemsUpdate[shardId]
	local post = self._shardSystemsPost[shardId]

	if pre == nil and update == nil and post == nil then
		return
	end

	self._lastStepDt = dt
	self._totalSteps += 1

	self._inStepDepth += 1

	local profilingEnabled = self._debugProfiling and profileBegin ~= nil and profileEnd ~= nil

	if profilingEnabled then
		local pb = profileBegin :: (string) -> ()
		local pe = profileEnd :: () -> ()

		if pre ~= nil and #pre > 0 then
			pb(("ECSWorld::StepShard(%d,pre)"):format(shardId))
			runSystems(pre, self, dt)
			pe()
		end

		if update ~= nil and #update > 0 then
			pb(("ECSWorld::StepShard(%d,update)"):format(shardId))
			runSystems(update, self, dt)
			pe()
		end

		if post ~= nil and #post > 0 then
			pb(("ECSWorld::StepShard(%d,post)"):format(shardId))
			runSystems(post, self, dt)
			pe()
		end
	else
		if pre ~= nil then
			runSystems(pre, self, dt)
		end
		if update ~= nil then
			runSystems(update, self, dt)
		end
		if post ~= nil then
			runSystems(post, self, dt)
		end
	end

	flushQueue(self)
	self._inStepDepth -= 1
end

local function forEach(self: World, name: string, fn: (entityId: number, data: T) -> ())
	local entityList = self._componentEntities[name]
	if entityList == nil then
		return
	end

	local dataTblAny = self._components[name]
	if dataTblAny == nil then
		return
	end

	local dataTbl = dataTblAny :: ComponentTable
	local alive = self._alive
	local count = #entityList
	local fnLocal = fn

	for i = 1, count do
		local entityId = entityList[i]
		if alive[entityId] then
			fnLocal(entityId, dataTbl[entityId])
		end
	end
end

local function forEach2(
	self: World,
	nameA: string,
	nameB: string,
	fn: (entityId: number, dataA: A, dataB: B) -> ()
)
	local entitiesA = self._componentEntities[nameA]
	local entitiesB = self._componentEntities[nameB]
	if entitiesA == nil or entitiesB == nil then
		return
	end

	local dataAAny = self._components[nameA]
	local dataBAny = self._components[nameB]
	if dataAAny == nil or dataBAny == nil then
		return
	end

	local alive = self._alive

	local listSmall, listLarge
	local dataSmallAny, dataLargeAny
	local nameSmall, nameLarge

	if #entitiesA <= #entitiesB then
		listSmall, listLarge = entitiesA, entitiesB
		dataSmallAny, dataLargeAny = dataAAny, dataBAny
		nameSmall, nameLarge = nameA, nameB
	else
		listSmall, listLarge = entitiesB, entitiesA
		dataSmallAny, dataLargeAny = dataBAny, dataAAny
		nameSmall, nameLarge = nameB, nameA
	end

	local count = #listSmall
	if count == 0 then
		return
	end

	local indexLarge = self._componentIndex[nameLarge]
	if indexLarge == nil then
		return
	end

	local fnLocal = fn

	if nameSmall == nameA then
		local dataA = dataSmallAny :: ComponentTable
		local dataB = dataLargeAny :: ComponentTable

		for i = 1, count do
			local entityId = listSmall[i]
			if alive[entityId] and indexLarge[entityId] ~= nil then
				fnLocal(entityId, dataA[entityId], dataB[entityId])
			end
		end
	else
		local dataB = dataSmallAny :: ComponentTable
		local dataA = dataLargeAny :: ComponentTable

		for i = 1, count do
			local entityId = listSmall[i]
			if alive[entityId] and indexLarge[entityId] ~= nil then
				fnLocal(entityId, dataA[entityId], dataB[entityId])
			end
		end
	end
end

local function getOrCreateQueryPlan(
	self: World,
	include: { string },
	exclude: { string }?
): QueryPlan
	local includeSet: { [string]: boolean } = {}
	local excludeSet: { [string]: boolean }? = nil

	local includeCount = #include
	for i = 1, includeCount do
		includeSet[include[i]] = true
	end

	local excludeCount = 0
	if exclude ~= nil then
		excludeSet = {}
		excludeCount = #exclude
		for i = 1, excludeCount do
			(excludeSet :: { [string]: boolean })[exclude[i]] = true
		end
	end

	local scratch = TablePool.acquire(includeCount + excludeCount)

	local idx = 1
	for name in includeSet do
		scratch[idx] = name
		idx += 1
	end

	if excludeSet ~= nil then
		for name in (excludeSet :: { [string]: boolean }) do
			scratch[idx] = ("!%s"):format(name)
			idx += 1
		end
	end

	table.sort(scratch, function(a, b)
		return (a :: string) < (b :: string)
	end)

	local key = table.concat(scratch :: { string }, "|", 1, idx - 1)
	TablePool.release(scratch)

	local queries = self._queries
	local existing = queries[key]
	if existing ~= nil then
		return existing
	end

	local archetypesList: { Archetype } = {}
	for _, archetype in self._archetypes do
		if archetypeMatchesQuery(archetype, includeSet, excludeSet) then
			archetypesList[#archetypesList + 1] = archetype
		end
	end

	local plan: QueryPlan = {
		includeSet = includeSet,
		excludeSet = excludeSet,
		archetypes = archetypesList,
	}

	queries[key] = plan
	return plan
end

local function forEachQuery(self: World, include: { string }, exclude: { string }?, fn: (entityId: number) -> ())
	if #include == 0 then
		return
	end

	local plan = getOrCreateQueryPlan(self, include, exclude)
	local alive = self._alive
	local archetypes = plan.archetypes
	local countArch = #archetypes
	local fnLocal = fn

	for i = 1, countArch do
		local archetype = archetypes[i]
		local entities = archetype.entities
		local countEntities = #entities

		for j = 1, countEntities do
			local entityId = entities[j]
			if alive[entityId] then
				fnLocal(entityId)
			end
		end
	end
end

local function clearWorld(self: World)
	self._nextEntityId = 0
	self._lastStepDt = 0
	self._totalSteps = 0

	table.clear(self._freeEntityIds)
	table.clear(self._alive)

	for _, tbl in self._components do
		table.clear(tbl)
	end

	for _, list in self._componentEntities do
		table.clear(list)
	end

	for _, indexMap in self._componentIndex do
		table.clear(indexMap)
	end

	table.clear(self._entityComponents)
	table.clear(self._componentScratch)

	for _, archetype in self._archetypes do
		table.clear(archetype.entities)
		table.clear(archetype.entityIndex)
	end

	table.clear(self._entityArchetype)

	table.clear(self._queueKinds)
	table.clear(self._queueEntityIds)
	table.clear(self._queueComponentNames)
	table.clear(self._queueComponentData)
end

local function setDebugOptions(self: World, options: DebugOptions)
	local profiling = options.profiling
	if profiling ~= nil then
		self._debugProfiling = profiling
	end

	local assertions = options.assertions
	if assertions ~= nil then
		self._debugAssertions = assertions
	end
end

local function getStats(self: World): WorldStats
	local entityCount = 0
	for _ in self._alive do
		entityCount += 1
	end

	local componentCounts: { [string]: number } = {}
	for name, list in self._componentEntities do
		componentCounts[name] = #list
	end

	local systemCounts = {
		pre = #self._systemsPre,
		update = #self._systemsUpdate,
		post = #self._systemsPost,
	}

	return {
		entityCount = entityCount,
		componentCounts = componentCounts,
		systemCounts = systemCounts,
		lastStepDt = self._lastStepDt,
		totalSteps = self._totalSteps,
	}
end

function ECSWorld.new(options: WorldOptions?): World
	local opt: WorldOptions = (options or {}) :: WorldOptions

	local entityCapacity = opt.initialEntityCapacity or DEFAULT_ENTITY_CAPACITY
	local componentEntityCapacity = opt.initialComponentEntityCapacity or DEFAULT_COMPONENT_ENTITY_CAPACITY
	local scratchSize = opt.scratchSize or DEFAULT_SCRATCH_SIZE

	if entityCapacity < 1 then
		entityCapacity = DEFAULT_ENTITY_CAPACITY
	end

	if componentEntityCapacity < 1 then
		componentEntityCapacity = DEFAULT_COMPONENT_ENTITY_CAPACITY
	end

	if scratchSize < 1 then
		scratchSize = DEFAULT_SCRATCH_SIZE
	end

	local debugProfiling = opt.debugProfiling == true
	local debugAssertions = opt.debugAssertions == true

	local freeEntityIds: { number } = table.create(entityCapacity) :: { number }
	table.clear(freeEntityIds)

	local world: World = {
		_nextEntityId = 0,
		_freeEntityIds = freeEntityIds,

		_entityCapacity = entityCapacity,
		_componentEntityCapacity = componentEntityCapacity,
		_scratchSize = scratchSize,

		_alive = {},

		_components = {} :: ComponentStore,
		_componentEntities = {} :: ComponentEntityLists,
		_componentIndex = {} :: ComponentIndexMaps,

		_entityComponents = {} :: EntityComponentSets,
		_componentScratch = table.create(scratchSize) :: { string },

		_archetypes = {},
		_archetypeBySignature = {},
		_entityArchetype = {},
		_nextArchetypeId = 0,

		_queries = {},

		_systemsPre = {},
		_systemsUpdate = {},
		_systemsPost = {},
		_hasSystems = false,

		_shardSystemsPre = {},
		_shardSystemsUpdate = {},
		_shardSystemsPost = {},

		_debugProfiling = debugProfiling,
		_debugAssertions = debugAssertions,

		_lastStepDt = 0,
		_totalSteps = 0,

		_inStepDepth = 0,

		_queueKinds = {},
		_queueEntityIds = {},
		_queueComponentNames = {},
		_queueComponentData = {},

		Spawn = spawnEntity,
		Despawn = despawnEntity,
		HasEntity = hasEntity,

		GetComponentTable = getComponentTable,
		AddComponent = addComponent,
		GetComponent = getComponent,
		RemoveComponent = removeComponent,

		RegisterSystem = registerSystem,
		RegisterSystemShard = registerSystemShard,

		Step = step,
		StepShard = stepShard,

		ForEach = forEach,
		ForEach2 = forEach2,
		ForEachQuery = forEachQuery,

		Clear = clearWorld,

		SetDebugOptions = setDebugOptions,
		GetStats = getStats,

		QueueDespawn = queueDespawn,
		QueueAddComponent = queueAddComponent,
		QueueRemoveComponent = queueRemoveComponent,
	}

	return world
end

return ECSWorld