diff --git a/common/cool/func.go b/common/cool/func.go index 2e69285da..426f92f31 100644 --- a/common/cool/func.go +++ b/common/cool/func.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/gogf/gf/v2/database/gredis" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/text/gstr" @@ -158,17 +159,32 @@ func ListenFunc(ctx g.Ctx) { // 处理消息(保留原有业务逻辑) if data != nil { - dataMap := data.MapStrStr() - if dataMap["Kind"] == "subscribe" { + dataMap, ok := data.Interface().(*gredis.Message) + if !ok { continue } - if dataMap["Channel"] == subscribeTopic { - Logger.Debug(ctx, "执行函数", "payload", dataMap["Payload"]) - err := RunFunc(ctx, dataMap["Payload"]) + // if dataMap. == "subscribe" { + // continue + // } + if dataMap.Channel == subscribeTopic { + Logger.Debug(ctx, "执行函数", "payload", dataMap.Payload) + err := RunFunc(ctx, dataMap.Payload) if err != nil { - Logger.Error(ctx, "执行函数失败", "payload", dataMap["Payload"], "error", err) + Logger.Error(ctx, "执行函数失败", "payload", dataMap.Payload, "error", err) } } + if dataMap.Channel == "sun:join:2458" { + + println("收到sun:join:2458", dataMap.Payload) + //universalClient, _ := g.Redis("cool").Client().(goredis.UniversalClient) + zs.Add(1001, User{ID: 1001, JoinTime: time.Now().Unix(), Score: 1000}) + if zs.Length() > 2 { + zs.FindNext(func(i User) bool { return i.ID > 1000 }) + //找到上一个,如果区间分数少于一定, + //直接进行匹配 + } + + } } } diff --git a/common/cool/user.go b/common/cool/user.go new file mode 100644 index 000000000..41db76fba --- /dev/null +++ b/common/cool/user.go @@ -0,0 +1,25 @@ +package cool + +import "github.com/liwnn/zset" + +type User struct { + JoinTime int64 + ID int + Score int +} + +func (u User) Key() uint32 { + return uint32(u.ID) +} + +// 如果分数不对的话,就按时间排序 +func (u User) Less(than User) bool { + if u.Score == than.Score { + return u.JoinTime < than.JoinTime + } + return u.Score < than.Score +} + +var zs = zset.New[uint32, User](func(a, b User) bool { + return a.Less(b) +}) diff --git a/common/utils/zset/README.md b/common/utils/zset/README.md new file mode 100644 index 000000000..9f775e862 --- /dev/null +++ b/common/utils/zset/README.md @@ -0,0 +1,177 @@ +# ZSet +This Go package provides an implementation of sorted set in redis. + +## Usage (go < 1.18) +All you have to do is to implement a comparison `function Less(Item) bool` and a `function Key() string` for your Item which will be store in the zset, here are some examples. +``` go +package main + +import ( + "fmt" + + "github.com/liwnn/zset" +) + +type User struct { + Name string + Score int +} + +func (u User) Key() string { + return u.Name +} + +func (u User) Less(than zset.Item) bool { + if u.Score == than.(User).Score { + return u.Name < than.(User).Name + } + return u.Score < than.(User).Score +} + +func main() { + zs := zset.New() + + // Add + zs.Add("Hurst", User{Name: "Hurst", Score: 88}) + zs.Add("Peek", User{Name: "Peek", Score: 100}) + zs.Add("Beaty", User{Name: "Beaty", Score: 66}) + + // Rank + rank := zs.Rank("Hurst", true) + fmt.Printf("Hurst's rank is %v\n", rank) // expected 2 + + // Range + fmt.Println() + fmt.Println("Range[0,3]:") + zs.Range(0, 3, true, func(v zset.Item, rank int) bool { + fmt.Printf("%v's rank is %v\n", v.(User).Key(), rank) + return true + }) + + // Range with Iterator + fmt.Println() + fmt.Println("Range[0,3] with Iterator:") + for it := zs.RangeIterator(0, 3, true); it.Valid(); it.Next() { + fmt.Printf("Ite: %v's rank is %v\n", it.Item().(User).Key(), it.Rank()) + } + + // Range by score [88, 100] + fmt.Println() + fmt.Println("RangeByScore[88,100]:") + zs.RangeByScore(func(i zset.Item) bool { + return i.(User).Score >= 88 + }, func(i zset.Item) bool { + return i.(User).Score <= 100 + }, true, func(i zset.Item, rank int) bool { + fmt.Printf("%v's score[%v] rank is %v\n", i.(User).Key(), i.(User).Score, rank) + return true + }) + + // Remove + zs.Remove("Peek") + + // Rank + fmt.Println() + fmt.Println("After remove Peek:") + rank = zs.Rank("Hurst", true) + fmt.Printf("Hurst's rank is %v\n", rank) // expected 1 +} +``` +Output: +``` +Hurst's rank is 2 + +Range[0,3]: +Peek's rank is 1 +Hurst's rank is 2 +Beaty's rank is 3 + +Range[0,3] with Iterator: +Ite: Peek's rank is 1 +Ite: Hurst's rank is 2 +Ite: Beaty's rank is 3 + +RangeByScore[88,100]: +Peek's score[100] rank is 1 +Hurst's score[88] rank is 2 + +After remove Peek: +Hurst's rank is 1 +``` +## Usage (go >= 1.18) +``` go +package main + +import ( + "fmt" + + "github.com/liwnn/zset" +) + +type User struct { + Name string + Score int +} + +func (u User) Key() string { + return u.Name +} + +func (u User) Less(than User) bool { + if u.Score == than.Score { + return u.Name < than.Name + } + return u.Score < than.Score +} + +func main() { + zs := zset.New[string, User](func(a, b User) bool { + return a.Less(b) + }) + + // Add + zs.Add("Hurst", User{Name: "Hurst", Score: 88}) + zs.Add("Peek", User{Name: "Peek", Score: 100}) + zs.Add("Beaty", User{Name: "Beaty", Score: 66}) + + // Rank + rank := zs.Rank("Hurst", true) + fmt.Printf("Hurst's rank is %v\n", rank) // expected 2 + + // Range + fmt.Println() + fmt.Println("Range[0,3]:") + zs.Range(0, 3, true, func(v User, rank int) bool { + fmt.Printf("%v's rank is %v\n", v.Key(), rank) + return true + }) + + // Range with Iterator + fmt.Println() + fmt.Println("Range[0,3] with Iterator:") + for it := zs.RangeIterator(0, 3, true); it.Valid(); it.Next() { + fmt.Printf("Ite: %v's rank is %v\n", it.Item().Key(), it.Rank()) + } + + // Range by score [88, 100] + fmt.Println() + fmt.Println("RangeByScore[88,100]:") + zs.RangeByScore(func(i User) bool { + return i.Score >= 88 + }, func(i User) bool { + return i.Score <= 100 + }, true, func(i User, rank int) bool { + fmt.Printf("%v's score[%v] rank is %v\n", i.Key(), i.Score, rank) + return true + }) + + // Remove + zs.Remove("Peek") + + // Rank + fmt.Println() + fmt.Println("After remove Peek:") + rank = zs.Rank("Hurst", true) + fmt.Printf("Hurst's rank is %v\n", rank) // expected 1 +} +``` \ No newline at end of file diff --git a/common/utils/zset/go.mod b/common/utils/zset/go.mod new file mode 100644 index 000000000..559cdcefc --- /dev/null +++ b/common/utils/zset/go.mod @@ -0,0 +1,3 @@ +module github.com/liwnn/zset + +go 1.18 diff --git a/common/utils/zset/zset.go b/common/utils/zset/zset.go new file mode 100644 index 000000000..01558f21e --- /dev/null +++ b/common/utils/zset/zset.go @@ -0,0 +1,540 @@ +//go:build !go1.18 +// +build !go1.18 + +// Package zset implements sorted set similar to redis zset. +package zset + +import ( + "math/rand" + "strconv" + "time" +) + +const ( + DefaultMaxLevel = 32 // (1/p)^MaxLevel >= maxNode + DefaultP = 0.25 // SkipList P = 1/4 + + DefaultFreeListSize = 32 +) + +// Item represents a single object in the set. +type Item interface { + // Less must provide a strict weak ordering + Less(Item) bool +} + +// ItemIterator allows callers of Range* to iterate of the zset. +// When this function returns false, iteration will stop. +type ItemIterator func(i Item, rank int) bool + +type skipListLevel struct { + forward *node + span int +} + +// node is an element of a skip list +type node struct { + item Item + backward *node + level []skipListLevel +} + +// FreeList represents a free list of set node. +type FreeList struct { + freelist []*node +} + +// NewFreeList creates a new free list. +func NewFreeList(size int) *FreeList { + return &FreeList{freelist: make([]*node, 0, size)} +} + +func (f *FreeList) newNode(lvl int) (n *node) { + if len(f.freelist) == 0 { + n = new(node) + n.level = make([]skipListLevel, lvl) + return + } + index := len(f.freelist) - 1 + n = f.freelist[index] + f.freelist[index] = nil + f.freelist = f.freelist[:index] + + if cap(n.level) < lvl { + n.level = make([]skipListLevel, lvl) + } else { + n.level = n.level[:lvl] + } + return +} + +func (f *FreeList) freeNode(n *node) (out bool) { + // for gc + n.item = nil + for j := 0; j < len(n.level); j++ { + n.level[j] = skipListLevel{} + } + + if len(f.freelist) < cap(f.freelist) { + f.freelist = append(f.freelist, n) + out = true + } + return +} + +// skipList represents a skip list +type skipList struct { + header, tail *node + length int + level int // current level count + maxLevel int + freelist *FreeList + random *rand.Rand +} + +// newSkipList creates a skip list +func newSkipList(maxLevel int) *skipList { + if maxLevel < DefaultMaxLevel { + panic("maxLevel must < 32") + } + return &skipList{ + level: 1, + header: &node{ + level: make([]skipListLevel, maxLevel), + }, + maxLevel: maxLevel, + freelist: NewFreeList(DefaultFreeListSize), + random: rand.New(rand.NewSource(time.Now().UnixNano())), + } +} + +// insert an item into the SkipList. +func (sl *skipList) insert(item Item) *node { + var update [DefaultMaxLevel]*node // [0...list.maxLevel) + var rank [DefaultMaxLevel]int + x := sl.header + for i := sl.level - 1; i >= 0; i-- { + if i == sl.level-1 { + rank[i] = 0 + } else { + rank[i] = rank[i+1] + } + for y := x.level[i].forward; y != nil && y.item.Less(item); y = x.level[i].forward { + rank[i] += x.level[i].span + x = y + } + update[i] = x + } + + lvl := sl.randomLevel() + if lvl > sl.level { + for i := sl.level; i < lvl; i++ { + rank[i] = 0 + update[i] = sl.header + update[i].level[i].span = sl.length + } + sl.level = lvl + } + + x = sl.freelist.newNode(lvl) + x.item = item + for i := 0; i < lvl; i++ { + x.level[i].forward = update[i].level[i].forward + update[i].level[i].forward = x + + x.level[i].span = update[i].level[i].span - (rank[0] - rank[i]) + update[i].level[i].span = (rank[0] - rank[i]) + 1 + } + + // increment span for untouched levels + for i := lvl; i < sl.level; i++ { + update[i].level[i].span++ + } + + if update[0] == sl.header { + x.backward = nil + } else { + x.backward = update[0] + } + if x.level[0].forward == nil { + sl.tail = x + } else { + x.level[0].forward.backward = x + } + sl.length++ + return x +} + +// delete element +func (sl *skipList) delete(n *node) Item { + var preAlloc [DefaultMaxLevel]*node // [0...list.maxLevel) + update := preAlloc[:sl.maxLevel] + x := sl.header + for i := sl.level - 1; i >= 0; i-- { + for y := x.level[i].forward; y != nil && y.item.Less(n.item); y = x.level[i].forward { + x = y + } + update[i] = x + } + x = x.level[0].forward + if x != nil && !n.item.Less(x.item) { + for i := 0; i < sl.level; i++ { + if update[i].level[i].forward == x { + update[i].level[i].span += x.level[i].span - 1 + update[i].level[i].forward = x.level[i].forward + } else { + update[i].level[i].span-- + } + } + for sl.level > 1 && sl.header.level[sl.level-1].forward == nil { + sl.level-- + } + if x.level[0].forward == nil { + sl.tail = x.backward + } else { + x.level[0].forward.backward = x.backward + } + removeItem := x.item + sl.freelist.freeNode(x) + sl.length-- + return removeItem + } + return nil +} + +func (sl *skipList) updateItem(node *node, item Item) bool { + if (node.level[0].forward == nil || !node.level[0].forward.item.Less(item)) && + (node.backward == nil || !item.Less(node.backward.item)) { + node.item = item + return true + } + return false +} + +// getRank find the rank for an element. +// Returns 0 when the element cannot be found, rank otherwise. +// Note that the rank is 1-based +func (sl *skipList) getRank(item Item) int { + var rank int + x := sl.header + for i := sl.level - 1; i >= 0; i-- { + for y := x.level[i].forward; y != nil && !item.Less(y.item); y = x.level[i].forward { + rank += x.level[i].span + x = y + } + if x.item != nil && !x.item.Less(item) { + return rank + } + } + return 0 +} + +func (sl *skipList) randomLevel() int { + lvl := 1 + for lvl < sl.maxLevel && float32(sl.random.Uint32()&0xFFFF) < DefaultP*0xFFFF { + lvl++ + } + return lvl +} + +// Finds an element by its rank. The rank argument needs to be 1-based. +func (sl *skipList) getNodeByRank(rank int) *node { + var traversed int + x := sl.header + for i := sl.level - 1; i >= 0; i-- { + for x.level[i].forward != nil && traversed+x.level[i].span <= rank { + traversed += x.level[i].span + x = x.level[i].forward + } + if traversed == rank { + return x + } + } + return nil +} + +func (sl *skipList) getMinNode() *node { + return sl.header.level[0].forward +} + +func (sl *skipList) getMaxNode() *node { + return sl.tail +} + +// return the first node greater and the node's 1-based rank. +func (sl *skipList) findNext(greater func(i Item) bool) (*node, int) { + x := sl.header + var rank int + for i := sl.level - 1; i >= 0; i-- { + for y := x.level[i].forward; y != nil && !greater(y.item); y = x.level[i].forward { + rank += x.level[i].span + x = y + } + } + return x.level[0].forward, rank + x.level[0].span +} + +// return the first node less and the node's 1-based rank. +func (sl *skipList) findPrev(less func(i Item) bool) (*node, int) { + var rank int + x := sl.header + for i := sl.level - 1; i >= 0; i-- { + for y := x.level[i].forward; y != nil && less(y.item); y = x.level[i].forward { + rank += x.level[i].span + x = y + } + } + return x, rank +} + +// ZSet set +type ZSet struct { + dict map[string]*node + sl *skipList +} + +// New creates a new ZSet. +func New() *ZSet { + return &ZSet{ + dict: make(map[string]*node), + sl: newSkipList(DefaultMaxLevel), + } +} + +// Add a new element or update the score of an existing element. If an item already +// exist, the removed item is returned. Otherwise, nil is returned. +func (zs *ZSet) Add(key string, item Item) (removeItem Item) { + if node := zs.dict[key]; node != nil { + // if the node after update, would be still exactly at the same position, + // we can just update item. + if zs.sl.updateItem(node, item) { + return + } + removeItem = zs.sl.delete(node) + } + zs.dict[key] = zs.sl.insert(item) + return +} + +// Remove the element 'ele' from the sorted set, +// return true if the element existed and was deleted, false otherwise +func (zs *ZSet) Remove(key string) (removeItem Item) { + node := zs.dict[key] + if node == nil { + return nil + } + removeItem = zs.sl.delete(node) + delete(zs.dict, key) + return +} + +// Rank return 1-based rank or 0 if not exist +func (zs *ZSet) Rank(key string, reverse bool) int { + node := zs.dict[key] + if node != nil { + rank := zs.sl.getRank(node.item) + if rank > 0 { + if reverse { + return zs.sl.length - rank + 1 + } + return rank + } + } + return 0 +} + +func (zs *ZSet) FindNext(iGreaterThan func(i Item) bool) (v Item, rank int) { + n, rank := zs.sl.findNext(iGreaterThan) + if n == nil { + return + } + return n.item, rank +} + +func (zs *ZSet) FindPrev(iLessThan func(i Item) bool) (v Item, rank int) { + n, rank := zs.sl.findPrev(iLessThan) + if n == nil { + return + } + return n.item, rank +} + +// RangeByScore calls the iterator for every value within the range [min, max], +// until iterator return false. If min is nil, it represents negative infinity. +// If max is nil, it represents positive infinity. +func (zs *ZSet) RangeByScore(min, max func(i Item) bool, reverse bool, iterator ItemIterator) { + llen := zs.sl.length + var minNode, maxNode *node + var minRank, maxRank int + if min == nil { + minNode = zs.sl.getMinNode() + minRank = 1 + } else { + minNode, minRank = zs.sl.findNext(min) + } + if minNode == nil { + return + } + if max == nil { + maxNode = zs.sl.getMaxNode() + maxRank = llen + } else { + maxNode, maxRank = zs.sl.findPrev(max) + } + if maxNode == nil { + return + } + if reverse { + n := maxNode + for i := maxRank; i >= minRank; i-- { + if iterator(n.item, llen-i+1) { + n = n.backward + } else { + break + } + } + } else { + n := minNode + for i := minRank; i <= maxRank; i++ { + if iterator(n.item, i) { + n = n.level[0].forward + } else { + break + } + } + } +} + +// Range calls the iterator for every value with in index range [start, end], +// until iterator return false. The and arguments represent +// zero-based indexes. +func (zs *ZSet) Range(start, end int, reverse bool, iterator ItemIterator) { + llen := zs.sl.length + if start < 0 { + start = llen + start + } + if end < 0 { + end = llen + end + } + if start < 0 { + start = 0 + } + if start > end || start >= llen { + return + } + if end >= llen { + end = llen - 1 + } + + rangeLen := end - start + 1 + if reverse { + ln := zs.sl.getNodeByRank(llen - start) + for i := 1; i <= rangeLen; i++ { + if iterator(ln.item, start+i) { + ln = ln.backward + } else { + break + } + } + } else { + ln := zs.sl.getNodeByRank(start + 1) + for i := 1; i <= rangeLen; i++ { + if iterator(ln.item, start+i) { + ln = ln.level[0].forward + } else { + break + } + } + } +} + +type RangeIterator struct { + node *node + start, end, cur int + reverse bool +} + +func (r *RangeIterator) Len() int { + return r.end - r.start + 1 +} + +func (r *RangeIterator) Valid() bool { + return r.cur <= r.end +} + +func (r *RangeIterator) Next() { + if r.reverse { + r.node = r.node.backward + } else { + r.node = r.node.level[0].forward + } + r.cur++ +} + +func (r *RangeIterator) Item() Item { + return r.node.item +} + +func (r *RangeIterator) Rank() int { + return r.cur + 1 +} + +// RangeIterator return iterator for visit elements in [start, end]. +// It is slower than Range. +func (zs *ZSet) RangeIterator(start, end int, reverse bool) RangeIterator { + llen := zs.sl.length + if start < 0 { + start = llen + start + } + if end < 0 { + end = llen + end + } + if start < 0 { + start = 0 + } + + if start > end || start >= llen { + return RangeIterator{end: -1} + } + + if end >= llen { + end = llen - 1 + } + + var n *node + if reverse { + n = zs.sl.getNodeByRank(llen - start) + } else { + n = zs.sl.getNodeByRank(start + 1) + } + return RangeIterator{ + start: start, + cur: start, + end: end, + node: n, + reverse: reverse, + } +} + +// Get return Item in dict. +func (zs *ZSet) Get(key string) Item { + if node, ok := zs.dict[key]; ok { + return node.item + } + return nil +} + +// Length return the element count +func (zs *ZSet) Length() int { + return zs.sl.length +} + +type Int int + +func (a Int) Key() string { + return strconv.Itoa(int(a)) +} + +func (a Int) Less(b Item) bool { + return a < b.(Int) +} diff --git a/common/utils/zset/zset_generic.go b/common/utils/zset/zset_generic.go new file mode 100644 index 000000000..e42de39ba --- /dev/null +++ b/common/utils/zset/zset_generic.go @@ -0,0 +1,529 @@ +//go:build go1.18 + +// Package zset implements sorted set similar to redis zset. +package zset + +import ( + "math/rand" + "time" +) + +const ( + DefaultMaxLevel = 32 // (1/p)^MaxLevel >= maxNode + DefaultP = 0.25 // SkipList P = 1/4 + + DefaultFreeListSize = 32 +) + +// ItemIterator allows callers of Range* to iterate of the zset. +// When this function returns false, iteration will stop. +type ItemIterator[T any] func(i T, rank int) bool + +type skipListLevel[T any] struct { + forward *node[T] + span int +} + +// node is an element of a skip list +type node[T any] struct { + item T + backward *node[T] + level []skipListLevel[T] +} + +// FreeList represents a free list of set node. +type FreeList[T any] struct { + freelist []*node[T] +} + +// NewFreeList creates a new free list. +func NewFreeList[T any](size int) *FreeList[T] { + return &FreeList[T]{freelist: make([]*node[T], 0, size)} +} + +func (f *FreeList[T]) newNode(lvl int) (n *node[T]) { + if len(f.freelist) == 0 { + n = new(node[T]) + n.level = make([]skipListLevel[T], lvl) + return + } + index := len(f.freelist) - 1 + n = f.freelist[index] + f.freelist[index] = nil + f.freelist = f.freelist[:index] + + if cap(n.level) < lvl { + n.level = make([]skipListLevel[T], lvl) + } else { + n.level = n.level[:lvl] + } + return +} + +func (f *FreeList[T]) freeNode(n *node[T]) (out bool) { + // for gc + var zero T + n.item = zero + for j := 0; j < len(n.level); j++ { + n.level[j] = skipListLevel[T]{} + } + + if len(f.freelist) < cap(f.freelist) { + f.freelist = append(f.freelist, n) + out = true + } + return +} + +// skipList represents a skip list +type skipList[T any] struct { + header, tail *node[T] + length int + level int // current level count + maxLevel int + freelist *FreeList[T] + random *rand.Rand + less LessFunc[T] +} + +// newSkipList creates a skip list +func newSkipList[T any](maxLevel int, less LessFunc[T]) *skipList[T] { + if maxLevel < DefaultMaxLevel { + panic("maxLevel must < 32") + } + return &skipList[T]{ + level: 1, + header: &node[T]{ + level: make([]skipListLevel[T], maxLevel), + }, + maxLevel: maxLevel, + freelist: NewFreeList[T](DefaultFreeListSize), + random: rand.New(rand.NewSource(time.Now().UnixNano())), + less: less, + } +} + +// insert an item into the SkipList. +func (sl *skipList[T]) insert(item T) *node[T] { + var update [DefaultMaxLevel]*node[T] // [0...list.maxLevel) + var rank [DefaultMaxLevel]int + x := sl.header + for i := sl.level - 1; i >= 0; i-- { + if i == sl.level-1 { + rank[i] = 0 + } else { + rank[i] = rank[i+1] + } + for y := x.level[i].forward; y != nil && sl.less(y.item, item); y = x.level[i].forward { + rank[i] += x.level[i].span + x = y + } + update[i] = x + } + + lvl := sl.randomLevel() + if lvl > sl.level { + for i := sl.level; i < lvl; i++ { + rank[i] = 0 + update[i] = sl.header + update[i].level[i].span = sl.length + } + sl.level = lvl + } + + x = sl.freelist.newNode(lvl) + x.item = item + for i := 0; i < lvl; i++ { + x.level[i].forward = update[i].level[i].forward + update[i].level[i].forward = x + + x.level[i].span = update[i].level[i].span - (rank[0] - rank[i]) + update[i].level[i].span = (rank[0] - rank[i]) + 1 + } + + // increment span for untouched levels + for i := lvl; i < sl.level; i++ { + update[i].level[i].span++ + } + + if update[0] == sl.header { + x.backward = nil + } else { + x.backward = update[0] + } + if x.level[0].forward == nil { + sl.tail = x + } else { + x.level[0].forward.backward = x + } + sl.length++ + return x +} + +// delete element +func (sl *skipList[T]) delete(n *node[T]) (_ T) { + var preAlloc [DefaultMaxLevel]*node[T] // [0...list.maxLevel) + update := preAlloc[:sl.maxLevel] + x := sl.header + for i := sl.level - 1; i >= 0; i-- { + for y := x.level[i].forward; y != nil && sl.less(y.item, n.item); y = x.level[i].forward { + x = y + } + update[i] = x + } + x = x.level[0].forward + if x != nil && !sl.less(n.item, x.item) { + for i := 0; i < sl.level; i++ { + if update[i].level[i].forward == x { + update[i].level[i].span += x.level[i].span - 1 + update[i].level[i].forward = x.level[i].forward + } else { + update[i].level[i].span-- + } + } + for sl.level > 1 && sl.header.level[sl.level-1].forward == nil { + sl.level-- + } + if x.level[0].forward == nil { + sl.tail = x.backward + } else { + x.level[0].forward.backward = x.backward + } + removeItem := x.item + sl.freelist.freeNode(x) + sl.length-- + return removeItem + } + return +} + +func (sl *skipList[T]) updateItem(node *node[T], item T) bool { + if (node.level[0].forward == nil || !sl.less(node.level[0].forward.item, item)) && + (node.backward == nil || !sl.less(item, node.backward.item)) { + node.item = item + return true + } + return false +} + +// getRank find the rank for an element. +// Returns 0 when the element cannot be found, rank otherwise. +// Note that the rank is 1-based +func (sl *skipList[T]) getRank(item T) int { + var rank int + x := sl.header + for i := sl.level - 1; i >= 0; i-- { + for y := x.level[i].forward; y != nil && !sl.less(item, y.item); y = x.level[i].forward { + rank += x.level[i].span + x = y + } + if x != sl.header && !sl.less(x.item, item) { + return rank + } + } + return 0 +} + +func (sl *skipList[T]) randomLevel() int { + lvl := 1 + for lvl < sl.maxLevel && float32(sl.random.Uint32()&0xFFFF) < DefaultP*0xFFFF { + lvl++ + } + return lvl +} + +// Finds an element by its rank. The rank argument needs to be 1-based. +func (sl *skipList[T]) getNodeByRank(rank int) *node[T] { + var traversed int + x := sl.header + for i := sl.level - 1; i >= 0; i-- { + for x.level[i].forward != nil && traversed+x.level[i].span <= rank { + traversed += x.level[i].span + x = x.level[i].forward + } + if traversed == rank { + return x + } + } + return nil +} + +func (sl *skipList[T]) getMinNode() *node[T] { + return sl.header.level[0].forward +} + +func (sl *skipList[T]) getMaxNode() *node[T] { + return sl.tail +} + +// return the first node greater and the node's 1-based rank. +func (sl *skipList[T]) findNext(greater func(i T) bool) (*node[T], int) { + x := sl.header + var rank int + for i := sl.level - 1; i >= 0; i-- { + for y := x.level[i].forward; y != nil && !greater(y.item); y = x.level[i].forward { + rank += x.level[i].span + x = y + } + } + return x.level[0].forward, rank + x.level[0].span +} + +// return the first node less and the node's 1-based rank. +func (sl *skipList[T]) findPrev(less func(i T) bool) (*node[T], int) { + var rank int + x := sl.header + for i := sl.level - 1; i >= 0; i-- { + for y := x.level[i].forward; y != nil && less(y.item); y = x.level[i].forward { + rank += x.level[i].span + x = y + } + } + return x, rank +} + +// ZSet set +type ZSet[K comparable, T any] struct { + dict map[K]*node[T] + sl *skipList[T] +} + +// LessFunc determines how to order a type 'T'. It should implement a strict +// ordering, and should return true if within that ordering, 'a' < 'b'. +type LessFunc[T any] func(a, b T) bool + +// New creates a new ZSet. +func New[K comparable, T any](less LessFunc[T]) *ZSet[K, T] { + return &ZSet[K, T]{ + dict: make(map[K]*node[T]), + sl: newSkipList[T](DefaultMaxLevel, less), + } +} + +// Add a new element or update the score of an existing element. If an item already +// exist, the removed item is returned. Otherwise, nil is returned. +func (zs *ZSet[K, T]) Add(key K, item T) (removeItem T) { + if node := zs.dict[key]; node != nil { + // if the node after update, would be still exactly at the same position, + // we can just update item. + if zs.sl.updateItem(node, item) { + return + } + removeItem = zs.sl.delete(node) + } + zs.dict[key] = zs.sl.insert(item) + return +} + +// Remove the element 'ele' from the sorted set, +// return true if the element existed and was deleted, false otherwise +func (zs *ZSet[K, T]) Remove(key K) (removeItem T) { + node := zs.dict[key] + if node == nil { + return + } + removeItem = zs.sl.delete(node) + delete(zs.dict, key) + return +} + +// Rank return 1-based rank or 0 if not exist +func (zs *ZSet[K, T]) Rank(key K, reverse bool) int { + node := zs.dict[key] + if node != nil { + rank := zs.sl.getRank(node.item) + if rank > 0 { + if reverse { + return zs.sl.length - rank + 1 + } + return rank + } + } + return 0 +} + +func (zs *ZSet[K, T]) FindNext(iGreaterThan func(i T) bool) (v T, rank int) { + n, rank := zs.sl.findNext(iGreaterThan) + if n == nil { + return + } + return n.item, rank +} + +func (zs *ZSet[K, T]) FindPrev(iLessThan func(i T) bool) (v T, rank int) { + n, rank := zs.sl.findPrev(iLessThan) + if n == nil { + return + } + return n.item, rank +} + +// RangeByScore calls the iterator for every value within the range [min, max], +// until iterator return false. If min is nil, it represents negative infinity. +// If max is nil, it represents positive infinity. +func (zs *ZSet[K, T]) RangeByScore(min, max func(i T) bool, reverse bool, iterator ItemIterator[T]) { + llen := zs.sl.length + var minNode, maxNode *node[T] + var minRank, maxRank int + if min == nil { + minNode = zs.sl.getMinNode() + minRank = 1 + } else { + minNode, minRank = zs.sl.findNext(min) + } + if minNode == nil { + return + } + if max == nil { + maxNode = zs.sl.getMaxNode() + maxRank = llen + } else { + maxNode, maxRank = zs.sl.findPrev(max) + } + if maxNode == nil { + return + } + if reverse { + n := maxNode + for i := maxRank; i >= minRank; i-- { + if iterator(n.item, llen-i+1) { + n = n.backward + } else { + break + } + } + } else { + n := minNode + for i := minRank; i <= maxRank; i++ { + if iterator(n.item, i) { + n = n.level[0].forward + } else { + break + } + } + } +} + +// Range calls the iterator for every value with in index range [start, end], +// until iterator return false. The and arguments represent +// zero-based indexes. +func (zs *ZSet[K, T]) Range(start, end int, reverse bool, iterator ItemIterator[T]) { + llen := zs.sl.length + if start < 0 { + start = llen + start + } + if end < 0 { + end = llen + end + } + if start < 0 { + start = 0 + } + if start > end || start >= llen { + return + } + if end >= llen { + end = llen - 1 + } + + rangeLen := end - start + 1 + if reverse { + ln := zs.sl.getNodeByRank(llen - start) + for i := 1; i <= rangeLen; i++ { + if iterator(ln.item, start+i) { + ln = ln.backward + } else { + break + } + } + } else { + ln := zs.sl.getNodeByRank(start + 1) + for i := 1; i <= rangeLen; i++ { + if iterator(ln.item, start+i) { + ln = ln.level[0].forward + } else { + break + } + } + } +} + +type RangeIterator[T any] struct { + node *node[T] + start, end, cur int + reverse bool +} + +func (r *RangeIterator[T]) Len() int { + return r.end - r.start + 1 +} + +func (r *RangeIterator[T]) Valid() bool { + return r.cur <= r.end +} + +func (r *RangeIterator[T]) Next() { + if r.reverse { + r.node = r.node.backward + } else { + r.node = r.node.level[0].forward + } + r.cur++ +} + +func (r *RangeIterator[T]) Item() T { + return r.node.item +} + +func (r *RangeIterator[T]) Rank() int { + return r.cur + 1 +} + +// RangeIterator return iterator for visit elements in [start, end]. +// It is slower than Range. +func (zs *ZSet[K, T]) RangeIterator(start, end int, reverse bool) RangeIterator[T] { + llen := zs.sl.length + if start < 0 { + start = llen + start + } + if end < 0 { + end = llen + end + } + if start < 0 { + start = 0 + } + + if start > end || start >= llen { + return RangeIterator[T]{end: -1} + } + + if end >= llen { + end = llen - 1 + } + + var n *node[T] + if reverse { + n = zs.sl.getNodeByRank(llen - start) + } else { + n = zs.sl.getNodeByRank(start + 1) + } + return RangeIterator[T]{ + start: start, + cur: start, + end: end, + node: n, + reverse: reverse, + } +} + +// Get return Item in dict. +func (zs *ZSet[K, T]) Get(key K) (item T, found bool) { + if n, ok := zs.dict[key]; ok { + return n.item, ok + } + return +} + +// Length return the element count +func (zs *ZSet[K, T]) Length() int { + return zs.sl.length +} diff --git a/common/utils/zset/zset_generic_test.go b/common/utils/zset/zset_generic_test.go new file mode 100644 index 000000000..bc6086fbc --- /dev/null +++ b/common/utils/zset/zset_generic_test.go @@ -0,0 +1,342 @@ +//go:build go1.18 + +package zset + +import ( + "math/rand" + "reflect" + "strconv" + "testing" +) + +type TestRank struct { + member string + score int +} + +// perm returns a random permutation of n Int items in the range [0, n). +func perm(n int) (out []TestRank) { + out = make([]TestRank, 0, n) + for _, v := range rand.Perm(n) { + out = append(out, TestRank{ + member: strconv.Itoa(v), + score: v, + }) + } + return +} + +// rang returns an ordered list of Int items in the range [0, n). +func rang(n int) (out []TestRank) { + for i := 0; i < n; i++ { + out = append(out, TestRank{ + member: strconv.Itoa(i), + score: i, + }) + } + return +} + +func revrang(n int, count int) (out []TestRank) { + for i := n - 1; i >= n-count; i-- { + out = append(out, TestRank{ + member: strconv.Itoa(i), + score: i, + }) + } + return +} + +func TestZSetRank(t *testing.T) { + const listSize = 10000 + zs := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + for i := 0; i < 10; i++ { + for _, v := range perm(listSize) { + zs.Add(v.member, v) + } + for _, v := range perm(listSize) { + if zs.Rank(v.member, false) != v.score+1 { + t.Error("rank error") + } + if zs.Rank(v.member, true) != listSize-v.score { + t.Error("rank error") + } + } + + var r []TestRank + zs.Range(0, 1, false, func(item TestRank, _ int) bool { + r = append(r, item) + return true + }) + if !reflect.DeepEqual(r, rang(2)) { + t.Error("range error") + } + + r = r[:0] + zs.RangeByScore(func(i TestRank) bool { + return i.score >= 0 + }, func(i TestRank) bool { + return i.score <= 1 + }, false, func(item TestRank, rank int) bool { + r = append(r, item) + return true + }) + if !reflect.DeepEqual(r, rang(2)) { + t.Error("RangeItem error", r, rang(2)) + } + + r = r[:0] + zs.Range(0, 1, true, func(item TestRank, _ int) bool { + r = append(r, item) + return true + }) + if !reflect.DeepEqual(r, revrang(listSize, 2)) { + t.Error("range error") + } + + for i := 0; i < listSize/2; i++ { + zs.Remove(strconv.Itoa(i)) + } + for i := listSize + 1; i < listSize; i++ { + if r := zs.Rank(strconv.Itoa(i), false); r != i-listSize/2 { + t.Error("rank failed") + } + } + } +} + +func TestRangeItem(t *testing.T) { + zs := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + zs.RangeByScore(nil, nil, false, func(i TestRank, rank int) bool { + return true + }) + + for _, i := range perm(10) { + zs.Add(i.member, i) + } + + var r []TestRank + zs.RangeByScore(nil, nil, false, func(i TestRank, rank int) bool { + r = append(r, i) + return true + }) + if !reflect.DeepEqual(r, rang(10)) { + t.Error("RangeItem error", r, rang(10)) + } + + r = r[:0] + zs.RangeByScore(func(i TestRank) bool { + return i.score >= 3 + }, func(i TestRank) bool { + return i.score <= 5 + }, false, func(i TestRank, rank int) bool { + r = append(r, i) + return true + }) + var expect []TestRank + for i := 3; i <= 5; i++ { + expect = append(expect, TestRank{ + member: strconv.Itoa(i), + score: i, + }) + } + if !reflect.DeepEqual(r, expect) { + t.Error("RangeItem error", r, expect) + } + + r = r[:0] + zs.RangeByScore(func(i TestRank) bool { + return i.score >= 3 + }, func(i TestRank) bool { + return i.score <= 5 + }, true, func(i TestRank, rank int) bool { + r = append(r, i) + return true + }) + expect = expect[:0] + for i := 5; i >= 3; i-- { + expect = append(expect, TestRank{ + member: strconv.Itoa(i), + score: i, + }) + } + if !reflect.DeepEqual(r, expect) { + t.Error("RangeItem error", r, expect) + } +} + +const benchmarkListSize = 10000 + +func BenchmarkAdd(b *testing.B) { + b.StopTimer() + insertP := perm(benchmarkListSize) + b.StartTimer() + i := 0 + for i < b.N { + tr := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + for _, item := range insertP { + tr.Add(item.member, item) + i++ + if i >= b.N { + return + } + } + } +} + +func BenchmarkAddIncrease(b *testing.B) { + b.StopTimer() + insertP := rang(benchmarkListSize) + b.StartTimer() + i := 0 + for i < b.N { + tr := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + for _, item := range insertP { + tr.Add(item.member, item) + i++ + if i >= b.N { + return + } + } + } +} + +func BenchmarkAddDecrease(b *testing.B) { + b.StopTimer() + insertP := revrang(benchmarkListSize, benchmarkListSize) + b.StartTimer() + i := 0 + for i < b.N { + tr := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + for _, item := range insertP { + tr.Add(item.member, item) + i++ + if i >= b.N { + return + } + } + } +} + +func BenchmarkRemoveAdd(b *testing.B) { + b.StopTimer() + insertP := perm(benchmarkListSize) + tr := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + for _, item := range insertP { + tr.Add(item.member, item) + } + b.StartTimer() + for i := 0; i < b.N; i++ { + tr.Remove(insertP[i%benchmarkListSize].member) + item := insertP[i%benchmarkListSize] + tr.Add(item.member, item) + } +} + +func BenchmarkRemove(b *testing.B) { + b.StopTimer() + insertP := perm(benchmarkListSize) + removeP := perm(benchmarkListSize) + b.StartTimer() + i := 0 + for i < b.N { + b.StopTimer() + tr := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + for _, item := range insertP { + tr.Add(item.member, item) + } + b.StartTimer() + for _, item := range removeP { + tr.Remove(item.member) + i++ + if i >= b.N { + return + } + } + if tr.Length() > 0 { + b.Error(tr.Length()) + } + } +} + +func BenchmarkRank(b *testing.B) { + b.StopTimer() + insertP := perm(benchmarkListSize) + tr := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + for _, item := range insertP { + tr.Add(item.member, item) + } + b.StartTimer() + for i := 0; i < b.N; i++ { + tr.Rank(insertP[i%benchmarkListSize].member, true) + } +} + +func BenchmarkRange(b *testing.B) { + insertP := perm(benchmarkListSize) + tr := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + for _, item := range insertP { + tr.Add(item.member, item) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + tr.Range(0, 100, true, func(i TestRank, rank int) bool { + return true + }) + } +} + +func BenchmarkRangeIterator(b *testing.B) { + insertP := perm(benchmarkListSize) + tr := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + for _, item := range insertP { + tr.Add(item.member, item) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + it := tr.RangeIterator(0, 100, true) + for ; it.Valid(); it.Next() { + } + } +} + +func BenchmarkRangeItem(b *testing.B) { + insertP := perm(benchmarkListSize) + tr := New[string, TestRank](func(a, b TestRank) bool { + return a.score < b.score + }) + for _, item := range insertP { + tr.Add(item.member, item) + } + minScore, maxScore := 0, 100 + b.ResetTimer() + for i := 0; i < b.N; i++ { + tr.RangeByScore(func(i TestRank) bool { + return i.score >= minScore + }, func(i TestRank) bool { + return i.score <= maxScore + }, true, func(i TestRank, rank int) bool { + return true + }) + } +} diff --git a/common/utils/zset/zset_test.go b/common/utils/zset/zset_test.go new file mode 100644 index 000000000..583dc9328 --- /dev/null +++ b/common/utils/zset/zset_test.go @@ -0,0 +1,329 @@ +//go:build !go1.18 +// +build !go1.18 + +package zset + +import ( + "math/rand" + "reflect" + "strconv" + "testing" +) + +type TestRank struct { + member string + score int +} + +func (a TestRank) Key() string { + return a.member +} + +func (a TestRank) Less(than Item) bool { + return a.score < than.(TestRank).score +} + +// perm returns a random permutation of n Int items in the range [0, n). +func perm(n int) (out []TestRank) { + out = make([]TestRank, 0, n) + for _, v := range rand.Perm(n) { + out = append(out, TestRank{ + member: strconv.Itoa(v), + score: v, + }) + } + return +} + +// rang returns an ordered list of Int items in the range [0, n). +func rang(n int) (out []TestRank) { + for i := 0; i < n; i++ { + out = append(out, TestRank{ + member: strconv.Itoa(i), + score: i, + }) + } + return +} + +func revrang(n int, count int) (out []TestRank) { + for i := n - 1; i >= n-count; i-- { + out = append(out, TestRank{ + member: strconv.Itoa(i), + score: i, + }) + } + return +} + +func TestZSetRank(t *testing.T) { + const listSize = 10000 + zs := New() + for i := 0; i < 10; i++ { + for _, v := range perm(listSize) { + zs.Add(v.member, v) + } + for _, v := range perm(listSize) { + if zs.Rank(v.Key(), false) != v.score+1 { + t.Error("rank error") + } + if zs.Rank(v.Key(), true) != listSize-v.score { + t.Error("rank error") + } + } + + var r []Item + zs.Range(0, 1, false, func(item Item, _ int) bool { + r = append(r, item) + return true + }) + if !reflect.DeepEqual(r, rang(2)) { + t.Error("range error") + } + + r = r[:0] + zs.RangeByScore(func(i Item) bool { + return i.(TestRank).score >= 0 + }, func(i Item) bool { + return i.(TestRank).score <= 1 + }, false, func(item Item, rank int) bool { + r = append(r, item) + return true + }) + if !reflect.DeepEqual(r, rang(2)) { + t.Error("RangeItem error", r, rang(2)) + } + + r = r[:0] + zs.Range(0, 1, true, func(item Item, _ int) bool { + r = append(r, item) + return true + }) + if !reflect.DeepEqual(r, revrang(listSize, 2)) { + t.Error("range error") + } + + for i := 0; i < listSize/2; i++ { + zs.Remove(strconv.Itoa(i)) + } + for i := listSize + 1; i < listSize; i++ { + if r := zs.Rank(strconv.Itoa(i), false); r != i-listSize/2 { + t.Error("rank failed") + } + } + } +} + +func TestRangeItem(t *testing.T) { + zs := New() + zs.RangeByScore(nil, nil, false, func(i Item, rank int) bool { + return true + }) + + for _, i := range perm(10) { + zs.Add(i.member, i) + } + + var r []Item + zs.RangeByScore(nil, nil, false, func(i Item, rank int) bool { + r = append(r, i) + return true + }) + if !reflect.DeepEqual(r, rang(10)) { + t.Error("RangeItem error", r, rang(10)) + } + + r = r[:0] + zs.RangeByScore(func(i Item) bool { + return i.(TestRank).score >= 3 + }, func(i Item) bool { + return i.(TestRank).score <= 5 + }, false, func(i Item, rank int) bool { + r = append(r, i) + return true + }) + var expect []Item + for i := 3; i <= 5; i++ { + expect = append(expect, TestRank{ + member: strconv.Itoa(i), + score: i, + }) + } + if !reflect.DeepEqual(r, expect) { + t.Error("RangeItem error", r, expect) + } + + r = r[:0] + zs.RangeByScore(func(i Item) bool { + return i.(TestRank).score >= 3 + }, func(i Item) bool { + return i.(TestRank).score <= 5 + }, true, func(i Item, rank int) bool { + r = append(r, i) + return true + }) + expect = expect[:0] + for i := 5; i >= 3; i-- { + expect = append(expect, TestRank{ + member: strconv.Itoa(i), + score: i, + }) + } + if !reflect.DeepEqual(r, expect) { + t.Error("RangeItem error", r, expect) + } +} + +const benchmarkListSize = 10000 + +func BenchmarkAdd(b *testing.B) { + b.StopTimer() + insertP := perm(benchmarkListSize) + b.StartTimer() + i := 0 + for i < b.N { + tr := New() + for _, item := range insertP { + tr.Add(item.member, item) + i++ + if i >= b.N { + return + } + } + } +} + +func BenchmarkAddIncrease(b *testing.B) { + b.StopTimer() + insertP := rang(benchmarkListSize) + b.StartTimer() + i := 0 + for i < b.N { + tr := New() + for _, item := range insertP { + tr.Add(item.Key(), item) + i++ + if i >= b.N { + return + } + } + } +} + +func BenchmarkAddDecrease(b *testing.B) { + b.StopTimer() + insertP := revrang(benchmarkListSize, benchmarkListSize) + b.StartTimer() + i := 0 + for i < b.N { + tr := New() + for _, item := range insertP { + tr.Add(item.member, item) + i++ + if i >= b.N { + return + } + } + } +} + +func BenchmarkRemoveAdd(b *testing.B) { + b.StopTimer() + insertP := perm(benchmarkListSize) + tr := New() + for _, item := range insertP { + tr.Add(item.member, item) + } + b.StartTimer() + for i := 0; i < b.N; i++ { + tr.Remove(insertP[i%benchmarkListSize].Key()) + item := insertP[i%benchmarkListSize] + tr.Add(item.member, item) + } +} + +func BenchmarkRemove(b *testing.B) { + b.StopTimer() + insertP := perm(benchmarkListSize) + removeP := perm(benchmarkListSize) + b.StartTimer() + i := 0 + for i < b.N { + b.StopTimer() + tr := New() + for _, v := range insertP { + tr.Add(v.member, v) + } + b.StartTimer() + for _, item := range removeP { + tr.Remove(item.Key()) + i++ + if i >= b.N { + return + } + } + if tr.Length() > 0 { + b.Error(tr.Length()) + } + } +} + +func BenchmarkRank(b *testing.B) { + b.StopTimer() + insertP := perm(benchmarkListSize) + tr := New() + for _, v := range insertP { + tr.Add(v.member, v) + } + b.StartTimer() + for i := 0; i < b.N; i++ { + tr.Rank(insertP[i%benchmarkListSize].Key(), true) + } +} + +func BenchmarkRange(b *testing.B) { + insertP := perm(benchmarkListSize) + tr := New() + for _, item := range insertP { + tr.Add(item.member, item) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + tr.Range(0, 100, true, func(i Item, rank int) bool { + return true + }) + } +} + +func BenchmarkRangeIterator(b *testing.B) { + insertP := perm(benchmarkListSize) + tr := New() + for _, item := range insertP { + tr.Add(item.member, item) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + it := tr.RangeIterator(0, 100, true) + for ; it.Valid(); it.Next() { + } + } +} + +func BenchmarkRangeItem(b *testing.B) { + insertP := perm(benchmarkListSize) + tr := New() + for _, item := range insertP { + tr.Add(item.member, item) + } + minScore, maxScore := 0, 100 + b.ResetTimer() + for i := 0; i < b.N; i++ { + tr.RangeByScore(func(i Item) bool { + return i.(TestRank).score >= minScore + }, func(i Item) bool { + return i.(TestRank).score <= maxScore + }, true, func(i Item, rank int) bool { + return true + }) + } +} diff --git a/go.work b/go.work index 682455af7..3dbcfd1dc 100644 --- a/go.work +++ b/go.work @@ -20,6 +20,7 @@ use ( ./common/utils/sturc ./common/utils/timer ./common/utils/xml + ./common/utils/zset ./logic ./login ./modules diff --git a/logic/controller/fight_base.go b/logic/controller/fight_base.go index 7ece8026c..9846e1fb2 100644 --- a/logic/controller/fight_base.go +++ b/logic/controller/fight_base.go @@ -2,6 +2,7 @@ package controller import ( "blazing/common/socket/errorcode" + "blazing/modules/player/model" "blazing/logic/service/fight" "blazing/logic/service/fight/info" @@ -40,7 +41,7 @@ func (h Controller) Escape(data *fight.EscapeFightInboundInfo, c *player.Player) return nil, err } - go c.FightC.Over(c, info.BattleOverReason.PlayerEscape) + go c.FightC.Over(c, model.BattleOverReason.PlayerEscape) return nil, 0 } diff --git a/logic/controller/fight_boss野怪和地图怪.go b/logic/controller/fight_boss野怪和地图怪.go index 86c0bdd69..99c79e206 100644 --- a/logic/controller/fight_boss野怪和地图怪.go +++ b/logic/controller/fight_boss野怪和地图怪.go @@ -102,7 +102,7 @@ func (Controller) PlayerFightBoss(data1 *fight.ChallengeBossInboundInfo, p *play } ai.Prop[0] = 2 - fight.NewFight(p, ai, func(foi info.FightOverInfo) { + fight.NewFight(p, ai, func(foi model.FightOverInfo) { if mdata.WinBonusID != 0 { if foi.Reason == 0 && foi.WinnerId == p.Info.UserID { p.SptCompletedTask(mdata.WinBonusID, 1) @@ -159,7 +159,7 @@ func (Controller) OnPlayerFightNpcMonster(data1 *fight.FightNpcMonsterInboundInf p.Fightinfo.Status = info.BattleMode.FIGHT_WITH_NPC //打野怪 p.Fightinfo.Mode = info.BattleMode.MULTI_MODE //多人模式 - fight.NewFight(p, ai, func(foi info.FightOverInfo) { + fight.NewFight(p, ai, func(foi model.FightOverInfo) { //p.Done.Exec(model.MilestoneMode.Moster, []uint32{p.Info.MapID, monsterInfo.PetList[0].ID, uint32(foi.Reason)}, nil) if foi.Reason == 0 && foi.WinnerId == p.Info.UserID && p.CanGet() { diff --git a/logic/controller/fight_pvp_king.go b/logic/controller/fight_pvp_king.go index 76350ef69..2cb5c6330 100644 --- a/logic/controller/fight_pvp_king.go +++ b/logic/controller/fight_pvp_king.go @@ -2,6 +2,7 @@ package controller import ( "blazing/common/socket/errorcode" + "blazing/modules/player/model" "blazing/logic/service/common" "blazing/logic/service/fight" @@ -17,7 +18,7 @@ func (h Controller) PetMelee(data *fight.StartPetWarInboundInfo, c *player.Playe c.Fightinfo.Status = info.BattleMode.PET_MELEE err = c.JoinFight(func(p common.PlayerI) bool { - _, err = fight.NewFight(p, c, func(foi info.FightOverInfo) { + _, err = fight.NewFight(p, c, func(foi model.FightOverInfo) { if foi.Reason == 0 { //我放获胜 if foi.WinnerId == c.GetInfo().UserID { @@ -32,7 +33,7 @@ func (h Controller) PetMelee(data *fight.StartPetWarInboundInfo, c *player.Playe } } - if foi.Reason == info.BattleOverReason.PlayerOffline { + if foi.Reason == model.BattleOverReason.PlayerOffline { if foi.WinnerId == c.GetInfo().UserID { p.MessWin(false) @@ -71,7 +72,7 @@ func (h Controller) PetKing(data *fight.PetKingJoinInboundInfo, c *player.Player c.Fightinfo.FightType = data.FightType } err = c.JoinFight(func(p common.PlayerI) bool { - _, err = fight.NewFight(p, c, func(foi info.FightOverInfo) { + _, err = fight.NewFight(p, c, func(foi model.FightOverInfo) { if foi.Reason == 0 { //我放获胜 switch data.Type { case 11: diff --git a/logic/controller/fight_pvp_withplayer.go b/logic/controller/fight_pvp_withplayer.go index 06d52581a..5898d383f 100644 --- a/logic/controller/fight_pvp_withplayer.go +++ b/logic/controller/fight_pvp_withplayer.go @@ -2,6 +2,7 @@ package controller import ( "blazing/common/socket/errorcode" + "blazing/modules/player/model" "sync/atomic" "blazing/logic/service/common" @@ -54,7 +55,7 @@ func (h Controller) OnPlayerHandleFightInvite(data *fight.HandleFightInviteInbou return } - _, err = fight.NewFight(v, c, func(foi info.FightOverInfo) { + _, err = fight.NewFight(v, c, func(foi model.FightOverInfo) { //println("好友对战测试", foi.Reason) diff --git a/logic/controller/fight_塔.go b/logic/controller/fight_塔.go index 27e9e62b6..09d0682ee 100644 --- a/logic/controller/fight_塔.go +++ b/logic/controller/fight_塔.go @@ -187,7 +187,7 @@ func (h Controller) PetTawor(data *fight.StartTwarInboundInfo, c *player.Player) } ai := player.NewAI_player(monsterInfo) - _, err = fight.NewFight(c, ai, func(foi fightinfo.FightOverInfo) { + _, err = fight.NewFight(c, ai, func(foi model.FightOverInfo) { if foi.Reason == 0 && foi.WinnerId == c.Info.UserID { //我放获胜 switch data.Head.CMD { case 2429: //试炼之塔 diff --git a/logic/controller/fight_巅峰.go b/logic/controller/fight_巅峰.go index 9edee7639..eca35d59c 100644 --- a/logic/controller/fight_巅峰.go +++ b/logic/controller/fight_巅峰.go @@ -5,11 +5,8 @@ import ( "blazing/cool" "blazing/logic/service/common" "blazing/logic/service/fight" - "blazing/logic/service/fight/top/repo" "blazing/logic/service/player" "context" - - goredis "github.com/redis/go-redis/v9" ) // 表示"宠物王加入"的入站消息数据 @@ -20,11 +17,10 @@ type PetTOPLEVELnboundInfo struct { } func (h Controller) JoINtop(data *PetTOPLEVELnboundInfo, c *player.Player) (result *fight.NullOutboundInfo, err errorcode.ErrorCode) { - cool.RedisDo(context.TODO(), "sun:join:2458", data.Head.UserID, data.Mode) - client := cool.Redis.Client() + cool.RedisDo(context.TODO(), "sun:join:2458", data.Head.UserID) - // 类型断言为 UniversalClient - universalClient, _ := client.(goredis.UniversalClient) - repo.NewPlayerRepository(universalClient).AddPlayerToPool(context.TODO(), data.Head.UserID, 1) + // // 类型断言为 UniversalClient + // universalClient, _ := client.(goredis.UniversalClient) + // repo.NewPlayerRepository(universalClient).AddPlayerToPool(context.TODO(), data.Head.UserID, 1) return nil, -1 } diff --git a/logic/controller/fight_擂台.go b/logic/controller/fight_擂台.go index f40a1c941..bd1926f4e 100644 --- a/logic/controller/fight_擂台.go +++ b/logic/controller/fight_擂台.go @@ -3,6 +3,7 @@ package controller import ( "blazing/common/data" "blazing/common/socket/errorcode" + "blazing/modules/player/model" "sync/atomic" "blazing/logic/service/fight" @@ -66,7 +67,7 @@ func (h Controller) ArenaFightOwner(data1 *fight.ARENA_FIGHT_OWENR, c *player.Pl c.Fightinfo.Mode = info.BattleMode.SINGLE_MODE c.Fightinfo.Status = info.BattleMode.FIGHT_ARENA - _, err = fight.NewFight(c, c.GetSpace().Owner.ARENA_Player, func(foi info.FightOverInfo) { //我方邀请擂主挑战,我方先手 + _, err = fight.NewFight(c, c.GetSpace().Owner.ARENA_Player, func(foi model.FightOverInfo) { //我方邀请擂主挑战,我方先手 if foi.Reason != 0 && foi.WinnerId == c.GetInfo().UserID { //异常退出 diff --git a/logic/service/common/fight.go b/logic/service/common/fight.go index 13675da7e..8a4ed09a8 100644 --- a/logic/service/common/fight.go +++ b/logic/service/common/fight.go @@ -2,14 +2,15 @@ package common import ( "blazing/logic/service/fight/info" + "blazing/modules/player/model" "math/rand" ) type FightI interface { - Over(c PlayerI, id info.EnumBattleOverReason) //逃跑 - UseSkill(c PlayerI, id uint32) //使用技能 - GetCurrPET(c PlayerI) *info.BattlePetEntity //当前精灵 - GetOverInfo() info.FightOverInfo + Over(c PlayerI, id model.EnumBattleOverReason) //逃跑 + UseSkill(c PlayerI, id uint32) //使用技能 + GetCurrPET(c PlayerI) *info.BattlePetEntity //当前精灵 + GetOverInfo() model.FightOverInfo Ownerid() uint32 ReadyFight(c PlayerI) //是否准备战斗 ChangePet(c PlayerI, id uint32) diff --git a/logic/service/fight/action.go b/logic/service/fight/action.go index a20e67826..76a2aedba 100644 --- a/logic/service/fight/action.go +++ b/logic/service/fight/action.go @@ -6,6 +6,7 @@ import ( "blazing/logic/service/fight/action" "blazing/logic/service/fight/info" "blazing/logic/service/fight/input" + "blazing/modules/player/model" "context" "time" @@ -26,12 +27,12 @@ func (*FightC) Compare(a, b action.BattleActionI) (action.BattleActionI, action. } // 玩家逃跑/无响应/掉线 -func (f *FightC) Over(c common.PlayerI, res info.EnumBattleOverReason) { +func (f *FightC) Over(c common.PlayerI, res model.EnumBattleOverReason) { if f.closefight { return } - if f.Info.Status != info.BattleMode.FIGHT_WITH_NPC && res == info.BattleOverReason.PlayerEscape { + if f.Info.Status != info.BattleMode.FIGHT_WITH_NPC && res == model.BattleOverReason.PlayerEscape { return } // case *action.EscapeAction: diff --git a/logic/service/fight/boss/NewSeIdx_13.go b/logic/service/fight/boss/NewSeIdx_13.go index fd5d4ec77..2a1daa679 100644 --- a/logic/service/fight/boss/NewSeIdx_13.go +++ b/logic/service/fight/boss/NewSeIdx_13.go @@ -1,8 +1,8 @@ package effect import ( - "blazing/logic/service/fight/info" "blazing/logic/service/fight/input" + "blazing/modules/player/model" ) // 13. n 回合逃跑;(a1: n, a2: 不逃跑精灵ID, a3/a4: 不逃跑系精灵) @@ -17,7 +17,7 @@ func (e *NewSel13) HookAction() bool { } r := e.Ctx().Our.FightC.GetOverInfo() if r.Round == uint32(e.Args()[0].IntPart()) { - e.Ctx().Our.FightC.Over(e.Ctx().Our.Player, info.BattleOverReason.PlayerEscape) + e.Ctx().Our.FightC.Over(e.Ctx().Our.Player, model.BattleOverReason.PlayerEscape) return false //阻止技能释放 } diff --git a/logic/service/fight/effect/EffectDefeatTrigger.go b/logic/service/fight/effect/EffectDefeatTrigger.go index 8b82167ee..918abe4a1 100644 --- a/logic/service/fight/effect/EffectDefeatTrigger.go +++ b/logic/service/fight/effect/EffectDefeatTrigger.go @@ -5,6 +5,7 @@ import ( "blazing/logic/service/fight/info" "blazing/logic/service/fight/input" "blazing/logic/service/fight/node" + "blazing/modules/player/model" "github.com/alpacahq/alpacadecimal" ) @@ -17,7 +18,7 @@ type EffectDefeatTrigger struct { can bool // 标记技能是否生效(当次攻击有效) effectID int // 效果ID,用于区分不同触发行为 isd bool - info info.AttackValue + info model.AttackValue } // 工厂函数:创建"击败触发"效果实例,传入效果ID @@ -95,7 +96,7 @@ func (e *EffectDefeatTrigger) SetArgs(t *input.Input, a ...int) { // ----------------------------------------------------------- // 根据effectID触发对应行为 // ----------------------------------------------------------- -func (e *EffectDefeatTrigger) triggerByID(at info.AttackValue) { +func (e *EffectDefeatTrigger) triggerByID(at model.AttackValue) { switch e.effectID { case 66: e.triggerHealSelfOnDefeat(at) @@ -115,7 +116,7 @@ func (e *EffectDefeatTrigger) triggerByID(at info.AttackValue) { // ----------------------------------------------------------- // triggerHealSelfOnDefeat:击败对方后,恢复自身最大体力的1/n(对应Effect66) -func (e *EffectDefeatTrigger) triggerHealSelfOnDefeat(_ info.AttackValue) { +func (e *EffectDefeatTrigger) triggerHealSelfOnDefeat(_ model.AttackValue) { // 计算恢复量:自身最大体力 / n(n=SideEffectArgs[0]) maxHP := e.Ctx().Our.CurrentPet.Info.MaxHp healAmount := alpacadecimal.NewFromInt(int64(maxHP)).Div(alpacadecimal.NewFromInt(int64(e.SideEffectArgs[0]))) @@ -124,7 +125,7 @@ func (e *EffectDefeatTrigger) triggerHealSelfOnDefeat(_ info.AttackValue) { } // triggerReduceNextHPOnDefeat:击败对方后,减少对方下次出战精灵最大体力的1/n(对应Effect67) -func (e *EffectDefeatTrigger) triggerReduceNextHPOnDefeat(_ info.AttackValue) { +func (e *EffectDefeatTrigger) triggerReduceNextHPOnDefeat(_ model.AttackValue) { // 计算伤害量:对方下只精灵最大体力 / n(n=SideEffectArgs[0]) nextMaxHP := e.Ctx().Opp.CurrentPet.Info.MaxHp // 假设CurrentPet为下次出战精灵 damageAmount := alpacadecimal.NewFromInt(int64(nextMaxHP)).Div(alpacadecimal.NewFromInt(int64(e.SideEffectArgs[0]))) @@ -140,7 +141,7 @@ func (e *EffectDefeatTrigger) triggerReduceNextHPOnDefeat(_ info.AttackValue) { // SideEffectArgs[0] = m(触发概率,如30=30%) // SideEffectArgs[1] = XX等级类型(如1=攻击等级,对应info.LevelType枚举) // SideEffectArgs[2] = n(提升的等级值,如1=+1级) -func (e *EffectDefeatTrigger) triggerLevelUpOnDefeat(_ info.AttackValue) { +func (e *EffectDefeatTrigger) triggerLevelUpOnDefeat(_ model.AttackValue) { // 1. 检查参数是否足够 if len(e.SideEffectArgs) < 3 { return @@ -162,7 +163,7 @@ func (e *EffectDefeatTrigger) triggerLevelUpOnDefeat(_ info.AttackValue) { // SideEffectArgs[0] = 目标对手类型(如info.PetType.Fire表示火系) // SideEffectArgs[1] = 要施加的状态(如info.PetStatus.Burned表示烧伤) // SideEffectArgs[2] = 状态持续回合 -func (e *EffectDefeatTrigger) triggerNextEnemyStatusOnDefeat(at info.AttackValue) { +func (e *EffectDefeatTrigger) triggerNextEnemyStatusOnDefeat(at model.AttackValue) { // 这里补充原逻辑中状态施加的完整判断(如检查对手类型是否匹配) // 简化示例:直接处理状态施加 for i, v := range at.Status { @@ -177,7 +178,7 @@ func (e *EffectDefeatTrigger) triggerNextEnemyStatusOnDefeat(at info.AttackValue } // triggerTransferBoostsOnDefeat:击败对手后,复制其所有能力提升效果到自身(对应Effect421) -func (e *EffectDefeatTrigger) triggerTransferBoostsOnDefeat(at info.AttackValue) { +func (e *EffectDefeatTrigger) triggerTransferBoostsOnDefeat(at model.AttackValue) { // 复制被击败对手的能力提升 for i, v := range at.Prop { if v > 0 { diff --git a/logic/service/fight/info/battle.go b/logic/service/fight/info/battle.go index 58b0ea50c..82fc8ac52 100644 --- a/logic/service/fight/info/battle.go +++ b/logic/service/fight/info/battle.go @@ -90,18 +90,6 @@ type DefaultEndData struct { Reason string } -// 战斗结束原因枚举 -type EnumBattleOverReason int - -var BattleOverReason = enum.New[struct { - PlayerOffline EnumBattleOverReason `enum:"1"` //掉线 - PlayerOVerTime EnumBattleOverReason `enum:"2"` //超时 - NOTwind EnumBattleOverReason `enum:"3"` //打成平手 - DefaultEnd EnumBattleOverReason `enum:"4"` //默认结束 - PlayerEscape EnumBattleOverReason `enum:"5"` //逃跑 - Cacthok EnumBattleOverReason `enum:"6"` -}]() - // 战斗模式 var Playerinvitemap map[uint32][]Playerinvite = make(map[uint32][]Playerinvite) //玩家邀请信息 ,比如一个玩家被多人邀请对战 diff --git a/logic/service/fight/info/info.go b/logic/service/fight/info/info.go index 4e9c0b521..ac5a49383 100644 --- a/logic/service/fight/info/info.go +++ b/logic/service/fight/info/info.go @@ -1,7 +1,6 @@ package info import ( - "blazing/common/data" "blazing/modules/player/model" "github.com/tnnmigga/enum" @@ -80,12 +79,12 @@ type FightPetInfo struct { Prop [6]int8 } type AttackValueS struct { - FAttack AttackValue - SAttack AttackValue + FAttack model.AttackValue + SAttack model.AttackValue } -func NewAttackValue(userid uint32) *AttackValue { - return &AttackValue{ +func NewAttackValue(userid uint32) *model.AttackValue { + return &model.AttackValue{ userid, 0, 0, @@ -103,28 +102,6 @@ func NewAttackValue(userid uint32) *AttackValue { } } -// AttackValue 战斗中的攻击数值信息 -type AttackValue struct { - UserID uint32 `json:"userId" fieldDescription:"玩家的米米号 与野怪对战userid = 0"` - SkillID uint32 `json:"skillId" fieldDescription:"使用技能的id"` - AttackTime uint32 `json:"attackTime" fieldDescription:"是否击中 如果为0 则miss 如果为1 则击中,2为必中"` - LostHp uint32 `json:"lostHp" fieldDescription:"我方造成的伤害"` - GainHp int32 `json:"gainHp" fieldDescription:"我方获得血量"` - RemainHp int32 `json:"remainHp" fieldDescription:"我方剩余血量"` - MaxHp uint32 `json:"maxHp" fieldDescription:"我方最大血量"` - //颜色 - State uint32 `json:"state" ` - SkillListLen uint32 `struc:"sizeof=SkillList"` - SkillList []model.SkillInfo `json:"skillList" fieldDescription:"根据精灵的数据插入技能 最多4条 不定长"` - IsCritical uint32 `json:"isCritical" fieldDescription:"是否暴击"` - Status [20]int8 //精灵的状态 - // 攻击,防御,特供,特防,速度,命中 - Prop [6]int8 - Offensive float32 - // OwnerMaxShield uint32 `json:"ownerMaxShield" fieldDescription:"我方最大护盾"` - // OwnerCurrentShield uint32 `json:"ownerCurrentShield" fieldDescription:"我方当前护盾"` -} - type WeakenedS struct { Stack int8 `struc:"skip"` Round int8 @@ -203,8 +180,8 @@ type PropDict struct { // NoteUseSkillOutboundInfo 战斗技能使用通知的出站信息结构体 type NoteUseSkillOutboundInfo struct { - FirstAttackInfo AttackValue // 本轮先手的精灵在释放技能结束后的状态 - SecondAttackInfo AttackValue // 本轮后手的精灵在释放技能结束后的状态 + FirstAttackInfo model.AttackValue // 本轮先手的精灵在释放技能结束后的状态 + SecondAttackInfo model.AttackValue // 本轮后手的精灵在释放技能结束后的状态 } type FightStartOutboundInfo struct { @@ -218,14 +195,6 @@ type FightStartOutboundInfo struct { Info2 FightPetInfo `fieldDesc:"当前战斗精灵的信息 可能不准.看前端代码是以userid来判断哪个结构体是我方的" serialize:"struct"` } -type FightUserInfo struct { - // 用户ID(野怪为0),@UInt long - UserID uint32 `fieldDesc:"userID 如果为野怪则为0" ` - - // 玩家名称(野怪为UTF-8的'-',固定16字节) - // 使用[16]byte存储固定长度的字节数组 - Nick string `struc:"[16]byte"` -} type S2C_2404 struct { // 用户ID(野怪为0),@UInt long UserID uint32 `fieldDesc:"userID 如果为野怪则为0" ` @@ -235,86 +204,7 @@ type S2C_50005 struct { Title uint32 } -// NoteReadyToFightInfo 战斗准备就绪消息结构体,NoteReadyToFightInfo -type NoteReadyToFightInfo struct { - //MAXPET uint32 `struc:"skip"` // 最大精灵数 struc:"skip"` - // 战斗类型ID(与野怪战斗为3,与人战斗为1,前端似乎未使用) - // @UInt long - Status uint32 `fieldDesc:"战斗类型ID 但前端好像没有用到 与野怪战斗为3,与人战斗似乎是1" ` - //Mode uint32 `struc:"skip"` - // 我方信息 - OurInfo FightUserInfo `fieldDesc:"我方信息" serialize:"struct"` - // Our *socket.Player `struc:"skip"` - OurPetListLen uint32 `struc:"sizeof=OurPetList"` - // 我方携带精灵的信息 - // ArrayList,使用切片模拟动态列表 - OurPetList []ReadyFightPetInfo `fieldDesc:"我方携带精灵的信息" serialize:"lengthFirst,lengthType=uint16,type=structArray"` - - // 对方信息 - OpponentInfo FightUserInfo `fieldDesc:"对方信息" serialize:"struct"` - //Opp *socket.Player `struc:"skip"` - OpponentPetListLen uint32 `struc:"sizeof=OpponentPetList"` - // 敌方的精灵信息 - // 野怪战斗时:客户端接收此包前已生成精灵PetInfo,将部分信息写入该列表 - OpponentPetList []ReadyFightPetInfo `fieldDesc:"敌方的精灵信息 如果是野怪 那么再给客户端发送这个包体时就提前生成好了这只精灵的PetInfo,然后把从PetInfo中把部分信息写入到这个敌方的精灵信息中再发送这个包结构体" serialize:"lengthFirst,lengthType=uint16,type=structArray"` -} - -// ReadyFightPetInfo 准备战斗的精灵信息结构体,ReadyFightPetInfo类 -type ReadyFightPetInfo struct { - // 精灵ID,@UInt long - ID uint32 `fieldDesc:"精灵ID" ` - - // 精灵等级,@UInt long - Level uint32 `fieldDesc:"精灵等级" ` - - // 精灵当前HP,@UInt long - Hp uint32 `fieldDesc:"精灵HP" ` - - // 精灵最大HP,@UInt long - MaxHp uint32 `fieldDesc:"最大HP" ` - SkillListLen uint32 `struc:"sizeof=SkillList"` - // 技能信息列表(固定4个元素,技能ID和剩余PP,无技能则为0) - // List,初始化容量为4 - SkillList []model.SkillInfo `fieldDesc:"技能信息 技能ID跟剩余PP 固定32字节 没有给0" serialize:"fixedLength=4,type=structArray"` - - // 精灵捕获时间,@UInt long - CatchTime uint32 `fieldDesc:"精灵捕获时间" ` - - // 捕捉地图(固定给0),@UInt long - CatchMap uint32 `fieldDesc:"捕捉地图 给0" ` - - // 固定给0,@UInt long - CatchRect uint32 `fieldDesc:"给0" ` - - // 固定给0,@UInt long - CatchLevel uint32 `fieldDesc:"给0" ` - SkinID uint32 `fieldDesc:"精灵皮肤ID" ` - // 是否闪光(@UInt long → uint32,0=否,1=是) - ShinyLen uint32 `json:"-" struc:"sizeof=ShinyInfo"` - ShinyInfo []data.GlowFilter `json:"ShinyInfo,omitempty"` -} - // FightOverInfo 战斗结束信息结构体 2506 -type FightOverInfo struct { - //0 正常结束 - //1=isPlayerLost 对方玩家退出 - // 2=isOvertime 超时 - // 3=isDraw 双方平手 - // 4=isSysError 系统错误 - // 5=isNpcEscape 精灵主动逃跑 - - Winpet *model.PetInfo `struc:"skip"` - Round uint32 `struc:"skip"` - LastAttavue AttackValue `struc:"skip"` - //7 切磋结束 - Reason EnumBattleOverReason // 固定值0 - WinnerId uint32 // 胜者的米米号 野怪为0 - TwoTimes uint32 // 双倍经验剩余次数 - ThreeTimes uint32 // 三倍经验剩余次数 - AutoFightTimes uint32 // 自动战斗剩余次数 - EnergyTime uint32 // 能量吸收器剩余次数 - LearnTimes uint32 // 双倍学习器剩余次数 -} type CatchMonsterOutboundInfo struct { // CatchTime 捕捉时间 diff --git a/logic/service/fight/input.go b/logic/service/fight/input.go index c5c146008..ebcbba63c 100644 --- a/logic/service/fight/input.go +++ b/logic/service/fight/input.go @@ -24,7 +24,7 @@ import ( type FightC struct { //准备战斗信息 - ReadyInfo info.NoteReadyToFightInfo + ReadyInfo model.NoteReadyToFightInfo //开始战斗信息 info.FightStartOutboundInfo Info info.Fightinfo @@ -47,9 +47,9 @@ type FightC struct { closefight bool overl sync.Once waittime int - info.FightOverInfo + model.FightOverInfo //战斗结束的插装 - callback func(info.FightOverInfo) + callback func(model.FightOverInfo) } func (f *FightC) Ownerid() uint32 { @@ -250,9 +250,9 @@ func RandomElfIDs(n int) []int { return ids } -func initfightready(in *input.Input) (info.FightUserInfo, []info.ReadyFightPetInfo) { - t := make([]info.ReadyFightPetInfo, len(in.AllPet)) - userindo := info.FightUserInfo{ +func initfightready(in *input.Input) (model.FightUserInfo, []model.ReadyFightPetInfo) { + t := make([]model.ReadyFightPetInfo, len(in.AllPet)) + userindo := model.FightUserInfo{ UserID: in.UserID, Nick: in.Player.GetInfo().Nick, } @@ -291,7 +291,7 @@ func (f *FightC) GetOverChan() chan struct{} { return f.over } -func (f *FightC) GetOverInfo() info.FightOverInfo { +func (f *FightC) GetOverInfo() model.FightOverInfo { return f.FightOverInfo } diff --git a/logic/service/fight/input/input.go b/logic/service/fight/input/input.go index f79ac514e..0310d5460 100644 --- a/logic/service/fight/input/input.go +++ b/logic/service/fight/input/input.go @@ -3,6 +3,7 @@ package input import ( "blazing/common/data/xmlres" "blazing/cool" + "blazing/modules/player/model" "fmt" "blazing/logic/service/common" @@ -23,7 +24,7 @@ type Input struct { Opp *Input CanCapture int Finished bool //是否加载完成 - *info.AttackValue + *model.AttackValue FightC common.FightI // info.BattleActionI Effects []Effect //effects 实际上全局就是effect无限回合 //effects容器 技能的 diff --git a/logic/service/fight/loop.go b/logic/service/fight/loop.go index 6ab6e2a6e..eeafb1006 100644 --- a/logic/service/fight/loop.go +++ b/logic/service/fight/loop.go @@ -5,6 +5,7 @@ import ( "blazing/common/socket/errorcode" "blazing/common/utils" "blazing/cool" + "blazing/modules/player/model" "context" "sync/atomic" @@ -97,7 +98,7 @@ func (f *FightC) battleLoop() { }) if f.Info.Status == info.BattleMode.FIGHT_WITH_NPC { addpet := f.Opp.Player.GetInfo().PetList[0] - if f.Reason == info.BattleOverReason.Cacthok { + if f.Reason == model.BattleOverReason.Cacthok { f.WinnerId = f.ownerID addpet.EffectInfo = nil //清空特性信息 @@ -280,7 +281,7 @@ func (f *FightC) handleTimeout(ourID, oppID uint32, actions map[uint32]action.Ba } //获胜方是已经出招的 f.WinnerId = f.GetInputByPlayer(f.getPlayerByID(pid), false).Player.GetInfo().UserID - f.Reason = info.BattleOverReason.PlayerOVerTime + f.Reason = model.BattleOverReason.PlayerOVerTime f.closefight = true return true } @@ -365,7 +366,7 @@ func (f *FightC) handleItemAction(a *action.UseItemAction) { ok, _ := f.Our.Capture(f.Opp.CurrentPet, a.ItemID, -1) our := f.Our.Player.(*player.Player) if ok { - f.Reason = info.BattleOverReason.Cacthok + f.Reason = model.BattleOverReason.Cacthok f.closefight = true } else { our.SendPack(common.NewTomeeHeader(2409, f.ownerID).Pack(&info.CatchMonsterOutboundInfo{})) diff --git a/logic/service/fight/new.go b/logic/service/fight/new.go index 645c17ea6..6b30f2969 100644 --- a/logic/service/fight/new.go +++ b/logic/service/fight/new.go @@ -9,12 +9,13 @@ import ( "blazing/logic/service/fight/input" "blazing/logic/service/player" "blazing/modules/config/service" + "blazing/modules/player/model" "math/rand" "time" ) // 创建新战斗,邀请方和被邀请方,或者玩家和野怪方 -func NewFight(p1, p2 common.PlayerI, fn func(info.FightOverInfo)) (*FightC, errorcode.ErrorCode) { +func NewFight(p1, p2 common.PlayerI, fn func(model.FightOverInfo)) (*FightC, errorcode.ErrorCode) { // fmt.Println("NewFight", p1.GetInfo().UserID) f := &FightC{} @@ -84,7 +85,7 @@ func NewFight(p1, p2 common.PlayerI, fn func(info.FightOverInfo)) (*FightC, erro //fmt.Println(f.Our.UserID, "战斗超时结算") if !f.Our.Finished || !f.Opp.Finished { //如果有任一没有加载完成 f.closefight = true //阻止继续添加action - f.Reason = info.BattleOverReason.PlayerOffline + f.Reason = model.BattleOverReason.PlayerOffline switch { case !f.Opp.Finished: //邀请方没加载完成 先判断邀请方,如果都没加载完成,就算做房主胜利 f.WinnerId = f.Our.Player.GetInfo().UserID diff --git a/logic/service/player/save.go b/logic/service/player/save.go index 93505feec..11bc942db 100644 --- a/logic/service/player/save.go +++ b/logic/service/player/save.go @@ -3,9 +3,9 @@ package player import ( "blazing/common/data/share" "blazing/cool" + "blazing/modules/player/model" "fmt" - "blazing/logic/service/fight/info" "blazing/logic/service/space" "context" "time" @@ -43,7 +43,7 @@ func (p *Player) Save() { } }() - p.FightC.Over(p, info.BattleOverReason.PlayerOffline) //玩家逃跑,但是不能锁线程 + p.FightC.Over(p, model.BattleOverReason.PlayerOffline) //玩家逃跑,但是不能锁线程 }() //<-ov select { diff --git a/modules/player/model/pvp.go b/modules/player/model/pvp.go new file mode 100644 index 000000000..e858c8ffb --- /dev/null +++ b/modules/player/model/pvp.go @@ -0,0 +1,187 @@ +package model + +import ( + "blazing/common/data" + "blazing/cool" + + "github.com/tnnmigga/enum" +) + +// 表名常量 +const TableNamePlayerPVP = "player_pvp" + +// PVP 对应数据库表 player_pvp,用于记录用户PVP赛季数据及场次统计 +type PVP struct { + Base + PlayerID uint64 `gorm:"not null;index:idx_pvp_player_id;comment:'所属玩家ID'" json:"player_id"` + //SeasonID uint32 `gorm:"not null;index:idx_pvp_season_id;comment:'赛季ID(如2026S1=101)'" json:"season_id"` + SeasonData []PVPRankInfo `gorm:"type:jsonb;not null;comment:'赛季核心数据'" json:"season_data"` + MatchRecords []PVPMatchRecord `gorm:"type:jsonb;not null;comment:'本赛季场次记录(仅存最近100场)'" json:"match_records"` + RankInfo PVPRankInfo `gorm:"type:jsonb;not null;comment:'本赛季排名信息'" json:"rank_info"` +} + +// PVPSeasonData PVP赛季核心统计数据 +// 聚合维度:总场次、胜负、积分、胜率等 +type PVPRankInfo struct { + Rank uint32 `json:"rank"` // 本赛季全服排名(0=未上榜) + Segment uint32 `json:"segment"` // 段位ID(如1=青铜 2=白银...) + SegmentStar uint32 `json:"segment_star"` // 段位星级(如青铜3星) + NextSegmentScore int32 `json:"next_segment_score"` // 升段所需积分 + TotalMatch uint32 `json:"total_match"` // 本赛季总场次 + WinMatch uint32 `json:"win_match"` // 本赛季胜利场次 + LoseMatch uint32 `json:"lose_match"` // 本赛季失败场次 + DrawMatch uint32 `json:"draw_match"` // 本赛季平局场次 + TotalScore int32 `json:"total_score"` // 本赛季总积分(胜加负减) + HighestScore int32 `json:"highest_score"` // 本赛季最高积分 + ContinuousWin uint32 `json:"continuous_win"` // 本赛季最高连胜次数 + LastMatchTime uint64 `json:"last_match_time"` // 最后一场PVP时间(时间戳) +} + +// PVPMatchRecord PVP单场记录 +// 记录每一场的详细信息,便于复盘和统计 +type PVPMatchRecord struct { + MatchID string `json:"match_id"` // 匹配局ID(全局唯一) + MatchTime uint64 `json:"match_time"` // 对局时间(时间戳) + NoteReadyToFightInfo + Result uint32 `json:"result"` // 对局结果:0=负 1=胜 2=平 + ScoreChange int32 `json:"score_change"` // 本场积分变化(+10/-5等) + UsedPetIDs []uint32 `json:"used_pet_ids"` // 本场使用的精灵ID列表 + WinStreak uint32 `json:"win_streak"` // 本场结束后的连胜数 + Duration uint32 `json:"duration"` // 对局时长(秒) +} + +// NoteReadyToFightInfo 战斗准备就绪消息结构体,NoteReadyToFightInfo +type NoteReadyToFightInfo struct { + //MAXPET uint32 `struc:"skip"` // 最大精灵数 struc:"skip"` + // 战斗类型ID(与野怪战斗为3,与人战斗为1,前端似乎未使用) + // @UInt long + Status uint32 `fieldDesc:"战斗类型ID 但前端好像没有用到 与野怪战斗为3,与人战斗似乎是1" ` + //Mode uint32 `struc:"skip"` + // 我方信息 + OurInfo FightUserInfo `fieldDesc:"我方信息" serialize:"struct"` + // Our *socket.Player `struc:"skip"` + OurPetListLen uint32 `struc:"sizeof=OurPetList"` + // 我方携带精灵的信息 + // ArrayList,使用切片模拟动态列表 + OurPetList []ReadyFightPetInfo `fieldDesc:"我方携带精灵的信息" serialize:"lengthFirst,lengthType=uint16,type=structArray"` + + // 对方信息 + OpponentInfo FightUserInfo `fieldDesc:"对方信息" serialize:"struct"` + //Opp *socket.Player `struc:"skip"` + OpponentPetListLen uint32 `struc:"sizeof=OpponentPetList"` + // 敌方的精灵信息 + // 野怪战斗时:客户端接收此包前已生成精灵PetInfo,将部分信息写入该列表 + OpponentPetList []ReadyFightPetInfo `fieldDesc:"敌方的精灵信息 如果是野怪 那么再给客户端发送这个包体时就提前生成好了这只精灵的PetInfo,然后把从PetInfo中把部分信息写入到这个敌方的精灵信息中再发送这个包结构体" serialize:"lengthFirst,lengthType=uint16,type=structArray"` +} +type FightUserInfo struct { + // 用户ID(野怪为0),@UInt long + UserID uint32 `fieldDesc:"userID 如果为野怪则为0" ` + + // 玩家名称(野怪为UTF-8的'-',固定16字节) + // 使用[16]byte存储固定长度的字节数组 + Nick string `struc:"[16]byte"` +} + +// ReadyFightPetInfo 准备战斗的精灵信息结构体,ReadyFightPetInfo类 +type ReadyFightPetInfo struct { + // 精灵ID,@UInt long + ID uint32 `fieldDesc:"精灵ID" ` + + // 精灵等级,@UInt long + Level uint32 `fieldDesc:"精灵等级" ` + + // 精灵当前HP,@UInt long + Hp uint32 `fieldDesc:"精灵HP" ` + + // 精灵最大HP,@UInt long + MaxHp uint32 `fieldDesc:"最大HP" ` + SkillListLen uint32 `struc:"sizeof=SkillList"` + // 技能信息列表(固定4个元素,技能ID和剩余PP,无技能则为0) + // List,初始化容量为4 + SkillList []SkillInfo `fieldDesc:"技能信息 技能ID跟剩余PP 固定32字节 没有给0" serialize:"fixedLength=4,type=structArray"` + + // 精灵捕获时间,@UInt long + CatchTime uint32 `fieldDesc:"精灵捕获时间" ` + + // 捕捉地图(固定给0),@UInt long + CatchMap uint32 `fieldDesc:"捕捉地图 给0" ` + + // 固定给0,@UInt long + CatchRect uint32 `fieldDesc:"给0" ` + + // 固定给0,@UInt long + CatchLevel uint32 `fieldDesc:"给0" ` + SkinID uint32 `fieldDesc:"精灵皮肤ID" ` + // 是否闪光(@UInt long → uint32,0=否,1=是) + ShinyLen uint32 `json:"-" struc:"sizeof=ShinyInfo"` + ShinyInfo []data.GlowFilter `json:"ShinyInfo,omitempty"` +} +type FightOverInfo struct { + //0 正常结束 + //1=isPlayerLost 对方玩家退出 + // 2=isOvertime 超时 + // 3=isDraw 双方平手 + // 4=isSysError 系统错误 + // 5=isNpcEscape 精灵主动逃跑 + + Winpet *PetInfo `struc:"skip"` + Round uint32 `struc:"skip"` + LastAttavue AttackValue `struc:"skip"` + //7 切磋结束 + Reason EnumBattleOverReason // 固定值0 + WinnerId uint32 // 胜者的米米号 野怪为0 + TwoTimes uint32 // 双倍经验剩余次数 + ThreeTimes uint32 // 三倍经验剩余次数 + AutoFightTimes uint32 // 自动战斗剩余次数 + EnergyTime uint32 // 能量吸收器剩余次数 + LearnTimes uint32 // 双倍学习器剩余次数 +} + +// 战斗结束原因枚举 +type EnumBattleOverReason int + +var BattleOverReason = enum.New[struct { + PlayerOffline EnumBattleOverReason `enum:"1"` //掉线 + PlayerOVerTime EnumBattleOverReason `enum:"2"` //超时 + NOTwind EnumBattleOverReason `enum:"3"` //打成平手 + DefaultEnd EnumBattleOverReason `enum:"4"` //默认结束 + PlayerEscape EnumBattleOverReason `enum:"5"` //逃跑 + Cacthok EnumBattleOverReason `enum:"6"` +}]() + +// AttackValue 战斗中的攻击数值信息 +type AttackValue struct { + UserID uint32 `json:"userId" fieldDescription:"玩家的米米号 与野怪对战userid = 0"` + SkillID uint32 `json:"skillId" fieldDescription:"使用技能的id"` + AttackTime uint32 `json:"attackTime" fieldDescription:"是否击中 如果为0 则miss 如果为1 则击中,2为必中"` + LostHp uint32 `json:"lostHp" fieldDescription:"我方造成的伤害"` + GainHp int32 `json:"gainHp" fieldDescription:"我方获得血量"` + RemainHp int32 `json:"remainHp" fieldDescription:"我方剩余血量"` + MaxHp uint32 `json:"maxHp" fieldDescription:"我方最大血量"` + //颜色 + State uint32 `json:"state" ` + SkillListLen uint32 `struc:"sizeof=SkillList"` + SkillList []SkillInfo `json:"skillList" fieldDescription:"根据精灵的数据插入技能 最多4条 不定长"` + IsCritical uint32 `json:"isCritical" fieldDescription:"是否暴击"` + Status [20]int8 //精灵的状态 + // 攻击,防御,特供,特防,速度,命中 + Prop [6]int8 + Offensive float32 + // OwnerMaxShield uint32 `json:"ownerMaxShield" fieldDescription:"我方最大护盾"` + // OwnerCurrentShield uint32 `json:"ownerCurrentShield" fieldDescription:"我方当前护盾"` +} + +// TableName 返回表名 +func (*PVP) TableName() string { + return TableNamePlayerPVP +} + +// GroupName 返回表组名 +func (*PVP) GroupName() string { + return "default" +} + +// init 程序启动时自动创建表 +func init() { + cool.CreateTable(&PVP{}) +}