ECS refactor

This commit is contained in:
Miroslav Vasilev 2025-06-25 20:14:07 +03:00
parent 87f5c2f842
commit b2212a279c
16 changed files with 598 additions and 200 deletions

View file

@ -0,0 +1,29 @@
package components
import "code.haedhutner.dev/mvv/LastMUD/internal/game/ecs"
const (
TypeName ecs.ComponentType = iota
TypeDescription
TypePlayerState
TypeInRoom
TypeNeighbors
TypeIsRoom
TypeIsPlayer
)
type NameComponent struct {
Name string
}
func (c NameComponent) Type() ecs.ComponentType {
return TypeName
}
type DescriptionComponent struct {
Description string
}
func (c DescriptionComponent) Type() ecs.ComponentType {
return TypeDescription
}

View file

@ -0,0 +1,35 @@
package components
import "code.haedhutner.dev/mvv/LastMUD/internal/game/ecs"
type PlayerState = byte
const (
PlayerStateJoining PlayerState = iota
PlayerStateLoggingIn
PlayerStateRegistering
PlayerStatePlaying
PlayerStateLeaving
)
type PlayerStateComponent struct {
State PlayerState
}
func (c PlayerStateComponent) Type() ecs.ComponentType {
return TypePlayerState
}
type InRoomComponent struct {
Room ecs.Entity
}
func (c InRoomComponent) Type() ecs.ComponentType {
return TypeInRoom
}
type IsPlayerComponent struct{}
func (c IsPlayerComponent) Type() ecs.ComponentType {
return TypeIsPlayer
}

View file

@ -0,0 +1,18 @@
package components
import "code.haedhutner.dev/mvv/LastMUD/internal/game/ecs"
type IsRoomComponent struct {
}
func (c IsRoomComponent) Type() ecs.ComponentType {
return TypeIsRoom
}
type NeighborsComponent struct {
North, South, East, West ecs.Entity
}
func (c NeighborsComponent) Type() ecs.ComponentType {
return TypeNeighbors
}

View file

@ -1,31 +0,0 @@
package ecs
type PlayerState = byte
const (
PlayerStateJoining PlayerState = iota
PlayerStateLoggingIn
PlayerStateRegistering
PlayerStatePlaying
PlayerStateLeaving
)
type PlayerStateComponent struct {
State PlayerState
}
type NameComponent struct {
Name string
}
type DescriptionComponent struct {
Description string
}
type InRoomComponent struct {
InRoom Entity
}
type NeighboringRoomsComponent struct {
North, South, East, West Entity
}

196
internal/game/ecs/ecs.go Normal file
View file

