Merge branch 'develop'

This commit is contained in:
kercylan98 2023-07-19 15:05:33 +08:00
commit 013436ab03
21 changed files with 647 additions and 0 deletions

View File

@ -19,6 +19,10 @@ func (slf *Player[ID]) GetID() ID {
return slf.id
}
func (slf *Player[ID]) GetConn() *server.Conn {
return slf.conn
}
func (slf *Player[ID]) UseConn(conn *server.Conn) {
if conn == nil {
return

View File

@ -6,6 +6,8 @@ import "github.com/kercylan98/minotaur/server"
type Player[ID comparable] interface {
// GetID 获取玩家ID
GetID() ID
// GetConn 获取玩家连接
GetConn() *server.Conn
// UseConn 指定连接
UseConn(conn *server.Conn)
// Close 关闭玩家并且释放其资源

View File

@ -0,0 +1,51 @@
package storage
// NewGlobalData 创建全局数据
func NewGlobalData[T any](name string, storage GlobalDataStorage[T]) *GlobalData[T] {
data := &GlobalData[T]{
storage: storage,
name: name,
data: storage.Load(name),
}
globalDataSaveHandles = append(globalDataSaveHandles, data.SaveData)
return data
}
// GlobalData 全局数据
type GlobalData[T any] struct {
storage GlobalDataStorage[T] // 全局数据存储器
name string // 全局数据名称
data T // 数据
}
// GetName 获取名称
func (slf *GlobalData[T]) GetName() string {
return slf.name
}
// GetData 获取数据
func (slf *GlobalData[T]) GetData() T {
return slf.data
}
// LoadData 加载数据
func (slf *GlobalData[T]) LoadData() {
slf.data = slf.storage.Load(slf.GetName())
}
// SaveData 保存数据
func (slf *GlobalData[T]) SaveData() error {
return slf.storage.Save(slf.GetName(), slf.GetData())
}
// Handle 处理数据
func (slf *GlobalData[T]) Handle(handler func(name string, data T)) *GlobalData[T] {
handler(slf.GetName(), slf.GetData())
return slf
}
// HandleWithCallback 处理数据
func (slf *GlobalData[T]) HandleWithCallback(handler func(name string, data T) error, callback func(err error)) *GlobalData[T] {
callback(handler(slf.GetName(), slf.GetData()))
return slf
}

View File

@ -0,0 +1,10 @@
package storage
// GlobalDataStorage 全局数据存储器接口
type GlobalDataStorage[T any] interface {
// Load 加载全局数据
// - 当全局数据不存在时,应当返回新的全局数据实例
Load(name string) T
// Save 保存全局数据
Save(name string, data T) error
}

View File

@ -0,0 +1,92 @@
package storage
import (
"github.com/kercylan98/minotaur/utils/generic"
"time"
)
// NewIndexData 创建索引数据
func NewIndexData[I generic.Ordered, T IndexDataItem[I]](name string, storage IndexDataStorage[I, T]) *IndexData[I, T] {
data := &IndexData[I, T]{
storage: storage,
name: name,
data: storage.LoadAll(name),
}
return data
}
// IndexData 全局数据
type IndexData[I generic.Ordered, T IndexDataItem[I]] struct {
storage IndexDataStorage[I, T] // 存储器
name string // 数据组名称
data map[I]T // 数据
}
// GetName 获取名称
func (slf *IndexData[I, T]) GetName() string {
return slf.name
}
// GetData 获取数据
func (slf *IndexData[I, T]) GetData(index I) T {
data, exist := slf.data[index]
if !exist {
slf.LoadData(index)
data = slf.data[index]
}
return data
}
// GetAllData 获取所有数据
func (slf *IndexData[I, T]) GetAllData() map[I]T {
return slf.data
}
// LoadData 加载数据
func (slf *IndexData[I, T]) LoadData(index I) {
slf.data[index] = slf.storage.Load(slf.GetName(), index)
}
// LoadAllData 加载所有数据
func (slf *IndexData[I, T]) LoadAllData() {
slf.data = slf.storage.LoadAll(slf.GetName())
}
// SaveData 保存数据
func (slf *IndexData[I, T]) SaveData(index I) error {
return slf.storage.Save(slf.GetName(), index, slf.GetData(index))
}
// SaveAllData 保存所有数据
// - errHandle 错误处理中如果返回 false 将重试,否则跳过当前保存下一个
func (slf *IndexData[I, T]) SaveAllData(errHandle func(err error) bool, retryInterval time.Duration) {
slf.storage.SaveAll(slf.GetName(), slf.GetAllData(), errHandle, retryInterval)
}
// DeleteData 删除数据
func (slf *IndexData[I, T]) DeleteData(index I) *IndexData[I, T] {
slf.storage.Delete(slf.GetName(), index)
delete(slf.data, index)
return slf
}
// DeleteAllData 删除所有数据
func (slf *IndexData[I, T]) DeleteAllData() *IndexData[I, T] {
slf.storage.DeleteAll(slf.GetName())
for k := range slf.data {
delete(slf.data, k)
}
return slf
}
// Handle 处理数据
func (slf *IndexData[I, T]) Handle(index I, handler func(name string, index I, data T)) *IndexData[I, T] {
handler(slf.GetName(), index, slf.GetData(index))
return slf
}
// HandleWithCallback 处理数据
func (slf *IndexData[I, T]) HandleWithCallback(index I, handler func(name string, index I, data T) error, callback func(err error)) *IndexData[I, T] {
callback(handler(slf.GetName(), index, slf.GetData(index)))
return slf
}

View File

@ -0,0 +1,7 @@
package storage
import "github.com/kercylan98/minotaur/utils/generic"
type IndexDataItem[I generic.Ordered] interface {
GetIndex() I
}

View File

@ -0,0 +1,23 @@
package storage
import (
"github.com/kercylan98/minotaur/utils/generic"
"time"
)
// IndexDataStorage 全局数据存储器接口
type IndexDataStorage[I generic.Ordered, T IndexDataItem[I]] interface {
// Load 加载特定索引数据
// - 通常情况下当数据不存在时,应当返回空指针
Load(name string, index I) T
// LoadAll 加载所有数据
LoadAll(name string) map[I]T
// Save 保存特定索引数据
Save(name string, index I, data T) error
// SaveAll 保存所有数据
SaveAll(name string, data map[I]T, errHandle func(err error) bool, retryInterval time.Duration)
// Delete 删除特定索引数据
Delete(name string, index I)
// DeleteAll 删除所有数据
DeleteAll(name string)
}

26
utils/storage/storage.go Normal file
View File

@ -0,0 +1,26 @@
package storage
import "time"
var (
// globalDataSaveHandles 全局数据保存句柄
globalDataSaveHandles []func() error
)
// SaveAll 保存所有数据
// - errorHandle 错误处理中如果返回 false 将重试,否则跳过当前保存下一个
func SaveAll(errorHandle func(err error) bool, retryInterval time.Duration) {
var err error
for _, handle := range globalDataSaveHandles {
for {
if err = handle(); err != nil {
if !errorHandle(err) {
time.Sleep(retryInterval)
continue
}
break
}
break
}
}
}

View File

@ -0,0 +1,23 @@
package storages
import "encoding/json"
// FileStorageEncoder 全局数据文件存储编码器
type FileStorageEncoder[T any] func(data T) ([]byte, error)
// FileStorageDecoder 全局数据文件存储解码器
type FileStorageDecoder[T any] func(bytes []byte, data T) error
// FileStorageJSONEncoder JSON 编码器
func FileStorageJSONEncoder[T any]() FileStorageEncoder[T] {
return func(data T) ([]byte, error) {
return json.Marshal(data)
}
}
// FileStorageJSONDecoder JSON 解码器
func FileStorageJSONDecoder[T any]() FileStorageDecoder[T] {
return func(bytes []byte, data T) error {
return json.Unmarshal(bytes, data)
}
}

View File

@ -0,0 +1 @@
{"CreateAt":"2023-07-19T14:49:35.7235348+08:00","TotalCount":10}

View File

@ -0,0 +1 @@
{"ID":"INDEX_001","Value":10}

View File

@ -0,0 +1,63 @@
package storages
import (
"fmt"
"github.com/kercylan98/minotaur/utils/file"
"path/filepath"
)
const (
// GlobalDataFileDefaultSuffix 是 GlobalDataFileStorage 的文件默认后缀
GlobalDataFileDefaultSuffix = "stock"
)
// NewGlobalDataFileStorage 创建一个 GlobalDataFileStoragedir 是文件存储目录generate 是生成数据的函数options 是可选参数
// - 生成的文件名为 ${name}.${suffix},可以通过 WithGlobalDataFileStorageSuffix 修改后缀
// - 默认使用 JSON 格式存储,可以通过 WithGlobalDataFileStorageEncoder 和 WithGlobalDataFileStorageDecoder 修改编码和解码函数
// - 内置了 JSON 编码和解码函数,可以通过 FileStorageJSONEncoder 和 FileStorageJSONDecoder 获取
func NewGlobalDataFileStorage[T any](dir string, generate func(name string) T, options ...GlobalDataFileStorageOption[T]) *GlobalDataFileStorage[T] {
abs, err := filepath.Abs(dir)
if err != nil {
panic(err)
}
storage := &GlobalDataFileStorage[T]{
dir: abs,
suffix: GlobalDataFileDefaultSuffix,
generate: generate,
encoder: FileStorageJSONEncoder[T](),
decoder: FileStorageJSONDecoder[T](),
}
for _, option := range options {
option(storage)
}
return storage
}
// GlobalDataFileStorage 用于存储全局数据的文件存储器
type GlobalDataFileStorage[T any] struct {
dir string
suffix string
generate func(name string) T
encoder FileStorageEncoder[T]
decoder FileStorageDecoder[T]
}
// Load 从文件中加载数据,如果文件不存在则使用 generate 生成数据
func (slf *GlobalDataFileStorage[T]) Load(name string) T {
bytes, err := file.ReadOnce(filepath.Join(slf.dir, fmt.Sprintf("%s.%s", name, slf.suffix)))
if err != nil {
return slf.generate(name)
}
var data = slf.generate(name)
_ = slf.decoder(bytes, data)
return data
}
// Save 将数据保存到文件中
func (slf *GlobalDataFileStorage[T]) Save(name string, data T) error {
bytes, err := slf.encoder(data)
if err != nil {
return err
}
return file.WriterFile(filepath.Join(slf.dir, fmt.Sprintf("%s.%s", name, slf.suffix)), bytes)
}

View File

@ -0,0 +1,19 @@
package storages_test
import (
"fmt"
"github.com/kercylan98/minotaur/utils/storage/storages"
"time"
)
func ExampleNewGlobalDataFileStorage() {
storage := storages.NewGlobalDataFileStorage[*GlobalData](".", func(name string) *GlobalData {
return &GlobalData{
CreateAt: time.Now(),
}
})
fmt.Println(storage != nil)
// Output:
// true
}

View File

@ -0,0 +1,28 @@
package storages
// GlobalDataFileStorageOption 全局数据文件存储选项
type GlobalDataFileStorageOption[T any] func(storage *GlobalDataFileStorage[T])
// WithGlobalDataFileStorageEncoder 设置编码器
// - 默认为 FileStorageJSONEncoder 编码器
func WithGlobalDataFileStorageEncoder[T any](encoder FileStorageEncoder[T]) GlobalDataFileStorageOption[T] {
return func(storage *GlobalDataFileStorage[T]) {
storage.encoder = encoder
}
}
// WithGlobalDataFileStorageDecoder 设置解码器
// - 默认为 FileStorageJSONDecoder 解码器
func WithGlobalDataFileStorageDecoder[T any](decoder FileStorageDecoder[T]) GlobalDataFileStorageOption[T] {
return func(storage *GlobalDataFileStorage[T]) {
storage.decoder = decoder
}
}
// WithGlobalDataFileStorageSuffix 设置文件后缀
// - 默认为 GlobalDataFileDefaultSuffix
func WithGlobalDataFileStorageSuffix[T any](suffix string) GlobalDataFileStorageOption[T] {
return func(storage *GlobalDataFileStorage[T]) {
storage.suffix = suffix
}
}

View File

@ -0,0 +1,42 @@
package storages_test
import (
"github.com/kercylan98/minotaur/utils/storage"
"github.com/kercylan98/minotaur/utils/storage/storages"
. "github.com/smartystreets/goconvey/convey"
"testing"
"time"
)
type GlobalData struct {
CreateAt time.Time
TotalCount int
}
func TestGlobalDataFileStorage_Save(t *testing.T) {
Convey("TestGlobalDataFileStorage_Save", t, func() {
data := storage.NewGlobalData[*GlobalData]("global_data_file_test", storages.NewGlobalDataFileStorage[*GlobalData]("./example-data", func(name string) *GlobalData {
return &GlobalData{
CreateAt: time.Now(),
}
}))
data.Handle(func(name string, data *GlobalData) {
data.TotalCount = 10
})
if err := data.SaveData(); err != nil {
t.Fatal(err)
}
So(data.GetData().TotalCount, ShouldEqual, 10)
})
}
func TestGlobalDataFileStorage_Load(t *testing.T) {
Convey("TestGlobalDataFileStorage_Load", t, func() {
data := storage.NewGlobalData[*GlobalData]("global_data_file_test", storages.NewGlobalDataFileStorage[*GlobalData]("./example-data", func(name string) *GlobalData {
return &GlobalData{
CreateAt: time.Now(),
}
}))
So(data.GetData().TotalCount, ShouldEqual, 10)
})
}

View File

@ -0,0 +1,117 @@
package storages
import (
"fmt"
"github.com/kercylan98/minotaur/utils/file"
"github.com/kercylan98/minotaur/utils/generic"
"github.com/kercylan98/minotaur/utils/storage"
"os"
"path/filepath"
"strings"
"time"
)
const (
// IndexDataFileDefaultSuffix 是 IndexDataFileStorage 的文件默认后缀
IndexDataFileDefaultSuffix = "stock"
indexNameFormat = "%s.%v.%s"
)
// NewIndexDataFileStorage 创建索引数据文件存储器
func NewIndexDataFileStorage[I generic.Ordered, T storage.IndexDataItem[I]](dir string, generate func(name string, index I) T, generateZero func(name string) T, options ...IndexDataFileStorageOption[I, T]) *IndexDataFileStorage[I, T] {
s := &IndexDataFileStorage[I, T]{
dir: dir,
suffix: IndexDataFileDefaultSuffix,
generate: generate,
generateZero: generateZero,
encoder: FileStorageJSONEncoder[T](),
decoder: FileStorageJSONDecoder[T](),
}
for _, option := range options {
option(s)
}
return s
}
// IndexDataFileStorage 索引数据文件存储器
type IndexDataFileStorage[I generic.Ordered, T storage.IndexDataItem[I]] struct {
dir string
suffix string
generate func(name string, index I) T
generateZero func(name string) T
encoder FileStorageEncoder[T]
decoder FileStorageDecoder[T]
}
func (slf *IndexDataFileStorage[I, T]) Load(name string, index I) T {
bytes, err := file.ReadOnce(filepath.Join(slf.dir, fmt.Sprintf(indexNameFormat, name, index, slf.suffix)))
if err != nil {
return slf.generate(name, index)
}
var data = slf.generate(name, index)
_ = slf.decoder(bytes, data)
return data
}
func (slf *IndexDataFileStorage[I, T]) LoadAll(name string) map[I]T {
var result = make(map[I]T)
files, err := os.ReadDir(slf.dir)
if err != nil {
return result
}
for _, entry := range files {
if entry.IsDir() || !strings.HasPrefix(entry.Name(), name) || !strings.HasSuffix(entry.Name(), slf.suffix) {
continue
}
bytes, err := file.ReadOnce(filepath.Join(slf.dir, entry.Name()))
if err != nil {
continue
}
data := slf.generateZero(name)
if err := slf.decoder(bytes, data); err == nil {
result[data.GetIndex()] = data
}
}
return result
}
func (slf *IndexDataFileStorage[I, T]) Save(name string, index I, data T) error {
bytes, err := slf.encoder(data)
if err != nil {
return err
}
return file.WriterFile(filepath.Join(slf.dir, fmt.Sprintf(indexNameFormat, name, index, slf.suffix)), bytes)
}
func (slf *IndexDataFileStorage[I, T]) SaveAll(name string, data map[I]T, errHandle func(err error) bool, retryInterval time.Duration) {
for index, data := range data {
for {
if err := slf.Save(name, index, data); err != nil {
if !errHandle(err) {
time.Sleep(retryInterval)
continue
}
break
}
break
}
}
}
func (slf *IndexDataFileStorage[I, T]) Delete(name string, index I) {
_ = os.Remove(filepath.Join(slf.dir, fmt.Sprintf(indexNameFormat, name, index, slf.suffix)))
}
func (slf *IndexDataFileStorage[I, T]) DeleteAll(name string) {
files, err := os.ReadDir(slf.dir)
if err != nil {
return
}
for _, entry := range files {
if entry.IsDir() || !strings.HasPrefix(entry.Name(), name) || !strings.HasSuffix(entry.Name(), slf.suffix) {
continue
}
_ = os.Remove(filepath.Join(slf.dir, entry.Name()))
}
}

View File

@ -0,0 +1,18 @@
package storages_test
import (
"fmt"
"github.com/kercylan98/minotaur/utils/storage/storages"
)
func ExampleNewIndexDataFileStorage() {
storage := storages.NewIndexDataFileStorage[string, *IndexData[string]]("./example-data", func(name string, index string) *IndexData[string] {
return &IndexData[string]{ID: index}
}, func(name string) *IndexData[string] {
return new(IndexData[string])
})
fmt.Println(storage != nil)
// Output:
// true
}

View File

@ -0,0 +1,33 @@
package storages
import (
"github.com/kercylan98/minotaur/utils/generic"
"github.com/kercylan98/minotaur/utils/storage"
)
// IndexDataFileStorageOption 索引数据文件存储器选项
type IndexDataFileStorageOption[I generic.Ordered, T storage.IndexDataItem[I]] func(storage *IndexDataFileStorage[I, T])
// WithIndexDataFileStorageEncoder 设置编码器
// - 默认为 FileStorageJSONEncoder 编码器
func WithIndexDataFileStorageEncoder[I generic.Ordered, T storage.IndexDataItem[I]](encoder FileStorageEncoder[T]) IndexDataFileStorageOption[I, T] {
return func(storage *IndexDataFileStorage[I, T]) {
storage.encoder = encoder
}
}
// WithIndexDataFileStorageDecoder 设置解码器
// - 默认为 FileStorageJSONDecoder 解码器
func WithIndexDataFileStorageDecoder[I generic.Ordered, T storage.IndexDataItem[I]](decoder FileStorageDecoder[T]) IndexDataFileStorageOption[I, T] {
return func(storage *IndexDataFileStorage[I, T]) {
storage.decoder = decoder
}
}
// WithIndexDataFileStorageSuffix 设置文件后缀
// - 默认为 IndexDataFileDefaultSuffix
func WithIndexDataFileStorageSuffix[I generic.Ordered, T storage.IndexDataItem[I]](suffix string) IndexDataFileStorageOption[I, T] {
return func(storage *IndexDataFileStorage[I, T]) {
storage.suffix = suffix
}
}

View File

@ -0,0 +1,45 @@
package storages_test
import (
"github.com/kercylan98/minotaur/utils/storage"
"github.com/kercylan98/minotaur/utils/storage/storages"
. "github.com/smartystreets/goconvey/convey"
"testing"
)
type IndexData[I string] struct {
ID I
Value int
}
func (slf *IndexData[I]) GetIndex() I {
return slf.ID
}
func TestIndexDataFileStorage_Save(t *testing.T) {
Convey("TestIndexDataFileStorage_Save", t, func() {
data := storage.NewIndexData[string, *IndexData[string]]("index_data_file_test", storages.NewIndexDataFileStorage[string, *IndexData[string]]("./example-data", func(name string, index string) *IndexData[string] {
return &IndexData[string]{ID: index}
}, func(name string) *IndexData[string] {
return new(IndexData[string])
}))
data.Handle("INDEX_001", func(name string, index string, data *IndexData[string]) {
data.Value = 10
})
if err := data.SaveData("INDEX_001"); err != nil {
t.Fatal(err)
}
So(data.GetData("INDEX_001").Value, ShouldEqual, 10)
})
}
func TestIndexDataFileStorage_Load(t *testing.T) {
Convey("TestIndexDataFileStorage_Load", t, func() {
data := storage.NewIndexData[string, *IndexData[string]]("index_data_file_test", storages.NewIndexDataFileStorage[string, *IndexData[string]]("./example-data", func(name string, index string) *IndexData[string] {
return &IndexData[string]{ID: index}
}, func(name string) *IndexData[string] {
return new(IndexData[string])
}))
So(data.GetData("INDEX_001").Value, ShouldEqual, 10)
})
}

22
utils/super/error.go Normal file
View File

@ -0,0 +1,22 @@
package super
import (
"errors"
)
var errorMapper = make(map[error]int)
// RegError 通过错误码注册错误,返回错误的引用
func RegError(code int, message string) error {
if code == 0 {
panic("error code can not be 0")
}
err := errors.New(message)
errorMapper[err] = code
return err
}
// GetErrorCode 通过错误引用获取错误码,如果错误不存在则返回 0
func GetErrorCode(err error) int {
return errorMapper[err]
}

View File

@ -17,3 +17,23 @@ func CalcNextSecWithTime(t time.Time, sec int) int {
next := now + int64(sec) - now%int64(sec)
return int(next - now)
}
// CalcNextTimeWithRefer 根据参考时间计算下一个整点时间
// - 假设当 now 为 14:15:16 参考时间为 10 分钟, 则返回 14:20:00
// - 假设当 now 为 14:15:16 参考时间为 20 分钟, 则返回 14:20:00
//
// 当 refer 小于 1 分钟时,将会返回当前时间
func CalcNextTimeWithRefer(now time.Time, refer time.Duration) time.Time {
referInSeconds := int(refer.Minutes()) * 60
if referInSeconds <= 0 {
return now
}
minutes := now.Minute()
seconds := now.Second()
remainder := referInSeconds - (minutes*60+seconds)%referInSeconds
nextTime := now.Add(time.Duration(remainder) * time.Second)
nextTime = time.Date(nextTime.Year(), nextTime.Month(), nextTime.Day(), nextTime.Hour(), nextTime.Minute(), 0, 0, nextTime.Location())
return nextTime
}