package services import ( lucifer3 "git.aiterp.net/lucifer3/server" "git.aiterp.net/lucifer3/server/commands" "git.aiterp.net/lucifer3/server/device" "math/rand" "sync" "sync/atomic" "time" ) func NewEffectEnforcer(resolver device.Resolver, sceneMap device.SceneMap) lucifer3.ActiveService { s := &effectEnforcer{ resolver: resolver, sceneMap: sceneMap, list: make([]*effectEnforcerRun, 0, 16), index: make(map[string]*effectEnforcerRun, 8), } return s } type effectEnforcer struct { mu sync.Mutex resolver device.Resolver sceneMap device.SceneMap started uint32 list []*effectEnforcerRun index map[string]*effectEnforcerRun } func (s *effectEnforcer) Active() bool { return true } func (s *effectEnforcer) HandleEvent(_ *lucifer3.EventBus, _ lucifer3.Event) {} func (s *effectEnforcer) HandleCommand(bus *lucifer3.EventBus, command lucifer3.Command) { switch command := command.(type) { case commands.Assign: pointers := s.resolver.Resolve(command.Match) allowedIDs := make([]string, 0, len(pointers)) for _, ptr := range pointers { if s.sceneMap.SceneID(ptr.ID) == nil { allowedIDs = append(allowedIDs, ptr.ID) } } s.mu.Lock() // Create a new run newRun := &effectEnforcerRun{ due: time.Now(), ids: allowedIDs, effect: command.Effect, } s.list = append(s.list, newRun) // Remove the ids from any old run. for _, id := range allowedIDs { if oldRun := s.index[id]; oldRun != nil { oldRun.remove(id) } s.index[id] = newRun } s.mu.Unlock() if atomic.CompareAndSwapUint32(&s.started, 0, 1) { go s.runLoop(bus) } } } func (s *effectEnforcer) runLoop(bus *lucifer3.EventBus) { deleteList := make([]int, 0, 8) commandQueue := make([]commands.SetState, 0, 32) for now := range time.NewTicker(time.Millisecond * 100).C { s.mu.Lock() for i, run := range s.list { if run.dead { deleteList = append(deleteList, i-len(deleteList)) continue } if run.due.IsZero() || now.Before(run.due) { continue } r := rand.Int() for j, id := range run.ids { if id == "" { continue } state := run.effect.State(j, len(run.ids), run.round, r) commandQueue = append(commandQueue, commands.SetState{ ID: id, State: state, }) } if freq := run.effect.Frequency(); freq > 0 { if freq < 100*time.Millisecond { run.due = now } else { run.due = run.due.Add(freq) } run.round += 1 } else { run.due = time.Time{} } } if len(deleteList) > 0 { for _, i := range deleteList { deleteList = append(deleteList[:i], deleteList[i+1:]...) } deleteList = deleteList[:0] } s.mu.Unlock() if len(commandQueue) > 0 { for _, command := range commandQueue { bus.RunCommand(command) } commandQueue = commandQueue[:0] } } } type effectEnforcerRun struct { due time.Time ids []string effect lucifer3.Effect dead bool round int } // remove takes out an id from the effect, and returns whether the effect is empty. func (r *effectEnforcerRun) remove(id string) { anyLeft := false for i, id2 := range r.ids { if id2 == id { r.ids[i] = "" } else if id2 != "" { anyLeft = true } } if !anyLeft { r.dead = true } return }