@ -0,0 +1,196 @@
package ecs
import (
"slices"
"time"
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
"github.com/google/uuid"
)
type Entity uuid.UUID
func CreateEntity(uuid uuid.UUID) Entity {
return Entity(uuid)
}
func NewEntity() Entity {
return Entity(uuid.New())
}
func NilEntity() Entity {
return Entity(uuid.Nil)
}
func (e Entity) AsUUID() uuid.UUID {
return uuid.UUID(e)
}
type ComponentType int16
type Resource string
type Component interface {
Type() ComponentType
}
type ComponentStorage[T Component] struct {
forType ComponentType
storage map[Entity]T
}
func CreateComponentStorage[T Component](forType ComponentType) *ComponentStorage[T] {
return &ComponentStorage[T]{
forType: forType,
storage: map[Entity]T{},
}
}
func (cs *ComponentStorage[T]) ComponentType() ComponentType {
return cs.forType
}
func (cs *ComponentStorage[T]) Set(e Entity, component T) {
cs.storage[e] = component
}
func (cs *ComponentStorage[T]) Get(e Entity) (component T, ok bool) {
component, ok = cs.storage[e]
return
}
func (cs *ComponentStorage[T]) Delete(e Entity) {
delete(cs.storage, e)
}
func (cs *ComponentStorage[T]) All() map[Entity]T {
return cs.storage
}
type System struct {
name string
priority int
work func(world *World, delta time.Duration) (err error)
}
func CreateSystem(name string, priority int, work func(world *World, delta time.Duration) (err error)) *System {
return &System{
name: name,
priority: priority,
work: work,
}
}
func (s *System) Priority() int {
return s.priority
}
func (s *System) DoWork(world *World, delta time.Duration) {
err := s.work(world, delta)
if err != nil {
logging.Error("Error in system '", s.name, "': ", err.Error())
}
}
type World struct {
systems []*System
componentsByType map[ComponentType]any
componentsByEntity map[Entity]map[ComponentType]any
resources map[Resource]any
}
func CreateWorld() (world *World) {
world = &World{
systems: []*System{},
componentsByType: map[ComponentType]any{},
componentsByEntity: map[Entity]map[ComponentType]any{}, // TODO: Can't figure out use-case right now
resources: map[Resource]any{},
}
return
}
func (w *World) Tick(delta time.Duration) {
for _, s := range w.systems {
s.DoWork(w, delta)
}
}
func DeleteEntity(world *World, entity Entity) {
for _, s := range world.componentsByType {
storage, ok := s.(*ComponentStorage[Component])
if ok {
storage.Delete(entity)
}
}
}
func SetResource(world *World, r Resource, val any) {
world.resources[r] = val
}
func GetResource[T any](world *World, r Resource) (res T, err error) {
val, ok := world.resources[r]
if !ok {
err = newECSError("Resource '", r, "' not found.")
return
}
res, ok = val.(T)
if !ok {
err = newECSError("Incompatible type for resource '", r, "'")
}
return
}
func RemoveResource(world *World, r Resource) {
delete(world.resources, r)
}
func RegisterComponent[T Component](world *World, compType ComponentType) {
world.componentsByType[compType] = CreateComponentStorage[T](compType)
}
func SetComponent[T Component](world *World, entity Entity, component T) {
compStorage := world.componentsByType[component.Type()].(*ComponentStorage[T])
compStorage.Set(entity, component)
// if _, ok := world.componentsByEntity[entity]; !ok {
// world.componentsByEntity[entity] = map[ComponentType]any{}
// }
// world.componentsByEntity[entity][component.Type()] = component
}
func GetComponent[T Component](world *World, entity Entity) (component T, exists bool) {
storage := GetComponentStorage[T](world)
return storage.Get(entity)
}
func DeleteComponent[T Component](world *World, entity Entity) {
storage := GetComponentStorage[T](world)
storage.Delete(entity)
}
func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage[T]) {
var zero T
return world.componentsByType[zero.Type()].(*ComponentStorage[T])
}
func RegisterSystem(world *World, s *System) {
world.systems = append(world.systems, s)
slices.SortFunc(
world.systems,
func(a, b *System) int {
return a.priority - b.priority
},
)
}

View file

@ -1,19 +0,0 @@
package ecs
import "github.com/google/uuid"
type Entity uuid.UUID
type Room struct {
Entity
NameComponent
DescriptionComponent
NeighboringRoomsComponent
}
type Player struct {
Entity
PlayerStateComponent
NameComponent
InRoomComponent
}

View file

@ -0,0 +1,23 @@
package ecs
import "fmt"
type ecsError struct {
err string
}
func newECSError(v ...any) *ecsError {
return &ecsError{
err: fmt.Sprint(v...),
}
}
func newFormattedECSError(format string, v ...any) *ecsError {
return &ecsError{
err: fmt.Sprintf(format, v...),
}
}
func (err *ecsError) Error() string {
return err.err
}

View file

@ -1 +0,0 @@
package ecs

View file

@ -1,15 +0,0 @@
package ecs
type World struct {
Players []*Player
Rooms []*Room
DefaultRoom *Room
}
func CreateWorld() *World {
world := &World{
Players: []*Player{},
Rooms: []*Room{},
}
}

39
internal/game/entities.go Normal file
View file

