Events, ECS almost there, systems for events
This commit is contained in:
parent
b2212a279c
commit
a18862a976
23 changed files with 614 additions and 475 deletions
4
go.mod
4
go.mod
|
@ -7,4 +7,6 @@ require (
|
|||
golang.org/x/term v0.32.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.33.0 // indirect
|
||||
require (
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
)
|
||||
|
|
2
go.sum
2
go.sum
|
@ -1,3 +1,5 @@
|
|||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package ecs
|
||||
|
||||
import (
|
||||
"iter"
|
||||
"maps"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/util"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
|
@ -50,6 +53,24 @@ func (cs *ComponentStorage[T]) ComponentType() ComponentType {
|
|||
return cs.forType
|
||||
}
|
||||
|
||||
func (cs *ComponentStorage[T]) Entities() iter.Seq[Entity] {
|
||||
return maps.Keys(cs.storage)
|
||||
}
|
||||
|
||||
func (cs *ComponentStorage[T]) Query(query func(comp T) bool) iter.Seq[Entity] {
|
||||
return func(yield func(Entity) bool) {
|
||||
for k, v := range cs.storage {
|
||||
if !query(v) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !yield(k) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *ComponentStorage[T]) Set(e Entity, component T) {
|
||||
cs.storage[e] = component
|
||||
}
|
||||
|
@ -85,7 +106,7 @@ func (s *System) Priority() int {
|
|||
return s.priority
|
||||
}
|
||||
|
||||
func (s *System) DoWork(world *World, delta time.Duration) {
|
||||
func (s *System) Execute(world *World, delta time.Duration) {
|
||||
err := s.work(world, delta)
|
||||
|
||||
if err != nil {
|
||||
|
@ -96,7 +117,6 @@ func (s *System) DoWork(world *World, delta time.Duration) {
|
|||
type World struct {
|
||||
systems []*System
|
||||
componentsByType map[ComponentType]any
|
||||
componentsByEntity map[Entity]map[ComponentType]any
|
||||
resources map[Resource]any
|
||||
}
|
||||
|
||||
|
@ -104,7 +124,6 @@ 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{},
|
||||
}
|
||||
|
||||
|
@ -113,17 +132,21 @@ func CreateWorld() (world *World) {
|
|||
|
||||
func (w *World) Tick(delta time.Duration) {
|
||||
for _, s := range w.systems {
|
||||
s.DoWork(w, delta)
|
||||
s.Execute(w, delta)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteEntity(world *World, entity Entity) {
|
||||
for _, s := range world.componentsByType {
|
||||
storage, ok := s.(*ComponentStorage[Component])
|
||||
storage := s.(*ComponentStorage[Component])
|
||||
|
||||
if ok {
|
||||
storage.Delete(entity)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteEntities(world *World, entities ...Entity) {
|
||||
for _, e := range entities {
|
||||
DeleteEntity(world, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,19 +175,20 @@ func RemoveResource(world *World, r Resource) {
|
|||
delete(world.resources, r)
|
||||
}
|
||||
|
||||
func RegisterComponent[T Component](world *World, compType ComponentType) {
|
||||
func registerComponent[T Component](world *World, compType ComponentType) {
|
||||
if _, ok := world.componentsByType[compType]; ok {
|
||||
return
|
||||
}
|
||||
|
||||
world.componentsByType[compType] = CreateComponentStorage[T](compType)
|
||||
}
|
||||
|
||||
func SetComponent[T Component](world *World, entity Entity, component T) {
|
||||
registerComponent[T](world, component.Type())
|
||||
|
||||
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) {
|
||||
|
@ -182,7 +206,59 @@ func DeleteComponent[T Component](world *World, entity Entity) {
|
|||
func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage[T]) {
|
||||
var zero T
|
||||
|
||||
return world.componentsByType[zero.Type()].(*ComponentStorage[T])
|
||||
compType := zero.Type()
|
||||
|
||||
registerComponent[T](world, compType)
|
||||
|
||||
return world.componentsByType[compType].(*ComponentStorage[T])
|
||||
}
|
||||
|
||||
func IterateEntitiesWithComponent[T Component](world *World) iter.Seq[Entity] {
|
||||
storage := GetComponentStorage[T](world)
|
||||
|
||||
return storage.Entities()
|
||||
}
|
||||
|
||||
func QueryEntitiesWithComponent[T Component](world *World, query func(comp T) bool) iter.Seq[Entity] {
|
||||
storage := GetComponentStorage[T](world)
|
||||
|
||||
return storage.Query(query)
|
||||
}
|
||||
|
||||
func FindEntitiesWithComponents(world *World, componentTypes ...ComponentType) (entities []Entity) {
|
||||
entities = []Entity{}
|
||||
|
||||
isFirst := true
|
||||
|
||||
for _, compType := range componentTypes {
|
||||
// If we've gone through at least one component, and we have an empty result already, return it
|
||||
if !isFirst && len(entities) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
storage, ok := world.componentsByType[compType].(*ComponentStorage[Component])
|
||||
|
||||
// If we can't find the storage for this component, then it hasn't been used yet.
|
||||
// Therefore, no entity could have all components requested. Return empty.
|
||||
if !ok {
|
||||
return []Entity{}
|
||||
}
|
||||
|
||||
// For the first component, simply add all entities to the array
|
||||
if isFirst {
|
||||
for entity := range storage.Entities() {
|
||||
entities = append(entities, entity)
|
||||
}
|
||||
|
||||
isFirst = false
|
||||
continue
|
||||
}
|
||||
|
||||
// For later components, intersect
|
||||
entities = util.IntersectSliceWithIterator(entities, storage.Entities())
|
||||
}
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
func RegisterSystem(world *World, s *System) {
|
||||
|
@ -194,3 +270,9 @@ func RegisterSystem(world *World, s *System) {
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
func RegisterSystems(world *World, systems ...*System) {
|
||||
for _, s := range systems {
|
||||
RegisterSystem(world, s)
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
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
|
||||
}
|
61
internal/game/data/common.go
Normal file
61
internal/game/data/common.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
TypeName ecs.ComponentType = iota
|
||||
TypeDescription
|
||||
TypePlayerState
|
||||
TypeInRoom
|
||||
TypeNeighbors
|
||||
TypeIsRoom
|
||||
TypeIsPlayer
|
||||
TypeCommandString
|
||||
TypeEntity
|
||||
TypeEvent
|
||||
TypeConnectionId
|
||||
TypeContents
|
||||
)
|
||||
|
||||
type EntityComponent struct {
|
||||
Entity ecs.Entity
|
||||
}
|
||||
|
||||
func (e EntityComponent) Type() ecs.ComponentType {
|
||||
return TypeEntity
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type CommandStringComponent struct {
|
||||
Command string
|
||||
}
|
||||
|
||||
func (cs CommandStringComponent) Type() ecs.ComponentType {
|
||||
return TypeCommandString
|
||||
}
|
||||
|
||||
type ConnectionIdComponent struct {
|
||||
ConnectionId uuid.UUID
|
||||
}
|
||||
|
||||
func (cid ConnectionIdComponent) Type() ecs.ComponentType {
|
||||
return TypeConnectionId
|
||||
}
|
159
internal/game/data/event.go
Normal file
159
internal/game/data/event.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventPlayerConnect EventType = "PlayerConnect"
|
||||
EventPlayerDisconnect = "PlayerDisconnect"
|
||||
EventPlayerCommand = "PlayerCommand"
|
||||
EventPlayerSpeak = "PlayerSpeak"
|
||||
)
|
||||
|
||||
type EventComponent struct {
|
||||
EventType EventType
|
||||
}
|
||||
|
||||
func (is EventComponent) Type() ecs.ComponentType {
|
||||
return TypeEvent
|
||||
}
|
||||
|
||||
func CreatePlayerConnectEvent(world *ecs.World, connectionId uuid.UUID) {
|
||||
event := ecs.NewEntity()
|
||||
|
||||
ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerConnect})
|
||||
ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId})
|
||||
}
|
||||
|
||||
func CreatePlayerDisconnectEvent(world *ecs.World, connectionId uuid.UUID) {
|
||||
event := ecs.NewEntity()
|
||||
|
||||
ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerDisconnect})
|
||||
ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId})
|
||||
}
|
||||
|
||||
func CreatePlayerCommandEvent(world *ecs.World, connectionId uuid.UUID, command string) {
|
||||
event := ecs.NewEntity()
|
||||
|
||||
ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerCommand})
|
||||
ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId})
|
||||
ecs.SetComponent(world, event, CommandStringComponent{Command: command})
|
||||
}
|
||||
|
||||
// type PlayerJoinEvent struct {
|
||||
// connectionId uuid.UUID
|
||||
// }
|
||||
|
||||
// func (game *LastMUDGame) CreatePlayerJoinEvent(connId uuid.UUID) *PlayerJoinEvent {
|
||||
// return &PlayerJoinEvent{
|
||||
// connectionId: connId,
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (pje *PlayerJoinEvent) Type() event.EventType {
|
||||
// return PlayerJoin
|
||||
// }
|
||||
|
||||
// func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||
// p, err := CreatePlayer(game.world.World, pje.connectionId, components.PlayerStateJoining)
|
||||
|
||||
// if err != nil {
|
||||
// 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 {
|
||||
// connectionId uuid.UUID
|
||||
// }
|
||||
|
||||
// func (game *LastMUDGame) CreatePlayerLeaveEvent(connId uuid.UUID) *PlayerLeaveEvent {
|
||||
// return &PlayerLeaveEvent{
|
||||
// connectionId: connId,
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (ple *PlayerLeaveEvent) Type() event.EventType {
|
||||
// return PlayerLeave
|
||||
// }
|
||||
|
||||
// func (ple *PlayerLeaveEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||
// ecs.DeleteEntity(game.world.World, ecs.CreateEntity(ple.connectionId))
|
||||
// }
|
||||
|
||||
// type PlayerCommandEvent struct {
|
||||
// connectionId uuid.UUID
|
||||
// command *command.CommandContext
|
||||
// }
|
||||
|
||||
// func (game *LastMUDGame) CreatePlayerCommandEvent(connId uuid.UUID, cmdString string) (event *PlayerCommandEvent, err error) {
|
||||
// cmdCtx, err := command.CreateCommandContext(game.commandRegistry(), cmdString)
|
||||
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// event = &PlayerCommandEvent{
|
||||
// connectionId: connId,
|
||||
// command: cmdCtx,
|
||||
// }
|
||||
|
||||
// return
|
||||
// }
|
||||
|
||||
// func (pce *PlayerCommandEvent) Type() event.EventType {
|
||||
// return PlayerCommand
|
||||
// }
|
||||
|
||||
// func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||
// if player == nil {
|
||||
// logging.Error("Unable to handle player command from player with id", pce.connectionId, ": Player does not exist")
|
||||
// return
|
||||
// }
|
||||
|
||||
// event := pce.parseCommandIntoEvent(game, player)
|
||||
// }
|
||||
|
||||
// func (pce *PlayerCommandEvent) parseCommandIntoEvent(game *LastMUDGame, player ecs.Entity) event.Event {
|
||||
// switch pce.command.Command().Definition().Name() {
|
||||
// case SayCommand:
|
||||
// speech, err := pce.command.Command().Parameters()[0].AsString()
|
||||
|
||||
// if err != nil {
|
||||
// logging.Error("Unable to handle player speech from player with id", pce.connectionId, ": Speech could not be parsed: ", err.Error())
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// return game.CreatePlayerSayEvent(player, speech)
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// type PlayerSayEvent struct {
|
||||
// player *Player
|
||||
// speech string
|
||||
// }
|
||||
|
||||
// func (game *LastMUDGame) CreatePlayerSayEvent(player *Player, speech string) *PlayerSayEvent {
|
||||
// return &PlayerSayEvent{
|
||||
// player: player,
|
||||
// speech: speech,
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (pse *PlayerSayEvent) Type() EventType {
|
||||
// return PlayerSpeak
|
||||
// }
|
||||
|
||||
// func (pse *PlayerSayEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||
// for _, p := range pse.player.CurrentRoom().Players() {
|
||||
// game.enqeueOutput(game.CreateOutput(p.Identity(), []byte(pse.player.id.String()+" in "+pse.player.CurrentRoom().Name+": "+pse.speech)))
|
||||
// }
|
||||
// }
|
21
internal/game/data/output.go
Normal file
21
internal/game/data/output.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ContentsComponent struct {
|
||||
Contents []byte
|
||||
}
|
||||
|
||||
func (cc ContentsComponent) Type() ecs.ComponentType {
|
||||
return TypeContents
|
||||
}
|
||||
|
||||
func CreateGameOutput(world *ecs.World, connectionId uuid.UUID, contents []byte) {
|
||||
gameOutput := ecs.NewEntity()
|
||||
|
||||
ecs.SetComponent(world, gameOutput, ConnectionIdComponent{ConnectionId: connectionId})
|
||||
ecs.SetComponent(world, gameOutput, ContentsComponent{Contents: contents})
|
||||
}
|
56
internal/game/data/player.go
Normal file
56
internal/game/data/player.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func CreatePlayer(world *ecs.World, id uuid.UUID, state PlayerState) (entity ecs.Entity, err error) {
|
||||
entity = ecs.NewEntity()
|
||||
|
||||
defaultRoom, err := ecs.GetResource[ecs.Entity](world, ResourceDefaultRoom)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ecs.SetComponent(world, entity, ConnectionIdComponent{ConnectionId: id})
|
||||
ecs.SetComponent(world, entity, PlayerStateComponent{State: state})
|
||||
ecs.SetComponent(world, entity, NameComponent{Name: id.String()})
|
||||
ecs.SetComponent(world, entity, InRoomComponent{Room: defaultRoom})
|
||||
ecs.SetComponent(world, entity, IsPlayerComponent{})
|
||||
|
||||
return
|
||||
}
|
33
internal/game/data/room.go
Normal file
33
internal/game/data/room.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package data
|
||||
|
||||
import "code.haedhutner.dev/mvv/LastMUD/internal/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
|
||||
}
|
||||
|
||||
func CreateRoom(
|
||||
world *ecs.World,
|
||||
name, description string,
|
||||
north, south, east, west ecs.Entity,
|
||||
) ecs.Entity {
|
||||
entity := ecs.NewEntity()
|
||||
|
||||
ecs.SetComponent(world, entity, IsRoomComponent{})
|
||||
ecs.SetComponent(world, entity, NameComponent{Name: name})
|
||||
ecs.SetComponent(world, entity, DescriptionComponent{Description: description})
|
||||
ecs.SetComponent(world, entity, NeighborsComponent{North: north, South: south, East: east, West: west})
|
||||
|
||||
return entity
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
package game
|
||||
package data
|
||||
|
||||
import (
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/components"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/ecs"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -70,32 +69,32 @@ func CreateGameWorld() (gw *GameWorld) {
|
|||
ecs.NilEntity(),
|
||||
)
|
||||
|
||||
ecs.SetComponent(gw.World, forest, components.NeighborsComponent{
|
||||
ecs.SetComponent(gw.World, forest, NeighborsComponent{
|
||||
North: cabin,
|
||||
South: graveyard,
|
||||
East: lake,
|
||||
West: chapel,
|
||||
})
|
||||
|
||||
ecs.SetComponent(gw.World, cabin, components.NeighborsComponent{
|
||||
ecs.SetComponent(gw.World, cabin, NeighborsComponent{
|
||||
South: graveyard,
|
||||
West: chapel,
|
||||
East: lake,
|
||||
})
|
||||
|
||||
ecs.SetComponent(gw.World, chapel, components.NeighborsComponent{
|
||||
ecs.SetComponent(gw.World, chapel, NeighborsComponent{
|
||||
North: cabin,
|
||||
South: graveyard,
|
||||
East: forest,
|
||||
})
|
||||
|
||||
ecs.SetComponent(gw.World, lake, components.NeighborsComponent{
|
||||
ecs.SetComponent(gw.World, lake, NeighborsComponent{
|
||||
West: forest,
|
||||
North: cabin,
|
||||
South: graveyard,
|
||||
})
|
||||
|
||||
ecs.SetComponent(gw.World, graveyard, components.NeighborsComponent{
|
||||
ecs.SetComponent(gw.World, graveyard, NeighborsComponent{
|
||||
North: forest,
|
||||
West: chapel,
|
||||
East: lake,
|
|
@ -1,39 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||
)
|
||||
|
||||
type EventType int
|
||||
|
||||
const (
|
||||
PlayerJoin EventType = iota
|
||||
PlayerCommand
|
||||
PlayerLeave
|
||||
|
||||
PlayerSpeak
|
||||
)
|
||||
|
||||
func (et EventType) String() string {
|
||||
switch et {
|
||||
case PlayerCommand:
|
||||
return "PlayerCommand"
|
||||
case PlayerJoin:
|
||||
return "PlayerJoin"
|
||||
case PlayerLeave:
|
||||
return "PlayerLeave"
|
||||
case PlayerSpeak:
|
||||
return "PlayerSpeak"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type GameEvent interface {
|
||||
Type() EventType
|
||||
Handle(game *LastMUDGame, delta time.Duration)
|
||||
}
|
||||
|
||||
func stringifyEvent(ev GameEvent) string {
|
||||
return ev.Type().String() + fmt.Sprintf(`%+v`, ev)
|
||||
}
|
||||
|
||||
type EventBus struct {
|
||||
events chan GameEvent
|
||||
}
|
||||
|
||||
func CreateEventBus(capacity int) *EventBus {
|
||||
return &EventBus{
|
||||
events: make(chan GameEvent, capacity),
|
||||
}
|
||||
}
|
||||
|
||||
func (eb *EventBus) HasNext() bool {
|
||||
return len(eb.events) > 0
|
||||
}
|
||||
|
||||
func (eb *EventBus) Pop() (event GameEvent) {
|
||||
select {
|
||||
case event := <-eb.events:
|
||||
logging.Debug("Popped event ", stringifyEvent(event))
|
||||
return event
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (eb *EventBus) Push(event GameEvent) {
|
||||
eb.events <- event
|
||||
logging.Debug("Enqueued event ", stringifyEvent(event))
|
||||
}
|
||||
|
||||
func (eb *EventBus) close() {
|
||||
close(eb.events)
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
package game
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PlayerJoinEvent struct {
|
||||
connectionId uuid.UUID
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) CreatePlayerJoinEvent(connId uuid.UUID) *PlayerJoinEvent {
|
||||
return &PlayerJoinEvent{
|
||||
connectionId: connId,
|
||||
}
|
||||
}
|
||||
|
||||
func (pje *PlayerJoinEvent) Type() EventType {
|
||||
return PlayerJoin
|
||||
}
|
||||
|
||||
func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||
p, err := CreatePlayer(game.world.World, pje.connectionId, components.PlayerStateJoining)
|
||||
|
||||
if err != nil {
|
||||
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 {
|
||||
connectionId uuid.UUID
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) CreatePlayerLeaveEvent(connId uuid.UUID) *PlayerLeaveEvent {
|
||||
return &PlayerLeaveEvent{
|
||||
connectionId: connId,
|
||||
}
|
||||
}
|
||||
|
||||
func (ple *PlayerLeaveEvent) Type() EventType {
|
||||
return PlayerLeave
|
||||
}
|
||||
|
||||
func (ple *PlayerLeaveEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||
ecs.DeleteEntity(game.world.World, ecs.CreateEntity(ple.connectionId))
|
||||
}
|
||||
|
||||
type PlayerCommandEvent struct {
|
||||
connectionId uuid.UUID
|
||||
command *command.CommandContext
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) CreatePlayerCommandEvent(connId uuid.UUID, cmdString string) (event *PlayerCommandEvent, err error) {
|
||||
cmdCtx, err := command.CreateCommandContext(game.commandRegistry(), cmdString)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event = &PlayerCommandEvent{
|
||||
connectionId: connId,
|
||||
command: cmdCtx,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (pce *PlayerCommandEvent) Type() EventType {
|
||||
return PlayerCommand
|
||||
}
|
||||
|
||||
func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||
if player == nil {
|
||||
logging.Error("Unable to handle player command from player with id", pce.connectionId, ": Player does not exist")
|
||||
return
|
||||
}
|
||||
|
||||
event := pce.parseCommandIntoEvent(game, player)
|
||||
}
|
||||
|
||||
func (pce *PlayerCommandEvent) parseCommandIntoEvent(game *LastMUDGame, player ecs.Entity) GameEvent {
|
||||
switch pce.command.Command().Definition().Name() {
|
||||
case SayCommand:
|
||||
speech, err := pce.command.Command().Parameters()[0].AsString()
|
||||
|
||||
if err != nil {
|
||||
logging.Error("Unable to handle player speech from player with id", pce.connectionId, ": Speech could not be parsed: ", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return game.CreatePlayerSayEvent(player, speech)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type PlayerSayEvent struct {
|
||||
player *Player
|
||||
speech string
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) CreatePlayerSayEvent(player *Player, speech string) *PlayerSayEvent {
|
||||
return &PlayerSayEvent{
|
||||
player: player,
|
||||
speech: speech,
|
||||
}
|
||||
}
|
||||
|
||||
func (pse *PlayerSayEvent) Type() EventType {
|
||||
return PlayerSpeak
|
||||
}
|
||||
|
||||
func (pse *PlayerSayEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||
for _, p := range pse.player.CurrentRoom().Players() {
|
||||
game.enqeueOutput(game.CreateOutput(p.Identity(), []byte(pse.player.id.String()+" in "+pse.player.CurrentRoom().Name+": "+pse.speech)))
|
||||
}
|
||||
}
|
|
@ -5,7 +5,10 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/command"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/systems"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
@ -13,7 +16,6 @@ import (
|
|||
const TickRate = time.Duration(50 * time.Millisecond)
|
||||
|
||||
const MaxEnqueuedOutputPerTick = 100
|
||||
const MaxEnqueuedGameEventsPerTick = 100
|
||||
|
||||
type GameOutput struct {
|
||||
connId uuid.UUID
|
||||
|
@ -41,9 +43,7 @@ type LastMUDGame struct {
|
|||
|
||||
cmdRegistry *command.CommandRegistry
|
||||
|
||||
world *GameWorld
|
||||
|
||||
eventBus *EventBus
|
||||
world *data.GameWorld
|
||||
|
||||
output chan GameOutput
|
||||
}
|
||||
|
@ -52,11 +52,12 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
|
|||
game = &LastMUDGame{
|
||||
wg: wg,
|
||||
ctx: ctx,
|
||||
eventBus: CreateEventBus(MaxEnqueuedGameEventsPerTick),
|
||||
output: make(chan GameOutput, MaxEnqueuedOutputPerTick),
|
||||
world: CreateGameWorld(),
|
||||
world: data.CreateGameWorld(),
|
||||
}
|
||||
|
||||
ecs.RegisterSystems(game.world.World, systems.CreateEventSystems()...)
|
||||
|
||||
game.cmdRegistry = game.CreateGameCommandRegistry()
|
||||
|
||||
wg.Add(1)
|
||||
|
@ -65,10 +66,6 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
|
|||
return
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) EnqueueEvent(event GameEvent) {
|
||||
game.eventBus.Push(event)
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
|
||||
select {
|
||||
case output := <-game.output:
|
||||
|
@ -78,6 +75,18 @@ func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
|
|||
}
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) ConnectPlayer(connectionId uuid.UUID) {
|
||||
data.CreatePlayerConnectEvent(game.world.World, connectionId)
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) DisconnectPlayer(connectionId uuid.UUID) {
|
||||
data.CreatePlayerDisconnectEvent(game.world.World, connectionId)
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) SendPlayerCommand(connectionId uuid.UUID, command string) {
|
||||
data.CreatePlayerCommandEvent(game.world.World, connectionId, command)
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) commandRegistry() *command.CommandRegistry {
|
||||
return game.cmdRegistry
|
||||
}
|
||||
|
@ -108,10 +117,23 @@ func (game *LastMUDGame) start() {
|
|||
}
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) consumeOutputs() {
|
||||
entities := ecs.FindEntitiesWithComponents(game.world.World, data.TypeConnectionId, data.TypeContents)
|
||||
|
||||
for _, entity := range entities {
|
||||
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](game.world.World, entity)
|
||||
contents, _ := ecs.GetComponent[data.ContentsComponent](game.world.World, entity)
|
||||
|
||||
game.enqeueOutput(GameOutput{
|
||||
connId: connId.ConnectionId,
|
||||
contents: contents.Contents,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) shutdown() {
|
||||
logging.Info("Stopping LastMUD...")
|
||||
close(game.output)
|
||||
game.eventBus.close()
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) shouldStop() bool {
|
||||
|
@ -128,13 +150,6 @@ func (game *LastMUDGame) enqeueOutput(output GameOutput) {
|
|||
}
|
||||
|
||||
func (g *LastMUDGame) tick(delta time.Duration) {
|
||||
for {
|
||||
event := g.eventBus.Pop()
|
||||
|
||||
if event == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event.Handle(g, delta)
|
||||
}
|
||||
g.world.Tick(delta)
|
||||
g.consumeOutputs()
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
package game
|
||||
|
||||
// import "github.com/google/uuid"
|
||||
|
||||
// type Player struct {
|
||||
// id uuid.UUID
|
||||
|
||||
// state PlayerState
|
||||
|
||||
// currentRoom *Room
|
||||
// }
|
||||
|
||||
// func CreateJoiningPlayer(identity uuid.UUID) *Player {
|
||||
// return &Player{
|
||||
// id: identity,
|
||||
// state: PlayerStateJoining,
|
||||
// currentRoom: nil,
|
||||
// }
|
||||
// }
|
||||
|
||||
// func CreatePlayer(identity uuid.UUID, state PlayerState, room *Room) *Player {
|
||||
// return &Player{
|
||||
// id: identity,
|
||||
// state: state,
|
||||
// currentRoom: room,
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (p *Player) Identity() uuid.UUID {
|
||||
// return p.id
|
||||
// }
|
||||
|
||||
// func (p *Player) SetRoom(r *Room) {
|
||||
// p.currentRoom = r
|
||||
// }
|
||||
|
||||
// func (p *Player) CurrentRoom() *Room {
|
||||
// return p.currentRoom
|
||||
// }
|
|
@ -1,51 +0,0 @@
|
|||
package game
|
||||
|
||||
// import "github.com/google/uuid"
|
||||
|
||||
// type RoomPlayer interface {
|
||||
// Identity() uuid.UUID
|
||||
// SetRoom(room *Room)
|
||||
// }
|
||||
|
||||
// type Room struct {
|
||||
// world *World
|
||||
|
||||
// North *Room
|
||||
// South *Room
|
||||
// East *Room
|
||||
// West *Room
|
||||
|
||||
// Name string
|
||||
// Description string
|
||||
|
||||
// players map[uuid.UUID]RoomPlayer
|
||||
// }
|
||||
|
||||
// func CreateRoom(world *World, name, description string) *Room {
|
||||
// return &Room{
|
||||
// world: world,
|
||||
// Name: name,
|
||||
// Description: description,
|
||||
// players: map[uuid.UUID]RoomPlayer{},
|
||||
// }
|
||||
// }
|
||||
|
||||
// func (r *Room) PlayerJoinRoom(player RoomPlayer) (err error) {
|
||||
// r.players[player.Identity()] = player
|
||||
|
||||
// return
|
||||
// }
|
||||
|
||||
// func (r *Room) PlayerLeaveRoom(player RoomPlayer) (err error) {
|
||||
// delete(r.players, player.Identity())
|
||||
|
||||
// return
|
||||
// }
|
||||
|
||||
// func (r *Room) Players() map[uuid.UUID]RoomPlayer {
|
||||
// return r.players
|
||||
// }
|
||||
|
||||
// func (r *Room) World() *World {
|
||||
// return r.world
|
||||
// }
|
|
@ -1 +0,0 @@
|
|||
package game
|
70
internal/game/systems/event.go
Normal file
70
internal/game/systems/event.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package systems
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||
)
|
||||
|
||||
type eventError struct {
|
||||
err string
|
||||
}
|
||||
|
||||
func createEventError(v ...any) *eventError {
|
||||
return &eventError{
|
||||
err: fmt.Sprint(v...),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *eventError) Error() string {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func EventTypeQuery(eventType data.EventType) func(comp data.EventComponent) bool {
|
||||
return func(comp data.EventComponent) bool {
|
||||
return comp.EventType == eventType
|
||||
}
|
||||
}
|
||||
|
||||
func CreateEventSystems() []*ecs.System {
|
||||
return []*ecs.System{
|
||||
ecs.CreateSystem("PlayerConnectEventHandlerSystem", 0, handlePlayerConnectEvents),
|
||||
}
|
||||
}
|
||||
|
||||
func handlePlayerConnectEvents(world *ecs.World, delta time.Duration) (err error) {
|
||||
events := ecs.QueryEntitiesWithComponent(world, EventTypeQuery(data.EventPlayerConnect))
|
||||
processedEvents := []ecs.Entity{}
|
||||
|
||||
for event := range events {
|
||||
err = handlePlayerConnectEvent(world, event)
|
||||
|
||||
if err != nil {
|
||||
logging.Error("PlayerConnect Error: ", err)
|
||||
}
|
||||
|
||||
processedEvents = append(processedEvents, event)
|
||||
}
|
||||
|
||||
ecs.DeleteEntities(world, processedEvents...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handlePlayerConnectEvent(world *ecs.World, entity ecs.Entity) (err error) {
|
||||
logging.Warn("Player connect")
|
||||
|
||||
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](world, entity)
|
||||
|
||||
if !ok {
|
||||
return createEventError("Event does not contain connectionId")
|
||||
}
|
||||
|
||||
data.CreatePlayer(world, connectionId.ConnectionId, data.PlayerStateJoining)
|
||||
data.CreateGameOutput(world, connectionId.ConnectionId, []byte("Welcome to LastMUD!"))
|
||||
|
||||
return
|
||||
}
|
|
@ -48,7 +48,7 @@ func CreateConnection(server *Server, conn *net.TCPConn, ctx context.Context, wg
|
|||
go c.listen()
|
||||
go c.checkAlive()
|
||||
|
||||
server.game().EnqueueEvent(server.game().CreatePlayerJoinEvent(c.Id()))
|
||||
server.game().ConnectPlayer(c.Id())
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -79,13 +79,7 @@ func (c *Connection) listen() {
|
|||
break
|
||||
}
|
||||
|
||||
event, err := c.server.game().CreatePlayerCommandEvent(c.Id(), message)
|
||||
|
||||
if err != nil {
|
||||
c.Write([]byte(err.Error()))
|
||||
} else {
|
||||
c.server.game().EnqueueEvent(event)
|
||||
}
|
||||
c.server.game().SendPlayerCommand(c.Id(), message)
|
||||
|
||||
c.lastSeen = time.Now()
|
||||
}
|
||||
|
@ -128,7 +122,7 @@ func (c *Connection) shouldClose() bool {
|
|||
func (c *Connection) closeConnection() {
|
||||
c.conn.Close()
|
||||
|
||||
c.server.game().EnqueueEvent(c.server.game().CreatePlayerLeaveEvent(c.Id()))
|
||||
c.server.game().DisconnectPlayer(c.Id())
|
||||
|
||||
logging.Info("Disconnected: ", c.conn.RemoteAddr())
|
||||
}
|
||||
|
|
57
internal/util/slices.go
Normal file
57
internal/util/slices.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package util
|
||||
|
||||
import "iter"
|
||||
|
||||
// Hash-based intersection of slices
|
||||
func IntersectSlices[T comparable](a []T, b []T) []T {
|
||||
set := make([]T, 0)
|
||||
hash := make(map[T]struct{})
|
||||
|
||||
for _, v := range a {
|
||||
hash[v] = struct{}{}
|
||||
}
|
||||
|
||||
for _, v := range b {
|
||||
if _, ok := hash[v]; ok {
|
||||
set = append(set, v)
|
||||
}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
// Hash-based intersection of iterators
|
||||
func IntersectIterators[T comparable](a, b iter.Seq[T]) []T {
|
||||
set := make([]T, 0)
|
||||
hash := make(map[T]struct{})
|
||||
|
||||
for v := range a {
|
||||
hash[v] = struct{}{}
|
||||
}
|
||||
|
||||
for v := range b {
|
||||
if _, ok := hash[v]; ok {
|
||||
set = append(set, v)
|
||||
}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
// Hash-based intersection of iterator and slice
|
||||
func IntersectSliceWithIterator[T comparable](a []T, b iter.Seq[T]) []T {
|
||||
set := make([]T, 0)
|
||||
hash := make(map[T]struct{})
|
||||
|
||||
for _, v := range a {
|
||||
hash[v] = struct{}{}
|
||||
}
|
||||
|
||||
for v := range b {
|
||||
if _, ok := hash[v]; ok {
|
||||
set = append(set, v)
|
||||
}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
Loading…
Add table
Reference in a new issue