@ -0,0 +1,39 @@
package game
import (
"code.haedhutner.dev/mvv/LastMUD/internal/game/components"
"code.haedhutner.dev/mvv/LastMUD/internal/game/ecs"
"github.com/google/uuid"
)
func CreatePlayer(world *ecs.World, id uuid.UUID, state components.PlayerState) (entity ecs.Entity, err error) {
entity = ecs.CreateEntity(id)
defaultRoom, err := ecs.GetResource[ecs.Entity](world, ResourceDefaultRoom)
if err != nil {
return
}
ecs.SetComponent(world, entity, components.PlayerStateComponent{State: state})
ecs.SetComponent(world, entity, components.NameComponent{Name: id.String()})
ecs.SetComponent(world, entity, components.InRoomComponent{Room: defaultRoom})
ecs.SetComponent(world, entity, components.IsPlayerComponent{})
return
}
func CreateRoom(
world *ecs.World,
name, description string,
north, south, east, west ecs.Entity,
) ecs.Entity {
entity := ecs.NewEntity()
ecs.SetComponent(world, entity, components.IsRoomComponent{})
ecs.SetComponent(world, entity, components.NameComponent{Name: name})
ecs.SetComponent(world, entity, components.DescriptionComponent{Description: description})
ecs.SetComponent(world, entity, components.NeighborsComponent{North: north, South: south, East: east, West: west})
return entity
}

View file

@ -4,6 +4,8 @@ import (
"time" "time"
"code.haedhutner.dev/mvv/LastMUD/internal/game/command" "code.haedhutner.dev/mvv/LastMUD/internal/game/command"
"code.haedhutner.dev/mvv/LastMUD/internal/game/components"
"code.haedhutner.dev/mvv/LastMUD/internal/game/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/logging" "code.haedhutner.dev/mvv/LastMUD/internal/logging"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -23,10 +25,14 @@ func (pje *PlayerJoinEvent) Type() EventType {
} }
func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) { func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) {
p := CreateJoiningPlayer(pje.connectionId) p, err := CreatePlayer(game.world.World, pje.connectionId, components.PlayerStateJoining)
game.world.AddPlayerToDefaultRoom(p)
game.enqeueOutput(game.CreateOutput(p.Identity(), []byte("Welcome to LastMUD!"))) if err != nil {
game.enqeueOutput(game.CreateOutput(p.Identity(), []byte("Please enter your name:"))) logging.Error("Unabled to create player: ", err)
}
game.enqeueOutput(game.CreateOutput(p.AsUUID(), []byte("Welcome to LastMUD!")))
game.enqeueOutput(game.CreateOutput(p.AsUUID(), []byte("Please enter your name:")))
} }
type PlayerLeaveEvent struct { type PlayerLeaveEvent struct {
@ -44,7 +50,7 @@ func (ple *PlayerLeaveEvent) Type() EventType {
} }
func (ple *PlayerLeaveEvent) Handle(game *LastMUDGame, delta time.Duration) { func (ple *PlayerLeaveEvent) Handle(game *LastMUDGame, delta time.Duration) {
game.world.RemovePlayerById(ple.connectionId) ecs.DeleteEntity(game.world.World, ecs.CreateEntity(ple.connectionId))
} }
type PlayerCommandEvent struct { type PlayerCommandEvent struct {
@ -72,8 +78,6 @@ func (pce *PlayerCommandEvent) Type() EventType {
} }
func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) { func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) {
player := game.world.FindPlayerById(pce.connectionId)
if player == nil { if player == nil {
logging.Error("Unable to handle player command from player with id", pce.connectionId, ": Player does not exist") logging.Error("Unable to handle player command from player with id", pce.connectionId, ": Player does not exist")
return return
@ -82,7 +86,7 @@ func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) {
event := pce.parseCommandIntoEvent(game, player) event := pce.parseCommandIntoEvent(game, player)
} }
func (pce *PlayerCommandEvent) parseCommandIntoEvent(game *LastMUDGame, player *Player) GameEvent { func (pce *PlayerCommandEvent) parseCommandIntoEvent(game *LastMUDGame, player ecs.Entity) GameEvent {
switch pce.command.Command().Definition().Name() { switch pce.command.Command().Definition().Name() {
case SayCommand: case SayCommand:
speech, err := pce.command.Command().Parameters()[0].AsString() speech, err := pce.command.Command().Parameters()[0].AsString()

View file

@ -41,7 +41,7 @@ type LastMUDGame struct {
cmdRegistry *command.CommandRegistry cmdRegistry *command.CommandRegistry
world *World world *GameWorld
eventBus *EventBus eventBus *EventBus
@ -54,7 +54,7 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
ctx: ctx, ctx: ctx,
eventBus: CreateEventBus(MaxEnqueuedGameEventsPerTick), eventBus: CreateEventBus(MaxEnqueuedGameEventsPerTick),
output: make(chan GameOutput, MaxEnqueuedOutputPerTick), output: make(chan GameOutput, MaxEnqueuedOutputPerTick),
world: CreateWorld(), world: CreateGameWorld(),
} }
game.cmdRegistry = game.CreateGameCommandRegistry() game.cmdRegistry = game.CreateGameCommandRegistry()

View file

@ -1,39 +1,39 @@
package game package game
import "github.com/google/uuid" // import "github.com/google/uuid"
type Player struct { // type Player struct {
id uuid.UUID // id uuid.UUID
state PlayerState // state PlayerState
currentRoom *Room // currentRoom *Room
} // }
func CreateJoiningPlayer(identity uuid.UUID) *Player { // func CreateJoiningPlayer(identity uuid.UUID) *Player {
return &Player{ // return &Player{
id: identity, // id: identity,
state: PlayerStateJoining, // state: PlayerStateJoining,
currentRoom: nil, // currentRoom: nil,
} // }
} // }
func CreatePlayer(identity uuid.UUID, state PlayerState, room *Room) *Player { // func CreatePlayer(identity uuid.UUID, state PlayerState, room *Room) *Player {
return &Player{ // return &Player{
id: identity, // id: identity,
state: state, // state: state,
currentRoom: room, // currentRoom: room,
} // }
} // }
func (p *Player) Identity() uuid.UUID { // func (p *Player) Identity() uuid.UUID {
return p.id // return p.id
} // }
func (p *Player) SetRoom(r *Room) { // func (p *Player) SetRoom(r *Room) {
p.currentRoom = r // p.currentRoom = r
} // }
func (p *Player) CurrentRoom() *Room { // func (p *Player) CurrentRoom() *Room {
return p.currentRoom // return p.currentRoom
} // }

View file

@ -1,51 +1,51 @@
package game package game
import "github.com/google/uuid" // import "github.com/google/uuid"
type RoomPlayer interface { // type RoomPlayer interface {
Identity() uuid.UUID // Identity() uuid.UUID
SetRoom(room *Room) // SetRoom(room *Room)
} // }
type Room struct { // type Room struct {
world *World // world *World
North *Room // North *Room
South *Room // South *Room
East *Room // East *Room
West *Room // West *Room
Name string // Name string
Description string // Description string
players map[uuid.UUID]RoomPlayer // players map[uuid.UUID]RoomPlayer
} // }
func CreateRoom(world *World, name, description string) *Room { // func CreateRoom(world *World, name, description string) *Room {
return &Room{ // return &Room{
world: world, // world: world,
Name: name, // Name: name,
Description: description, // Description: description,
players: map[uuid.UUID]RoomPlayer{}, // players: map[uuid.UUID]RoomPlayer{},
} // }
} // }
func (r *Room) PlayerJoinRoom(player RoomPlayer) (err error) { // func (r *Room) PlayerJoinRoom(player RoomPlayer) (err error) {
r.players[player.Identity()] = player // r.players[player.Identity()] = player
return // return
} // }
func (r *Room) PlayerLeaveRoom(player RoomPlayer) (err error) { // func (r *Room) PlayerLeaveRoom(player RoomPlayer) (err error) {
delete(r.players, player.Identity()) // delete(r.players, player.Identity())
return // return
} // }
func (r *Room) Players() map[uuid.UUID]RoomPlayer { // func (r *Room) Players() map[uuid.UUID]RoomPlayer {
return r.players // return r.players
} // }
func (r *Room) World() *World { // func (r *Room) World() *World {
return r.world // return r.world
} // }

1
internal/game/systems.go Normal file
View file

@ -0,0 +1 @@
package game

View file

@ -1,80 +1,199 @@
package game package game
import "github.com/google/uuid" import (
"code.haedhutner.dev/mvv/LastMUD/internal/game/components"
"code.haedhutner.dev/mvv/LastMUD/internal/game/ecs"
)
type World struct { const (
rooms []*Room ResourceDefaultRoom ecs.Resource = "world:room:default"
players map[uuid.UUID]*Player )
defaultRoom *Room
type GameWorld struct {
*ecs.World
} }
func CreateWorld() (world *World) { func CreateGameWorld() (gw *GameWorld) {
world = &World{ gw = &GameWorld{
players: map[uuid.UUID]*Player{}, World: ecs.CreateWorld(),
} }
forest := CreateRoom(world, "Forest", "A dense, misty forest stretches endlessly, its towering trees whispering secrets through rustling leaves. Sunbeams filter through the canopy, dappling the mossy ground with golden light.") forest := CreateRoom(
cabin := CreateRoom(world, "Wooden Cabin", "The cabins interior is cozy and rustic, with wooden beams overhead and a stone fireplace crackling warmly. A wool rug lies on creaky floorboards, and shelves brim with books, mugs, and old lanterns.") gw.World,
lake := CreateRoom(world, "Ethermere Lake", "Ethermire Lake lies shrouded in mist, its dark, still waters reflecting a sky perpetually overcast. Whispers ride the wind, and strange lights flicker beneath the surface, never breaking it.") "Forest",
graveyard := CreateRoom(world, "Graveyard", "An overgrown graveyard shrouded in fog, with cracked headstones and leaning statues. The wind sighs through dead trees, and unseen footsteps echo faintly among the mossy graves.") "A dense, misty forest stretches endlessly, its towering trees whispering secrets through rustling leaves. Sunbeams filter through the canopy, dappling the mossy ground with golden light.",
chapel := CreateRoom(world, "Chapel of the Hollow Light", "This ruined chapel leans under ivy and age. Faint light filters through shattered stained glass, casting broken rainbows across dust-choked pews and a long-silent altar.") ecs.NilEntity(),
ecs.NilEntity(),
ecs.NilEntity(),
ecs.NilEntity(),
)
forest.North = cabin ecs.SetResource(gw.World, ResourceDefaultRoom, forest)
forest.South = graveyard
forest.East = lake
forest.West = chapel
cabin.South = forest cabin := CreateRoom(
cabin.West = chapel gw.World,
cabin.East = lake "Wooden Cabin",
"The cabins interior is cozy and rustic, with wooden beams overhead and a stone fireplace crackling warmly. A wool rug lies on creaky floorboards, and shelves brim with books, mugs, and old lanterns.",
ecs.NilEntity(),
ecs.NilEntity(),
ecs.NilEntity(),
ecs.NilEntity(),
)
chapel.North = cabin lake := CreateRoom(
chapel.South = graveyard gw.World,
chapel.East = forest "Ethermere Lake",
"Ethermire Lake lies shrouded in mist, its dark, still waters reflecting a sky perpetually overcast. Whispers ride the wind, and strange lights flicker beneath the surface, never breaking it.",
ecs.NilEntity(),
ecs.NilEntity(),
ecs.NilEntity(),
ecs.NilEntity(),
)
lake.West = forest graveyard := CreateRoom(
lake.North = cabin gw.World,
lake.South = graveyard "Graveyard",
"An overgrown graveyard shrouded in fog, with cracked headstones and leaning statues. The wind sighs through dead trees, and unseen footsteps echo faintly among the mossy graves.",
ecs.NilEntity(),
ecs.NilEntity(),
ecs.NilEntity(),
ecs.NilEntity(),
)
graveyard.North = forest chapel := CreateRoom(
graveyard.West = chapel gw.World,
graveyard.East = lake "Chapel of the Hollow Light",
"This ruined chapel leans under ivy and age. Faint light filters through shattered stained glass, casting broken rainbows across dust-choked pews and a long-silent altar.",
ecs.NilEntity(),
ecs.NilEntity(),
ecs.NilEntity(),
ecs.NilEntity(),
)
world.rooms = []*Room{ ecs.SetComponent(gw.World, forest, components.NeighborsComponent{
forest, North: cabin,
cabin, South: graveyard,
lake, East: lake,
graveyard, West: chapel,
chapel, })
}
world.defaultRoom = forest ecs.SetComponent(gw.World, cabin, components.NeighborsComponent{
South: graveyard,
West: chapel,
East: lake,
})
ecs.SetComponent(gw.World, chapel, components.NeighborsComponent{
North: cabin,
South: graveyard,
East: forest,
})
ecs.SetComponent(gw.World, lake, components.NeighborsComponent{
West: forest,
North: cabin,
South: graveyard,
})
ecs.SetComponent(gw.World, graveyard, components.NeighborsComponent{
North: forest,
West: chapel,
East: lake,
})
return return
} }
func (w *World) AddPlayerToDefaultRoom(p *Player) { // type World struct {
w.players[p.Identity()] = p // // rooms []*Room
w.defaultRoom.PlayerJoinRoom(p) // // players map[uuid.UUID]*Player
p.SetRoom(w.defaultRoom) // // defaultRoom *Room
}
func (w *World) RemovePlayerById(id uuid.UUID) { // entities []Entity
p, ok := w.players[id] // systems []*System
// components map[ComponentType]any
// }
if ok { // func CreateWorld() (world *World) {
p.currentRoom.PlayerLeaveRoom(p) // world = &World{
delete(w.players, id) // entities: []Entity{},
return // systems: []*System{},
} // components: map[ComponentType]any{},
} // }
// // world = &World{
// // players: map[uuid.UUID]*Player{},
// // }
func (w *World) FindPlayerById(id uuid.UUID) *Player { // // forest := CreateRoom(world, "Forest", "A dense, misty forest stretches endlessly, its towering trees whispering secrets through rustling leaves. Sunbeams filter through the canopy, dappling the mossy ground with golden light.")
p, ok := w.players[id] // // cabin := CreateRoom(world, "Wooden Cabin", "The cabins interior is cozy and rustic, with wooden beams overhead and a stone fireplace crackling warmly. A wool rug lies on creaky floorboards, and shelves brim with books, mugs, and old lanterns.")
// // lake := CreateRoom(world, "Ethermere Lake", "Ethermire Lake lies shrouded in mist, its dark, still waters reflecting a sky perpetually overcast. Whispers ride the wind, and strange lights flicker beneath the surface, never breaking it.")
// // graveyard := CreateRoom(world, "Graveyard", "An overgrown graveyard shrouded in fog, with cracked headstones and leaning statues. The wind sighs through dead trees, and unseen footsteps echo faintly among the mossy graves.")
// // chapel := CreateRoom(world, "Chapel of the Hollow Light", "This ruined chapel leans under ivy and age. Faint light filters through shattered stained glass, casting broken rainbows across dust-choked pews and a long-silent altar.")
if ok { // // forest.North = cabin
return p // // forest.South = graveyard
} else { // // forest.East = lake
return nil // // forest.West = chapel
}
} // // cabin.South = forest
// // cabin.West = chapel
// // cabin.East = lake
// // chapel.North = cabin
// // chapel.South = graveyard
// // chapel.East = forest
// // lake.West = forest
// // lake.North = cabin
// // lake.South = graveyard
// // graveyard.North = forest
// // graveyard.West = chapel
// // graveyard.East = lake
// // world.rooms = []*Room{
// // forest,
// // cabin,
// // lake,
// // graveyard,
// // chapel,
// // }
// // world.defaultRoom = forest
// return
// }
// func RegisterComponentType[T any](world *World, compType ComponentType) {
// world.components[compType] = CreateComponentStorage[any](compType)
// }
// func SetComponent[T](compType ComponentType, ent Entity, component any) {
// world.components[compType]
// }
// func (w *World) AddPlayerToDefaultRoom(p *Player) {
// w.players[p.Identity()] = p
// w.defaultRoom.PlayerJoinRoom(p)
// p.SetRoom(w.defaultRoom)
// }
// func (w *World) RemovePlayerById(id uuid.UUID) {
// p, ok := w.players[id]
// if ok {
// p.currentRoom.PlayerLeaveRoom(p)
// delete(w.players, id)
// return
// }
// }
// func (w *World) FindPlayerById(id uuid.UUID) *Player {
// p, ok := w.players[id]
// if ok {
// return p
// } else {
// return nil
// }
// }