├── docs ├── .nojekyll ├── _404.md ├── en │ ├── _404.md │ ├── _sidebar.md │ └── README.md ├── _navbar.md ├── _sidebar.md ├── index.html ├── development │ └── api │ │ ├── README.md │ │ ├── localization.md │ │ └── card_deck.md └── README.md ├── data ├── persistence │ └── .gitkeep └── .gitignore ├── mod ├── adapter │ ├── mod.go │ ├── player.go │ ├── skill.go │ ├── summon.go │ ├── rule.go │ ├── event.go │ ├── single.go │ └── card.go ├── definition │ ├── summon.go │ ├── damage.go │ ├── context.go │ ├── rule.go │ ├── character.go │ ├── mod.go │ ├── event.go │ ├── factory.go │ ├── skill.go │ ├── card.go │ ├── language.go │ ├── modifier.go │ └── description.go └── implement │ ├── benchmark_test.go │ ├── entity.go │ ├── damage.go │ ├── context.go │ ├── event.go │ ├── character.go │ ├── rule.go │ ├── skill.go │ ├── mod.go │ └── util.go ├── protocol ├── websocket │ ├── service │ │ ├── battle.go │ │ └── match.go │ ├── message │ │ ├── initialize.go │ │ └── action.go │ ├── config │ │ └── handler.go │ └── engine.go └── http │ ├── config │ ├── service.go │ ├── engine.go │ └── middleware.go │ ├── service │ ├── init.go │ ├── config.go │ ├── roominfo.go │ └── localization.go │ ├── message │ ├── localization.go │ ├── carddeck.go │ └── player.go │ ├── middleware │ ├── uuid.go │ ├── qps.go │ ├── trace.go │ ├── interdictor.go │ └── authenticator.go │ ├── engine.go │ └── util │ └── preprocess.go ├── entity ├── support.go ├── summon.go ├── model │ ├── entity.go │ ├── event.go │ ├── skill.go │ ├── interface.go │ ├── card.go │ ├── rule.go │ └── cost.go ├── carddeck.go ├── benchmark_test.go ├── eventmap.go ├── character.go └── player.go ├── model ├── adapter │ └── adapter.go ├── modifier │ ├── modifier.go │ ├── definition │ │ └── modifier.go │ ├── context.go │ └── benchmark_test.go ├── kv │ ├── map.go │ ├── pair.go │ ├── simple.go │ └── sync.go ├── context │ ├── charge.go │ ├── heal.go │ ├── cost.go │ ├── callback.go │ └── damage.go └── localization │ └── language.go ├── enum ├── description.go ├── rule.go ├── modifier.go ├── equipment.go ├── skill.go ├── weapon.go ├── stage.go ├── character.go ├── language.go ├── affiliation.go ├── player.go ├── action.go ├── trigger.go ├── element.go ├── card.go └── reaction.go ├── util ├── logger.go ├── password.go ├── benchmark_test.go ├── os.go ├── unit_test.go └── generate.go ├── exec ├── cli │ └── main.go └── backend │ └── main.go ├── .github └── workflows │ └── go.yml ├── license ├── .drone.yml ├── persistence ├── memcache.go ├── adapter.go ├── interface.go ├── sqlite.go ├── model.go ├── db.go └── timing.go ├── go.mod ├── main.go ├── .gitignore └── readme.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/persistence/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mod/adapter/mod.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | -------------------------------------------------------------------------------- /mod/adapter/player.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | -------------------------------------------------------------------------------- /mod/adapter/skill.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | -------------------------------------------------------------------------------- /mod/adapter/summon.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | -------------------------------------------------------------------------------- /protocol/websocket/service/battle.go: -------------------------------------------------------------------------------- 1 | package service 2 | -------------------------------------------------------------------------------- /protocol/websocket/service/match.go: -------------------------------------------------------------------------------- 1 | package service 2 | -------------------------------------------------------------------------------- /entity/support.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Support interface{} 4 | -------------------------------------------------------------------------------- /entity/summon.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type Summon interface { 4 | } 5 | -------------------------------------------------------------------------------- /docs/_404.md: -------------------------------------------------------------------------------- 1 | # 你要找的页面走丢了 2 | 3 |
也有可能是作者创建了文件夹但是没有写东西
-------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | # go-plugin的插件文件 2 | *.so 3 | 4 | # 可能的配置文件 5 | *.json 6 | *.xml 7 | *.yml 8 | 9 | # 持久化文件 10 | *.psc 11 | *.psc.quit 12 | *.db 13 | -------------------------------------------------------------------------------- /model/adapter/adapter.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | type Adapter[Origin any, Dest any] interface { 4 | Convert(source Origin) (success bool, result Dest) 5 | } 6 | -------------------------------------------------------------------------------- /protocol/http/config/service.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type ServiceConfig struct { 4 | MaxRooms uint64 `json:"max_rooms" yaml:"max_rooms" xml:"max_rooms"` 5 | } 6 | -------------------------------------------------------------------------------- /entity/model/entity.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | // BaseEntity 实体的基本接口,包含被框架托管的必须方法 4 | type BaseEntity interface { 5 | TypeID() uint64 6 | InjectTypeID(id uint64) 7 | } 8 | -------------------------------------------------------------------------------- /enum/description.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | type DescriptionType byte 4 | 5 | const ( 6 | DescriptionTypePlainText DescriptionType = iota 7 | DescriptionTypeHtml 8 | ) 9 | -------------------------------------------------------------------------------- /docs/en/_404.md: -------------------------------------------------------------------------------- 1 | # The page you are looking for is missing 2 | 3 |
It is also possible that the author created a folder but did not write anything
-------------------------------------------------------------------------------- /enum/rule.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | type RuleType byte 4 | 5 | const ( 6 | RuleTypeNone RuleType = iota 7 | RuleTypeReactionCalculator 8 | RuleTypeVictorCalculator 9 | ) 10 | -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | - [pkg.go.dev](https://pkg.go.dev/github.com/sunist-c/genius-invokation-simulator-backend#section-readme) 2 | - Translations 3 | - [:cn: 中文](/) 4 | - [:uk: English](/en/) -------------------------------------------------------------------------------- /mod/definition/summon.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 4 | 5 | type Summon interface { 6 | model.BaseEntity 7 | } 8 | -------------------------------------------------------------------------------- /mod/definition/damage.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import "github.com/sunist-c/genius-invokation-simulator-backend/enum" 4 | 5 | type Damage interface { 6 | ElementType() enum.ElementType 7 | DamageAmount() uint 8 | } 9 | -------------------------------------------------------------------------------- /enum/modifier.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | type ModifierType byte 4 | 5 | const ( 6 | ModifierTypeNone ModifierType = iota 7 | ModifierTypeAttack 8 | ModifierTypeCharacter 9 | ModifierTypeCharge 10 | ModifierTypeCost 11 | ModifierTypeDefence 12 | ModifierTypeHeal 13 | ) 14 | -------------------------------------------------------------------------------- /mod/definition/context.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 4 | 5 | type Context interface { 6 | ActiveCharacter() model.Character 7 | BackgroundCharacters() []model.Character 8 | Self() model.Player 9 | Players() []model.Player 10 | } 11 | -------------------------------------------------------------------------------- /protocol/http/service/init.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/config" 5 | ) 6 | 7 | func InitServices(conf config.EngineConfig) { 8 | SetConfig(conf) 9 | initPlayerService() 10 | initLocalizeService() 11 | initCardDeckService() 12 | initRoomInfoService() 13 | } 14 | -------------------------------------------------------------------------------- /model/modifier/modifier.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | ) 6 | 7 | type Modifier[data any] interface { 8 | ID() uint64 9 | Type() enum.ModifierType 10 | Handler() func(ctx *Context[data]) 11 | Clone() Modifier[data] 12 | RoundReset() 13 | Effective() bool 14 | EffectLeft() uint 15 | } 16 | -------------------------------------------------------------------------------- /enum/equipment.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // EquipmentType 装备类型 4 | type EquipmentType byte 5 | 6 | const ( 7 | EquipmentNone EquipmentType = iota // EquipmentNone 无装备 8 | EquipmentWeapon // EquipmentWeapon 武器 9 | EquipmentArtifact // EquipmentArtifact 圣遗物 10 | EquipmentTalent // EquipmentTalent 天赋 11 | ) 12 | -------------------------------------------------------------------------------- /model/kv/map.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | // Pair key-value存储对的封装 4 | type Pair[key, value any] interface { 5 | Key() key 6 | Value() value 7 | SetKey(key) 8 | SetValue(value) 9 | } 10 | 11 | // Map key-value存储的封装 12 | type Map[key comparable, value any] interface { 13 | Exists(key) bool 14 | Get(key) value 15 | Set(key, value) 16 | Remove(key) 17 | Range(func(key, value) bool) 18 | } 19 | -------------------------------------------------------------------------------- /protocol/http/service/config.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/config" 5 | ) 6 | 7 | var ( 8 | middlewareConfig config.MiddlewareConfig 9 | serviceConfig config.ServiceConfig 10 | ) 11 | 12 | func SetConfig(conf config.EngineConfig) { 13 | middlewareConfig = conf.Middleware 14 | serviceConfig = conf.Service 15 | } 16 | -------------------------------------------------------------------------------- /util/logger.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | type logger struct{} 4 | 5 | func Trace(chan interface{}) {} 6 | 7 | func Info(chan interface{}) {} 8 | 9 | func Error(chan error) {} 10 | 11 | type TraceLogger struct{} 12 | 13 | type InfoLogger struct{} 14 | 15 | type ErrorHandler struct { 16 | errorChan chan error 17 | } 18 | 19 | func (e *ErrorHandler) Handle(err error) { 20 | e.errorChan <- err 21 | } 22 | -------------------------------------------------------------------------------- /entity/model/event.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/model/context" 6 | ) 7 | 8 | type Event interface { 9 | BaseEntity 10 | TriggerAt() enum.TriggerType 11 | CanTriggered(context.CallbackContext) bool 12 | NeedClear() bool 13 | Callback(*context.CallbackContext) 14 | } 15 | -------------------------------------------------------------------------------- /mod/definition/rule.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | ) 7 | 8 | type Rule interface { 9 | model.BaseEntity 10 | CopyFrom(source Rule, filter ...enum.RuleType) 11 | Implements(ruleType enum.RuleType) interface{} 12 | CheckImplements() (success bool) 13 | } 14 | -------------------------------------------------------------------------------- /enum/skill.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // SkillType 技能类型 4 | type SkillType byte 5 | 6 | const ( 7 | SkillPassive SkillType = iota // SkillPassive 被动技能 8 | SkillNormalAttack // SkillNormalAttack 普通攻击 9 | SkillElementalSkill // SkillElementalSkill 元素战技 10 | SkillElementalBurst // SkillElementalBurst 元素爆发 11 | SkillCooperative // SkillCooperative 协同攻击技能 12 | ) 13 | -------------------------------------------------------------------------------- /mod/definition/character.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | ) 7 | 8 | type Character interface { 9 | model.BaseEntity 10 | Name() string 11 | Affiliation() enum.Affiliation 12 | Vision() enum.ElementType 13 | Weapon() enum.WeaponType 14 | Skills() []Skill 15 | HP() uint 16 | MP() uint 17 | } 18 | -------------------------------------------------------------------------------- /enum/weapon.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // WeaponType 武器类型 4 | type WeaponType byte 5 | 6 | const ( 7 | WeaponSword WeaponType = iota // WeaponSword 单手剑 8 | WeaponClaymore // WeaponClaymore 双手剑 9 | WeaponBow // WeaponBow 弓 10 | WeaponCatalyst // WeaponCatalyst 法器 11 | WeaponPolearm // WeaponPolearm 长柄武器 12 | WeaponOthers // WeaponOthers 其他武器 13 | ) 14 | -------------------------------------------------------------------------------- /enum/stage.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // RoundStage 回合生命周期 4 | type RoundStage byte 5 | 6 | const ( 7 | RoundStageInitialized RoundStage = iota // RoundStageInitialized 初始化完成阶段,仅在游戏开始时进入 8 | RoundStageStart // RoundStageStart 回合开始阶段 9 | RoundStageRoll // RoundStageRoll 投掷阶段 10 | RoundStageBattle // RoundStageBattle 对战阶段 11 | RoundStageEnd // RoundStageEnd 回合结束阶段 12 | ) 13 | -------------------------------------------------------------------------------- /mod/definition/mod.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | type Mod interface { 4 | CharacterFactory 5 | SkillFactory 6 | EventFactory 7 | SummonFactory 8 | CardFactory 9 | RuleFactory 10 | LanguagePackFactory 11 | RegisterCharacter(character Character) 12 | RegisterSkill(skill Skill) 13 | RegisterEvent(event Event) 14 | RegisterSummon(summon Summon) 15 | RegisterCard(card Card) 16 | RegisterRule(rule Rule) 17 | AttachLanguagePack(languagePack LanguagePack) 18 | } 19 | -------------------------------------------------------------------------------- /protocol/http/message/localization.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "github.com/sunist-c/genius-invokation-simulator-backend/model/localization" 4 | 5 | type LocalizationQueryResponse struct { 6 | LanguagePack localization.MultipleLanguagePack `json:"language_pack"` 7 | } 8 | 9 | type TranslationRequest struct { 10 | Words []string `json:"words"` 11 | } 12 | 13 | type TranslationResponse struct { 14 | Translation map[string]string `json:"translation"` 15 | } 16 | -------------------------------------------------------------------------------- /mod/definition/event.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/model/context" 7 | ) 8 | 9 | type Event interface { 10 | model.BaseEntity 11 | TriggerAt() enum.TriggerType 12 | TriggeredNow(ctx context.CallbackContext) bool 13 | ClearNow() bool 14 | CallBack(callbackContext *context.CallbackContext) 15 | } 16 | -------------------------------------------------------------------------------- /exec/cli/main.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func Run() { 12 | for { 13 | if input, _, err := bufio.NewReader(os.Stdin).ReadLine(); err != nil { 14 | fmt.Printf("[cli.log] error occurred in cli.Run() %v\n", err) 15 | time.Sleep(time.Second) 16 | } else { 17 | if strings.Split(string(input), " ")[0] == "exit" { 18 | break 19 | } 20 | fmt.Printf("%+v\n", string(input)) 21 | } 22 | } 23 | } 24 | 25 | func Quit() {} 26 | -------------------------------------------------------------------------------- /exec/backend/main.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/config" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/service" 7 | ) 8 | 9 | func Run(port uint) { 10 | errChan := make(chan error) 11 | service.InitServices(config.GetConfig()) 12 | http.Serve(port, errChan) 13 | 14 | err := <-errChan 15 | panic(err) 16 | } 17 | 18 | func Quit() {} 19 | -------------------------------------------------------------------------------- /model/kv/pair.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | type pair[key, value any] struct { 4 | k key 5 | v value 6 | } 7 | 8 | func (p pair[key, value]) Key() key { 9 | return p.k 10 | } 11 | 12 | func (p pair[key, value]) Value() value { 13 | return p.v 14 | } 15 | 16 | func (p *pair[key, value]) SetKey(k key) { 17 | p.k = k 18 | } 19 | 20 | func (p *pair[key, value]) SetValue(v value) { 21 | p.v = v 22 | } 23 | 24 | func NewPair[key, value any](k key, v value) Pair[key, value] { 25 | return &pair[key, value]{ 26 | k: k, 27 | v: v, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /protocol/websocket/message/initialize.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import "github.com/sunist-c/genius-invokation-simulator-backend/enum" 4 | 5 | type MatchingMessage struct { 6 | UID uint64 7 | Characters []uint64 8 | CardDeck []uint64 9 | } 10 | 11 | type GameOptions struct { 12 | ReRollTime uint 13 | ElementAmount uint 14 | GetCards uint 15 | StaticElement map[enum.ElementType]uint 16 | RuleSet uint64 17 | } 18 | 19 | type InitializeMessage struct { 20 | Players []MatchingMessage 21 | Options GameOptions 22 | } 23 | -------------------------------------------------------------------------------- /enum/character.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // CharacterStatus 角色的状态 4 | type CharacterStatus byte 5 | 6 | const ( 7 | CharacterStatusReady CharacterStatus = iota // CharacterStatusReady 角色已就绪,无状态 8 | CharacterStatusActive // CharacterStatusActive 角色已激活,前台角色 9 | CharacterStatusBackground // CharacterStatusBackground 角色已激活,后台角色 10 | CharacterStatusDisabled // CharacterStatusDisabled 前台角色,但无法进行操作 11 | CharacterStatusDefeated // CharacterStatusDefeated 角色已被击败,不可操作 12 | ) 13 | -------------------------------------------------------------------------------- /mod/implement/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkTestNewCharacterWithOpts(b *testing.B) { 9 | SetDebugFlag(true) 10 | 11 | for i := 0; i < b.N; i++ { 12 | NewCharacterWithOpts( 13 | WithCharacterID(1), 14 | WithCharacterName("Ganyu"), 15 | WithCharacterAffiliation(enum.AffiliationLiyue), 16 | WithCharacterHP(10), 17 | WithCharacterMP(2), 18 | WithCharacterVision(enum.ElementCryo), 19 | WithCharacterWeapon(enum.WeaponBow), 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /mod/definition/factory.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | type CharacterFactory interface { 4 | ProduceCharacters() []Character 5 | } 6 | 7 | type SummonFactory interface { 8 | ProduceSummons() []Summon 9 | } 10 | 11 | type EventFactory interface { 12 | ProduceEvents() []Event 13 | } 14 | 15 | type SkillFactory interface { 16 | ProduceSkills() []Skill 17 | } 18 | 19 | type CardFactory interface { 20 | ProduceCards() []Card 21 | } 22 | 23 | type RuleFactory interface { 24 | ProduceRules() []Rule 25 | } 26 | 27 | type LanguagePackFactory interface { 28 | ProduceLanguagePacks() []LanguagePack 29 | } 30 | -------------------------------------------------------------------------------- /enum/language.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | type Language uint 4 | 5 | const ( 6 | ChineseSimplified Language = iota // ChineseSimplified 简体中文 7 | ChineseTraditional // ChineseTraditional 繁體中文 8 | English // English 9 | French // French Français 10 | German // German Deutsch 11 | Japanese // Japanese 日本語 12 | Korean // Korean 한국어 13 | Russian // Russian Русский язык 14 | Unknown // Unknown ያንሽቤ 15 | ) 16 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: gisb-test 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: build 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Initialize 9 | uses: actions/setup-go@v1 10 | with: 11 | go-version: 1.19 12 | id: go 13 | 14 | - name: Checkout 15 | uses: actions/checkout@v1 16 | 17 | - name: Build 18 | run: go mod tidy && go build . 19 | 20 | - name: UnitTest 21 | run: go mod tidy && go test -v ./... 22 | 23 | - name: BenchmarkTest 24 | run: go mod tidy && go test -bench=. -benchtime=1s ./... -run=^$ -------------------------------------------------------------------------------- /entity/model/skill.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/model/context" 6 | ) 7 | 8 | type Skill interface { 9 | BaseEntity 10 | Type() enum.SkillType 11 | } 12 | 13 | type AttackSkill interface { 14 | Skill 15 | Cost() Cost 16 | BaseDamage(targetCharacter, senderCharacter uint64, backgroundCharacters []uint64) *context.DamageContext 17 | } 18 | 19 | type CooperativeSkill interface { 20 | Skill 21 | BaseDamage() *context.DamageContext 22 | EffectLeft() uint 23 | Effective() bool 24 | } 25 | 26 | type PassiveSkill interface { 27 | } 28 | -------------------------------------------------------------------------------- /mod/definition/skill.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | ) 7 | 8 | type Skill interface { 9 | model.BaseEntity 10 | SkillType() enum.SkillType 11 | } 12 | 13 | type AttackSkill interface { 14 | Skill 15 | SkillCost() map[enum.ElementType]uint 16 | ActiveDamage(ctx Context) Damage 17 | BackgroundDamage(ctx Context) Damage 18 | } 19 | 20 | type CooperativeSkill interface { 21 | Skill 22 | ActiveDamage(ctx Context) Damage 23 | BackgroundDamage(ctx Context) Damage 24 | EffectLeft(ctx Context) uint 25 | Effective(ctx Context) bool 26 | } 27 | -------------------------------------------------------------------------------- /mod/definition/card.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | ) 7 | 8 | type Card interface { 9 | model.BaseEntity 10 | CardType() enum.CardType 11 | Cost() map[enum.ElementType]uint 12 | } 13 | 14 | type EventCard interface { 15 | Card 16 | Event() Event 17 | } 18 | 19 | type SupportCard interface { 20 | Card 21 | Support() Event 22 | } 23 | 24 | type EquipmentCard interface { 25 | Card 26 | EquipmentType() enum.EquipmentType 27 | Modify() Event 28 | } 29 | 30 | type WeaponCard interface { 31 | EquipmentCard 32 | WeaponType() enum.WeaponType 33 | } 34 | -------------------------------------------------------------------------------- /util/password.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "golang.org/x/crypto/scrypt" 4 | 5 | // EncodeRandomSalt 对任意类型的key进行编码生成一个长度为8的[]byte结果 6 | func EncodeRandomSalt[PK any](key PK) []byte { 7 | hashed := GenerateHash(key) 8 | salt := make([]byte, 8) 9 | for i := 0; i < 8; i++ { 10 | salt[i] = byte(hashed % 256) 11 | hashed = hashed >> 8 12 | } 13 | 14 | return salt 15 | } 16 | 17 | // EncodePassword 使用crypto/scrypt密钥库对密码进行单向加密 18 | func EncodePassword[PK any](original []byte, key PK) (success bool, result []byte) { 19 | if encrypted, err := scrypt.Key(original, EncodeRandomSalt(key), 32768, 8, 1, 64); err != nil { 20 | return false, []byte{} 21 | } else { 22 | return true, encrypted 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /enum/affiliation.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // Affiliation 角色的归属势力 4 | type Affiliation byte 5 | 6 | const ( 7 | AffiliationMondstadt Affiliation = iota // AffiliationMondstadt 蒙德人 8 | AffiliationLiyue // AffiliationLiyue 璃月人 9 | AffiliationInazuma // AffiliationInazuma 稻妻人 10 | AffiliationSumeru // AffiliationSumeru 须弥人 11 | AffiliationFatui Affiliation = 1<<4 + iota // AffiliationFatui 愚人众 12 | AffiliationHilichurl // AffiliationHilichurl 丘丘人 13 | AffiliationMonster // AffiliationMonster 魔物 14 | AffiliationUndefined Affiliation = 1<<8 - 1 // AffiliationUndefined 未定义 15 | ) 16 | -------------------------------------------------------------------------------- /enum/player.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // PlayerStatus 玩家的状态 4 | type PlayerStatus byte 5 | 6 | const ( 7 | PlayerStatusInitialized PlayerStatus = iota // PlayerStatusInitialized 玩家初始化完成 8 | PlayerStatusReady // PlayerStatusReady 玩家在回合开始阶段准备 9 | PlayerStatusWaiting // PlayerStatusWaiting 玩家在等待其他玩家操作 10 | PlayerStatusActing // PlayerStatusActing 玩家正在执行操作 11 | PlayerStatusDefeated // PlayerStatusDefeated 玩家被击败 12 | PlayerStatusViewing // PlayerStatusViewing 玩家正在观战 13 | PlayerStatusCharacterDefeated // PlayerStatusCharacterDefeated 玩家前台角色被击败,正在切换角色 14 | ) 15 | -------------------------------------------------------------------------------- /util/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "math/rand" 5 | "sync/atomic" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func BenchmarkTestGenerateID(b *testing.B) { 11 | mac := rand.Uint64() % (1 << 48) 12 | b.ResetTimer() 13 | for i := 0; i < b.N; i++ { 14 | GenerateUID(mac, time.Now()) 15 | } 16 | } 17 | 18 | func BenchmarkGenerateRealID(b *testing.B) { 19 | mac := rand.Uint64() % (1 << 48) 20 | uids := make([]uint64, 114514) 21 | for i := 0; i < 114514; i++ { 22 | uids[i] = GenerateUID(mac, time.Now()) 23 | } 24 | 25 | var index int32 = 0 26 | b.ResetTimer() 27 | for i := 0; i < b.N; i++ { 28 | if index >= 114514 { 29 | atomic.StoreInt32(&index, 0) 30 | } 31 | GenerateRealID(uids[index], uint16(index)) 32 | atomic.AddInt32(&index, 1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /enum/action.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // ActionType 玩家可进行的操作类型 4 | type ActionType byte 5 | 6 | const ( 7 | ActionNone ActionType = iota // ActionNone 没有操作 8 | ActionNormalAttack // ActionNormalAttack 进行普通攻击 9 | ActionElementalSkill // ActionElementalSkill 释放元素战技 10 | ActionElementalBurst // ActionElementalBurst 释放元素爆发 11 | ActionSwitch // ActionSwitch 切换在场角色 12 | ActionBurnCard // ActionBurnCard 将卡牌转换为元素 13 | ActionUseCard // ActionUseCard 使用卡牌 14 | ActionReRoll // ActionReRoll 重掷元素骰子 15 | ActionSkipRound // ActionSkipRound 结束回合 16 | ActionConcede // ActionConcede 弃权让步 17 | ) 18 | -------------------------------------------------------------------------------- /enum/trigger.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // TriggerType 触发器类型 4 | type TriggerType byte 5 | 6 | const ( 7 | AfterNone TriggerType = iota // AfterNone 不会被触发的触发器 8 | AfterAttack // AfterAttack 攻击结算后触发 9 | AfterBurnCard // AfterBurnCard 使用卡牌转换元素后触发 10 | AfterCharge // AfterCharge 充能后触发 11 | AfterDefence // AfterDefence 防御结算后触发 12 | AfterEatFood // AfterEatFood 使用食物后触发 13 | AfterHeal // AfterHeal 治疗后触发 14 | AfterReset // AfterReset 重置时触发 15 | AfterRoundStart // AfterRoundStart 回合开始后触发 16 | AfterRoundEnd // AfterRoundEnd 回合结束后触发 17 | AfterSwitch // AfterSwitch 切换角色后触发 18 | ) 19 | -------------------------------------------------------------------------------- /model/context/charge.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | type ChargeContext struct { 4 | charges map[uint64]int 5 | } 6 | 7 | // AddMagic 向指定目标添加能量 8 | func (c *ChargeContext) AddMagic(target uint64, amount uint) { 9 | c.charges[target] += int(amount) 10 | } 11 | 12 | // SubMagic 减少指定目标的能量 13 | func (c *ChargeContext) SubMagic(target uint64, amount uint) { 14 | c.charges[target] -= int(amount) 15 | } 16 | 17 | // Charge 返回ChargeContext携带的充能信息,只读 18 | func (c ChargeContext) Charge() map[uint64]int { 19 | result := map[uint64]int{} 20 | for target, amount := range c.charges { 21 | result[target] = amount 22 | } 23 | 24 | return result 25 | } 26 | 27 | // NewChargeContext 新建一个空的ChargeContext 28 | func NewChargeContext() *ChargeContext { 29 | return &ChargeContext{ 30 | charges: map[uint64]int{}, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /protocol/http/middleware/uuid.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/config" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/util" 7 | ) 8 | 9 | // GetUUID 获取context中携带的uuid信息,若没有则生成并写入一个uuid 10 | func GetUUID(ctx *gin.Context, conf config.MiddlewareConfig) (uuid string) { 11 | if result, gotten := ctx.Get(conf.UUIDKey); !gotten { 12 | uuid = util.GenerateUUID() 13 | ctx.Set(conf.UUIDKey, uuid) 14 | return uuid 15 | } else { 16 | return result.(string) 17 | } 18 | } 19 | 20 | // NewUUIDTagger 创建一个UUID标记器,将会往context中写入uuid 21 | func NewUUIDTagger(conf config.MiddlewareConfig) func(ctx *gin.Context) { 22 | return func(ctx *gin.Context) { 23 | ctx.Set(conf.UUIDKey, util.GenerateUUID()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /model/context/heal.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | type HealContext struct { 4 | heals map[uint64]uint 5 | } 6 | 7 | // AddHeal 向指定目标添加治疗量 8 | func (h *HealContext) AddHeal(target uint64, amount uint) { 9 | h.heals[target] += amount 10 | } 11 | 12 | // SubHeal 减少指定目标的治疗量 13 | func (h *HealContext) SubHeal(target uint64, amount uint) { 14 | if h.heals[target] > amount { 15 | h.heals[target] -= amount 16 | } else { 17 | h.heals[target] = 0 18 | } 19 | } 20 | 21 | // Heal 返回HealContext携带的治疗信息,只读 22 | func (h HealContext) Heal() map[uint64]uint { 23 | result := map[uint64]uint{} 24 | for target, amount := range h.heals { 25 | result[target] = amount 26 | } 27 | 28 | return result 29 | } 30 | 31 | // NewHealContext 新建一个空的HealContext 32 | func NewHealContext() *HealContext { 33 | return &HealContext{ 34 | heals: map[uint64]uint{}, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mod/implement/entity.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/util" 6 | ) 7 | 8 | type EntityImpl struct { 9 | typeID uint64 10 | } 11 | 12 | func (b *EntityImpl) TypeID() uint64 { 13 | return b.typeID 14 | } 15 | 16 | func (b *EntityImpl) InjectTypeID(id uint64) { 17 | b.typeID = util.GenerateRealID(ModID(), uint16(id)) 18 | } 19 | 20 | type EntityOptions func(entity *EntityImpl) 21 | 22 | func WithEntityID(id uint16) EntityOptions { 23 | return func(entity *EntityImpl) { 24 | entity.InjectTypeID(uint64(id)) 25 | } 26 | } 27 | 28 | func NewEntityWithOpts(options ...EntityOptions) model.BaseEntity { 29 | entity := &EntityImpl{} 30 | for _, o := range options { 31 | o(entity) 32 | } 33 | 34 | return entity 35 | } 36 | -------------------------------------------------------------------------------- /model/modifier/definition/modifier.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/model/context" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/model/modifier" 6 | ) 7 | 8 | type AttackModifiers = modifier.Chain[context.DamageContext] 9 | 10 | type DefenceModifiers = modifier.Chain[context.DamageContext] 11 | 12 | type HealModifiers = modifier.Chain[context.HealContext] 13 | 14 | type ChargeModifiers = modifier.Chain[context.ChargeContext] 15 | 16 | type CostModifiers = modifier.Chain[context.CostContext] 17 | 18 | type AttackModifier = modifier.Modifier[context.DamageContext] 19 | 20 | type DefenceModifier = modifier.Modifier[context.DamageContext] 21 | 22 | type HealModifier = modifier.Modifier[context.HealContext] 23 | 24 | type ChargeModifier = modifier.Modifier[context.ChargeContext] 25 | 26 | type CostModifier = modifier.Modifier[context.CostContext] 27 | -------------------------------------------------------------------------------- /model/context/cost.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import "github.com/sunist-c/genius-invokation-simulator-backend/enum" 4 | 5 | type CostContext struct { 6 | need map[enum.ElementType]int 7 | } 8 | 9 | // AddCost 增加指定ElementType的花费 10 | func (c *CostContext) AddCost(element enum.ElementType, amount uint) { 11 | c.need[element] += int(amount) 12 | } 13 | 14 | // SubCost 降低指定ElementType的花费 15 | func (c *CostContext) SubCost(element enum.ElementType, amount uint) { 16 | c.need[element] -= int(amount) 17 | } 18 | 19 | // Cost 返回CostContext携带的费用信息,只读 20 | func (c *CostContext) Cost() map[enum.ElementType]int { 21 | result := map[enum.ElementType]int{} 22 | for elementType, cost := range c.need { 23 | result[elementType] = cost 24 | } 25 | 26 | return result 27 | } 28 | 29 | // NewCostContext 新建一个空CostContext 30 | func NewCostContext() *CostContext { 31 | return &CostContext{ 32 | need: map[enum.ElementType]int{}, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /model/kv/simple.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | type simpleMap[key comparable, value any] struct { 4 | data map[key]value 5 | } 6 | 7 | func (m simpleMap[key, value]) Exists(k key) bool { 8 | _, ok := m.data[k] 9 | return ok 10 | } 11 | 12 | func (m simpleMap[key, value]) Get(k key) value { 13 | return m.data[k] 14 | } 15 | 16 | func (m *simpleMap[key, value]) Set(k key, data value) { 17 | m.data[k] = data 18 | } 19 | 20 | func (m *simpleMap[key, value]) Remove(k key) { 21 | delete(m.data, k) 22 | } 23 | 24 | func (m *simpleMap[key, value]) Range(f func(key, value) bool) { 25 | for k, v := range m.data { 26 | if !f(k, v) { 27 | return 28 | } 29 | } 30 | } 31 | 32 | func NewSimpleMap[value any]() Map[uint64, value] { 33 | return &simpleMap[uint64, value]{data: map[uint64]value{}} 34 | } 35 | 36 | func NewCommonMap[key comparable, value any]() Map[key, value] { 37 | return &simpleMap[key, value]{data: map[key]value{}} 38 | } 39 | -------------------------------------------------------------------------------- /enum/element.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // ElementType 元素类型,七元素与通用元素 4 | type ElementType byte 5 | 6 | const ( 7 | ElementCurrency ElementType = iota // ElementCurrency 通用元素,用于表示任意元素 8 | ElementAnemo // ElementAnemo 风元素 9 | ElementCryo // ElementCryo 冰元素 10 | ElementDendro // ElementDendro 草元素 11 | ElementElectro // ElementElectro 雷元素 12 | ElementGeo // ElementGeo 岩元素 13 | ElementHydro // ElementHydro 水元素 14 | ElementPyro // ElementPyro 火元素 15 | ElementNone ElementType = 1 << 4 // ElementNone 无元素,用于表示物理攻击 16 | ElementSame ElementType = 1 << 4 // ElementNone 无元素,用于表示相同元素 17 | ElementUndefined ElementType = 1<<8 - 1 // ElementUndefined 未定义元素,转化错误时传出此元素 18 | 19 | ElementStartIndex ElementType = 1 // ElementStartIndex 七元素起始 20 | ElementEndIndex ElementType = 7 // ElementEndIndex 七元素终止 21 | ) 22 | -------------------------------------------------------------------------------- /protocol/http/config/engine.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ( 4 | config = &EngineConfig{ 5 | Middleware: MiddlewareConfig{ 6 | UUIDKey: "uuid", 7 | IPTranceKey: "ip", 8 | InterdictorTraceKey: "interdicted", 9 | InterdictorBlockedTime: 600, 10 | InterdictorTriggerCount: 5, 11 | QPSLimitTime: 1, 12 | TokenIDKey: "gisb_token_id", 13 | TokenKey: "gisb_token", 14 | TokenRefreshTime: 7200, 15 | TokenRemainingTime: 1800, 16 | CookieDomain: "localhost", 17 | }, 18 | Service: ServiceConfig{ 19 | MaxRooms: 100, 20 | }, 21 | } 22 | ) 23 | 24 | type EngineConfig struct { 25 | Middleware MiddlewareConfig `json:"middleware" yaml:"middleware" xml:"middleware"` 26 | Service ServiceConfig `json:"service" yaml:"service" xml:"service"` 27 | } 28 | 29 | func SetConfig(conf EngineConfig) { 30 | config = &conf 31 | } 32 | 33 | func GetConfig() EngineConfig { 34 | return *config 35 | } 36 | -------------------------------------------------------------------------------- /mod/definition/language.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import "github.com/sunist-c/genius-invokation-simulator-backend/enum" 4 | 5 | type LanguagePack interface { 6 | Language() enum.Language 7 | ModDescription() ModDescription 8 | GetCharacterDescription(longID uint64) (has bool, description CharacterDescription) 9 | GetSkillDescription(longID uint64) (has bool, description SkillDescription) 10 | GetEventDescription(longID uint64) (has bool, description EventDescription) 11 | GetCardDescription(longID uint64) (has bool, description CardDescription) 12 | GetSummonDescription(longID uint64) (has bool, description SummonDescription) 13 | GetModifierDescription(longID uint64) (has bool, description ModifierDescription) 14 | AddCharacterDescription(description CharacterDescription) 15 | AddSkillDescription(description SkillDescription) 16 | AddEventDescription(description EventDescription) 17 | AddCardDescription(description CardDescription) 18 | AddModifierDescription(description ModifierDescription) 19 | } 20 | -------------------------------------------------------------------------------- /protocol/http/engine.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/config" 7 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/middleware" 8 | ) 9 | 10 | var nullHttpEngine *gin.Engine = nil 11 | 12 | var ( 13 | httpEngine *gin.Engine 14 | ) 15 | 16 | var ( 17 | EngineMiddlewares = []gin.HandlerFunc{ 18 | middleware.NewIPTracer(config.GetConfig().Middleware), // IP追踪器 19 | middleware.NewUUIDTagger(config.GetConfig().Middleware), // UUID标记器 20 | } 21 | ) 22 | 23 | func init() { 24 | httpEngine = gin.Default() 25 | } 26 | 27 | func RegisterServices(subPath string) *gin.RouterGroup { 28 | if httpEngine == nullHttpEngine { 29 | panic("nil http engine") 30 | } else { 31 | return httpEngine.Group(subPath) 32 | } 33 | } 34 | 35 | func Serve(port uint, errChan chan error) { 36 | if err := httpEngine.Run(fmt.Sprintf("0.0.0.0:%v", port)); err != nil { 37 | errChan <- err 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /protocol/http/middleware/qps.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/model/kv" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/config" 7 | "time" 8 | ) 9 | 10 | // NewQPSLimiter 生成一个QPS限制器,以IP来源为基础 11 | func NewQPSLimiter(conf config.MiddlewareConfig) func(ctx *gin.Context) { 12 | var limiter = kv.NewSyncMap[time.Time]() 13 | return func(ctx *gin.Context) { 14 | if success, ip := GetIPTrace(ctx, conf); !success { 15 | // 无法成功获取客户端IP,返回BadRequest 16 | ctx.AbortWithStatus(400) 17 | } else { 18 | if limiter.Exists(ip) { 19 | if t := limiter.Get(ip); !t.Add(time.Duration(conf.QPSLimitTime) * time.Second).Before(time.Now()) { 20 | // 请求过快,返回PreconditionFailed 21 | ctx.AbortWithStatus(412) 22 | } else { 23 | // 正常请求,更新访问时间 24 | limiter.Set(ip, time.Now()) 25 | } 26 | } else { 27 | // 之前未访问过,记录访问时间 28 | limiter.Set(ip, time.Now()) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [首页](/) 2 | * [开发文档](/development/) 3 | * [API文档](/development/api/) 4 | * [房间](/development/api/room.md) 5 | * [玩家](/development/api/player.md) 6 | * [卡组](/development/api/card_deck.md) 7 | * [本地化](/development/api/localization.md) 8 | * [框架文档](/development/framework/) 9 | * [战斗框架](/development/framework/battle.md) 10 | * [通信框架](/development/framework/communication.md) 11 | * [网络框架](/development/framework/network.md) 12 | * [MOD文档](/development/mod/) 13 | * [MOD基础](/development/mod/guide.md) 14 | * [制作卡牌](/development/mod/card.md) 15 | * [制作角色](/development/mod/character.md) 16 | * [制作技能](/development/mod/skill.md) 17 | * [制作事件](/development/mod/event.md) 18 | * [制作召唤物](/development/mod/summon.md) 19 | * [制作支援物](/development/mod/support.md) 20 | * [使用其他语言制作MOD](/development/mod/scripts.md) 21 | * [发布MOD](/development/mod/publish.md) 22 | * [使用文档](/guide/) 23 | * [部署到服务器](/guide/deploy.md) 24 | * [加载MOD](/guide/modify.md) 25 | * [连接到服务器](/guide/connection.md) 26 | -------------------------------------------------------------------------------- /mod/implement/damage.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 6 | ) 7 | 8 | type DamageImpl struct { 9 | elementType enum.ElementType 10 | damageAmount uint 11 | } 12 | 13 | func (impl *DamageImpl) ElementType() enum.ElementType { 14 | return impl.elementType 15 | } 16 | 17 | func (impl *DamageImpl) DamageAmount() uint { 18 | return impl.damageAmount 19 | } 20 | 21 | type DamageOptions func(option *DamageImpl) 22 | 23 | func WithDamageElementType(elementType enum.ElementType) DamageOptions { 24 | return func(option *DamageImpl) { 25 | option.elementType = elementType 26 | } 27 | } 28 | 29 | func WithDamageAmount(amount uint) DamageOptions { 30 | return func(option *DamageImpl) { 31 | option.damageAmount = amount 32 | } 33 | } 34 | 35 | func NewDamageWithOpts(options ...DamageOptions) definition.Damage { 36 | impl := &DamageImpl{} 37 | for _, option := range options { 38 | option(impl) 39 | } 40 | 41 | return impl 42 | } 43 | -------------------------------------------------------------------------------- /mod/adapter/rule.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 7 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/implement" 8 | "github.com/sunist-c/genius-invokation-simulator-backend/model/adapter" 9 | ) 10 | 11 | type RuleSetAdapter struct{} 12 | 13 | func (r RuleSetAdapter) Convert(source definition.Rule) (success bool, result model.RuleSet) { 14 | adapterLayer := model.RuleSet{} 15 | adapterLayer.ID = source.TypeID() 16 | _, adapterLayer.ReactionCalculator = implement.RuleConvert[model.ReactionCalculator](source.Implements(enum.RuleTypeReactionCalculator)) 17 | _, adapterLayer.VictorCalculator = implement.RuleConvert[model.VictorCalculator](source.Implements(enum.RuleTypeVictorCalculator)) 18 | 19 | return true, adapterLayer 20 | } 21 | 22 | func NewRuleSetAdapter() adapter.Adapter[definition.Rule, model.RuleSet] { 23 | return RuleSetAdapter{} 24 | } 25 | -------------------------------------------------------------------------------- /model/kv/sync.go: -------------------------------------------------------------------------------- 1 | package kv 2 | 3 | import "sync" 4 | 5 | type syncMap[value any] struct { 6 | mutex *sync.RWMutex 7 | data map[uint]value 8 | } 9 | 10 | func (s *syncMap[value]) Exists(key uint) bool { 11 | s.mutex.RLock() 12 | _, ok := s.data[key] 13 | s.mutex.RUnlock() 14 | return ok 15 | } 16 | 17 | func (s *syncMap[value]) Get(key uint) value { 18 | s.mutex.RLock() 19 | result := s.data[key] 20 | s.mutex.RUnlock() 21 | return result 22 | } 23 | 24 | func (s *syncMap[value]) Set(key uint, data value) { 25 | s.mutex.Lock() 26 | s.data[key] = data 27 | s.mutex.Unlock() 28 | } 29 | 30 | func (s *syncMap[value]) Remove(key uint) { 31 | s.mutex.Lock() 32 | delete(s.data, key) 33 | s.mutex.Unlock() 34 | } 35 | 36 | func (s *syncMap[value]) Range(f func(uint, value) bool) { 37 | s.mutex.RLock() 38 | for k, v := range s.data { 39 | if !f(k, v) { 40 | break 41 | } 42 | } 43 | s.mutex.RUnlock() 44 | } 45 | 46 | func NewSyncMap[value any]() Map[uint, value] { 47 | return &syncMap[value]{ 48 | mutex: &sync.RWMutex{}, 49 | data: map[uint]value{}, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /mod/implement/context.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 6 | ) 7 | 8 | type ContextImpl struct { 9 | activeCharacter model.Character 10 | backgroundCharacters []model.Character 11 | self model.Player 12 | players []model.Player 13 | } 14 | 15 | func (impl *ContextImpl) ActiveCharacter() model.Character { 16 | return impl.activeCharacter 17 | } 18 | 19 | func (impl *ContextImpl) BackgroundCharacters() []model.Character { 20 | return impl.backgroundCharacters 21 | } 22 | 23 | func (impl *ContextImpl) Self() model.Player { 24 | return impl.self 25 | } 26 | 27 | func (impl *ContextImpl) Players() []model.Player { 28 | return impl.players 29 | } 30 | 31 | func NewEmptyContext() definition.Context { 32 | return &ContextImpl{ 33 | activeCharacter: nil, 34 | backgroundCharacters: []model.Character{}, 35 | self: nil, 36 | players: []model.Player{}, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mod/definition/modifier.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/model/context" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/model/modifier" 7 | ) 8 | 9 | type BaseModifier interface { 10 | ModifierID() uint64 11 | ModifierType() enum.ModifierType 12 | RoundStartReset() 13 | Effective() bool 14 | EffectLeft() uint 15 | } 16 | 17 | type AttackModifier interface { 18 | BaseModifier 19 | ModifyAttack() func(ctx *modifier.Context[context.DamageContext]) 20 | Clone() AttackModifier 21 | } 22 | 23 | type CostModifier interface { 24 | BaseModifier 25 | ModifyCost() func(ctx *modifier.Context[context.CostContext]) 26 | Clone() CostModifier 27 | } 28 | 29 | type HealModifier interface { 30 | BaseModifier 31 | ModifyHeal() func(ctx *modifier.Context[context.HealContext]) 32 | Clone() HealModifier 33 | } 34 | 35 | type ChargeModifier interface { 36 | BaseModifier 37 | ModifyCharacter() func(ctx *modifier.Context[context.ChargeContext]) 38 | Clone() ChargeModifier 39 | } 40 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sunist 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /enum/card.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // CardType 卡牌类型,大类 4 | type CardType byte 5 | 6 | // CardSubType 卡牌类型,小类 7 | type CardSubType = CardType 8 | 9 | const ( 10 | CardEvent CardType = iota << 4 // CardEvent 事件卡 11 | CardSupport // CardSupport 支援卡 12 | CardEquipment // CardEquipment 装备卡 13 | CardUnknown CardType = 1 << 7 14 | ) 15 | 16 | // EventCardSubType 17 | const ( 18 | CardElementalResonance CardType = CardEvent + 1 + iota // CardElementalResonance 元素共鸣事件卡 19 | CardFood // CardFood 食物事件卡 20 | ) 21 | 22 | // SupportCardSubType 23 | const ( 24 | CardLocation CardType = CardSupport + 1 + iota // CardLocation 地点支援卡 25 | CardCompanion // CardCompanion 伙伴支援卡 26 | CardItem // CardItem 物品支援卡 27 | ) 28 | 29 | // EquipmentCardSubType 30 | const ( 31 | CardTalent CardType = CardEquipment + 1 + iota // CardTalent 天赋装备卡 32 | CardWeapon // CardWeapon 武器装备卡 33 | CardArtifact // CardArtifact 圣遗物装备卡 34 | ) 35 | -------------------------------------------------------------------------------- /mod/definition/description.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | import "github.com/sunist-c/genius-invokation-simulator-backend/enum" 4 | 5 | type Description interface { 6 | DescriptionType() enum.DescriptionType 7 | ShortID() uint16 8 | LongID() uint64 9 | } 10 | 11 | type CharacterDescription interface { 12 | Description 13 | CharacterName() string 14 | CharacterDescription() string 15 | CharacterTitle() string 16 | CharacterStory() string 17 | } 18 | 19 | type SkillDescription interface { 20 | Description 21 | SkillName() string 22 | SkillDescription() string 23 | } 24 | 25 | type EventDescription interface { 26 | Description 27 | EventName() string 28 | EventDescription() string 29 | } 30 | 31 | type CardDescription interface { 32 | Description 33 | CardName() string 34 | CardDescription() string 35 | } 36 | 37 | type SummonDescription interface { 38 | Description 39 | SummonName() string 40 | SummonDescription() string 41 | } 42 | 43 | type ModifierDescription interface { 44 | Description 45 | ModifierName() string 46 | ModifierDescription() string 47 | } 48 | 49 | type ModDescription interface { 50 | ModID() uint64 51 | ModName() string 52 | ModDescription() string 53 | } 54 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | kind: pipeline 2 | type: docker 3 | name: build 4 | 5 | clone: 6 | disable: false 7 | depth: 1 8 | 9 | steps: 10 | - name: golangci-lint 11 | image: golang:1.19 12 | pull: if-not-exist 13 | commands: 14 | - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1 15 | - go mod tidy 16 | - golangci-lint run -v -D unused --timeout=10m 17 | 18 | - name: go build 19 | image: golang:1.19 20 | pull: if-not-exist 21 | commands: 22 | - go mod tidy 23 | - go build . 24 | 25 | - name: go test 26 | image: golang:1.19 27 | pull: if-not-exist 28 | commands: 29 | - go mod tidy 30 | - go test -v ./... 31 | 32 | - name: go bench test 33 | image: golang:1.19 34 | pull: if-not-exist 35 | commands: 36 | - go mod tidy 37 | - go test -bench=. -benchtime=1s ./... -run=^$ 38 | 39 | - name: feishu notification 40 | image: serialt/drone-feishu-message 41 | pull: if-not-exists 42 | settings: 43 | token: 44 | from_secret: notification_token 45 | sha_link: true 46 | when: 47 | status: [ failure, success ] -------------------------------------------------------------------------------- /protocol/http/config/middleware.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type MiddlewareConfig struct { 4 | UUIDKey string `json:"uuid_key" yaml:"uuid_key" xml:"uuid_key"` 5 | IPTranceKey string `json:"ip_trace_key" yaml:"ip_trance_key" xml:"ip_trance_key"` 6 | InterdictorTraceKey string `json:"interdictor_trace_key" yaml:"interdictor_trace_key" xml:"interdictor_trace_key"` 7 | InterdictorBlockedTime uint `json:"interdictor_blocked_time" yaml:"interdictor_blocked_time" xml:"interdictor_blocked_time"` 8 | InterdictorTriggerCount uint `json:"interdictor_trigger_count" yaml:"interdictor_trigger_count" xml:"interdictor_trigger_count"` 9 | QPSLimitTime uint `json:"qps_limit_time" yaml:"qps_limit_time" xml:"qps_limit_time"` 10 | TokenIDKey string `json:"token_id_key" yaml:"token_id_key" xml:"token_id_key"` 11 | TokenKey string `json:"token_key" yaml:"token_key" xml:"token_key"` 12 | TokenRefreshTime uint `json:"token_refresh_time" yaml:"token_refresh_time" xml:"token_refresh_time"` 13 | TokenRemainingTime uint `json:"token_remaining_time" yaml:"token_remaining_time" xml:"token_remaining_time"` 14 | CookieDomain string `json:"cookie_domain" yaml:"cookie_domain" xml:"cookie_domain"` 15 | } 16 | -------------------------------------------------------------------------------- /docs/en/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [Home](/en/) 2 | * [Development](/en/development/) 3 | * [API](/en/development/api/) 4 | * [Room](/en/development/api/room.md) 5 | * [Player](/en/development/api/player.md) 6 | * [CardDeck](/en/development/api/card_deck.md) 7 | * [Localization](/en/development/api/localization.md) 8 | * [Framework](/en/development/framework/) 9 | * [Battle](/en/development/framework/battle.md) 10 | * [Communication](/en/development/framework/communication.md) 11 | * [Network](/en/development/framework/network.md) 12 | * [Mod](/en/development/mod/) 13 | * [Mod Basic](/en/development/mod/guide.md) 14 | * [Create Cards](/en/development/mod/card.md) 15 | * [Create Characters](/en/development/mod/character.md) 16 | * [Create Skills](/en/development/mod/skill.md) 17 | * [Create Events](/en/development/mod/event.md) 18 | * [Create Summons](/en/development/mod/summon.md) 19 | * [Create Supports](/en/development/mod/support.md) 20 | * [Use Other Coding Language](/en/development/mod/scripts.md) 21 | * [Publish Mod](/en/development/mod/publish.md) 22 | * [Guide](/en/guide/) 23 | * [Deploy to Server](/en/guide/deploy.md) 24 | * [Load Mods](/en/guide/modify.md) 25 | * [Connect to Sever](/en/guide/connection.md) -------------------------------------------------------------------------------- /protocol/websocket/config/handler.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | var ( 4 | websocketConfig = &WebsocketConfig{ 5 | HandshakeTimeout: 60, 6 | AllowCrossOrigin: true, 7 | WebsocketWriterBufferSize: 4096, 8 | WebsocketReaderBufferSize: 4096, 9 | ServerMessageBufferSize: 128, 10 | AllowOriginDomains: []string{""}, 11 | } 12 | ) 13 | 14 | type WebsocketConfig struct { 15 | HandshakeTimeout uint `json:"handshake_timeout" yaml:"handshake_timeout" xml:"handshake_timeout"` 16 | AllowCrossOrigin bool `json:"allow_cross_origin" yaml:"allow_cross_origin" xml:"allow_cross_origin"` 17 | WebsocketWriterBufferSize uint `json:"websocket_writer_buffer_size" yaml:"websocket_writer_buffer_size" xml:"websocket_writer_buffer_size"` 18 | WebsocketReaderBufferSize uint `json:"websocket_reader_buffer_size" yaml:"websocket_reader_buffer_size" xml:"websocket_reader_buffer_size"` 19 | ServerMessageBufferSize uint `json:"server_message_buffer_size" yaml:"server_message_buffer_size" xml:"server_message_buffer_size"` 20 | AllowOriginDomains []string `json:"allow_origin_domains" yaml:"allow_origin_domains" xml:"allow_origin_domains"` 21 | } 22 | 23 | func SetConfig(conf WebsocketConfig) { 24 | *websocketConfig = conf 25 | } 26 | 27 | func GetConfig() WebsocketConfig { 28 | return *websocketConfig 29 | } 30 | -------------------------------------------------------------------------------- /enum/reaction.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | // Reaction 反应类型 4 | type Reaction byte 5 | 6 | const ( 7 | ReactionNone Reaction = iota // ReactionNone 无反应 8 | ReactionMelt // ReactionMelt 融化 9 | ReactionVaporize // ReactionVaporize 蒸发 10 | ReactionOverloaded // ReactionOverloaded 超载 11 | ReactionSuperconduct // ReactionSuperconduct 超导 12 | ReactionFrozen // ReactionFrozen 冻结 13 | ReactionElectroCharged // ReactionElectroCharged 感电 14 | ReactionBurning // ReactionBurning 燃烧 15 | ReactionBloom // ReactionBloom 绽放 16 | ReactionQuicken // ReactionQuicken 激化 17 | ReactionCryoSwirl // ReactionCryoSwirl 冰扩散 18 | ReactionElectroSwirl // ReactionElectroSwirl 电扩散 19 | ReactionHydroSwirl // ReactionHydroSwirl 水扩散 20 | ReactionPyroSwirl // ReactionPyroSwirl 火扩散 21 | ReactionCryoCrystalize // ReactionCryoCrystalize 冰结晶 22 | ReactionElectroCrystalize // ReactionElectroCrystalize 雷结晶 23 | ReactionHydroCrystalize // ReactionHydroCrystalize 水结晶 24 | ReactionPyroCrystalize // ReactionPyroCrystalize 火结晶 25 | ) 26 | -------------------------------------------------------------------------------- /model/modifier/context.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | const abortIndex byte = 1 << 7 4 | 5 | type Context[data any] struct { 6 | index byte 7 | chain Chain[data] 8 | data *data 9 | extendMap map[string]any 10 | } 11 | 12 | // Next 让Context继续执行事件链 13 | func (c *Context[data]) Next() { 14 | c.index += 1 15 | for c.index <= c.chain.size { 16 | c.chain.index(c.index - 1).modifier.Handler()(c) 17 | c.index += 1 18 | } 19 | } 20 | 21 | // Abort 中断Context事件链的调用,但是在终止之前的调用仍会生效 22 | func (c *Context[data]) Abort() { 23 | c.index = abortIndex 24 | } 25 | 26 | // IsAborted 判断Context是否被某一handler中断了 27 | func (c Context[data]) IsAborted() bool { 28 | return c.index >= abortIndex 29 | } 30 | 31 | // Data 获取Context中的数据,只读 32 | func (c Context[data]) Data() *data { 33 | return c.data 34 | } 35 | 36 | // Get 在Context的extendMap中获取数据,不建议使用这种方法传递信息 37 | func (c Context[data]) Get(key string) (value any, ok bool) { 38 | value, ok = c.extendMap[key] 39 | return value, ok 40 | } 41 | 42 | // Set 在Context的extendMap中写入数据,不建议使用这种方法传递信息 43 | func (c *Context[data]) Set(key string, value any) { 44 | c.extendMap[key] = value 45 | } 46 | 47 | // newContext 使用给定的data和handlers生成Context 48 | func newContext[data any](ctx *data, handlers Chain[data]) *Context[data] { 49 | return &Context[data]{ 50 | index: 0, 51 | chain: handlers, 52 | data: ctx, 53 | extendMap: map[string]any{}, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /protocol/http/message/carddeck.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | type UploadCardDeckRequest struct { 4 | Owner uint64 `json:"owner"` 5 | RequiredPackage []string `json:"required_package"` 6 | Cards []uint64 `json:"cards"` 7 | Characters []uint64 `json:"characters"` 8 | } 9 | 10 | type UploadCardDeckResponse struct { 11 | ID uint64 `json:"id"` 12 | Owner uint64 `json:"owner"` 13 | RequiredPackage []string `json:"required_package"` 14 | Cards []uint64 `json:"cards"` 15 | Characters []uint64 `json:"characters"` 16 | } 17 | 18 | type UpdateCardDeckRequest struct { 19 | Owner uint64 `json:"owner"` 20 | RequiredPackage []string `json:"required_package"` 21 | Cards []uint64 `json:"cards"` 22 | Characters []uint64 `json:"characters"` 23 | } 24 | 25 | type UpdateCardDeckResponse struct { 26 | ID uint64 `json:"id"` 27 | Owner uint64 `json:"owner"` 28 | RequiredPackage []string `json:"required_package"` 29 | Cards []uint64 `json:"cards"` 30 | Characters []uint64 `json:"characters"` 31 | } 32 | 33 | type QueryCardDeckResponse struct { 34 | ID uint64 `json:"id"` 35 | Owner uint64 `json:"owner"` 36 | RequiredPackage []string `json:"required_package"` 37 | Cards []uint64 `json:"cards"` 38 | Characters []uint64 `json:"characters"` 39 | } 40 | -------------------------------------------------------------------------------- /protocol/http/message/player.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | type CardDeck struct { 4 | ID uint64 `json:"id"` 5 | OwnerUID uint64 `json:"owner_uid"` 6 | RequiredPackages []string `json:"required_packages"` 7 | Cards []uint64 `json:"cards"` 8 | Characters []uint64 `json:"characters"` 9 | } 10 | 11 | type LoginResponse struct { 12 | PlayerUID uint64 `json:"player_uid"` 13 | Success bool `json:"success"` 14 | PlayerNickName string `json:"player_nick_name"` 15 | PlayerCardDecks []CardDeck `json:"player_card_decks"` 16 | } 17 | 18 | type LoginRequest struct { 19 | PlayerUID uint64 `json:"player_uid"` 20 | Password string `json:"password"` 21 | } 22 | 23 | type RegisterRequest struct { 24 | NickName string `json:"nick_name"` 25 | Password string `json:"password"` 26 | } 27 | 28 | type RegisterResponse struct { 29 | PlayerUID uint64 `json:"player_uid"` 30 | PlayerNickName string `json:"player_nick_name"` 31 | } 32 | 33 | type UpdatePasswordRequest struct { 34 | OriginalPassword string `json:"original_password"` 35 | NewPassword string `json:"new_password"` 36 | } 37 | 38 | type UpdateNickNameRequest struct { 39 | Password string `json:"password"` 40 | NewNickName string `json:"new_nick_name"` 41 | } 42 | 43 | type DestroyPlayerRequest struct { 44 | Password string `json:"password"` 45 | Confirm bool `json:"confirm"` 46 | } 47 | 48 | type DestroyPlayerResponse struct { 49 | Success bool `json:"success"` 50 | } 51 | -------------------------------------------------------------------------------- /entity/model/interface.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/sunist-c/genius-invokation-simulator-backend/enum" 4 | 5 | // Player 供外部包调用的玩家接口 6 | type Player interface { 7 | GetUID() (uid uint64) 8 | GetCost() (cost map[enum.ElementType]uint) 9 | GetCards() (cards []uint64) 10 | GetSummons() (summons []uint64) 11 | GetSupports() (supports []uint64) 12 | CardDeckRemain() (remain uint) 13 | GetActiveCharacter() (character uint64) 14 | GetBackgroundCharacters() (characters []uint64) 15 | GetCharacter(character uint64) (has bool, entity Character) 16 | GetStatus() (status enum.PlayerStatus) 17 | GetGlobalModifiers(modifierType enum.ModifierType) (modifiers []uint64) 18 | GetCooperativeSkills(trigger enum.TriggerType) (skills []uint64) 19 | GetEvents(trigger enum.TriggerType) (events []uint64) 20 | } 21 | 22 | // Character 供外部包调用的角色接口 23 | type Character interface { 24 | GetID() (id uint64) 25 | GetOwner() (owner uint64) 26 | GetAffiliation() (affiliation enum.Affiliation) 27 | GetVision() (element enum.ElementType) 28 | GetWeaponType() (weaponType enum.WeaponType) 29 | GetSkills() (skills []uint64) 30 | GetHP() (hp uint) 31 | GetMaxHP() (maxHP uint) 32 | GetMP() (mp uint) 33 | GetMaxMP() (maxMP uint) 34 | GetEquipment(equipmentType enum.EquipmentType) (equipped bool, equipment uint64) 35 | GetSatiety() (satiety bool) 36 | GetAttachedElements() (elements []enum.ElementType) 37 | GetStatus() (status enum.CharacterStatus) 38 | GetLocalModifiers(modifierType enum.ModifierType) (modifiers []uint64) 39 | } 40 | -------------------------------------------------------------------------------- /persistence/memcache.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // memoryCache 内存缓存的实现,简单的并发安全map封装,同kv.syncMap实现 8 | type memoryCache[PK comparable, T any] struct { 9 | mu sync.RWMutex 10 | cache map[PK]T 11 | } 12 | 13 | func (m *memoryCache[PK, T]) QueryByID(id PK) (has bool, result T) { 14 | m.mu.RLock() 15 | defer m.mu.RUnlock() 16 | if entity, exist := m.cache[id]; !exist { 17 | return false, result 18 | } else { 19 | return true, entity 20 | } 21 | } 22 | 23 | func (m *memoryCache[PK, T]) UpdateByID(id PK, newEntity T) (success bool) { 24 | m.mu.RLock() 25 | defer m.mu.RUnlock() 26 | if _, exist := m.cache[id]; !exist { 27 | return false 28 | } else { 29 | m.cache[id] = newEntity 30 | return true 31 | } 32 | } 33 | 34 | func (m *memoryCache[PK, T]) InsertOne(id PK, entity T) (success bool) { 35 | m.mu.RLock() 36 | _, exist := m.cache[id] 37 | m.mu.RUnlock() 38 | 39 | if !exist { 40 | m.mu.Lock() 41 | m.cache[id] = entity 42 | m.mu.Unlock() 43 | return true 44 | } else { 45 | return false 46 | } 47 | } 48 | 49 | func (m *memoryCache[PK, T]) DeleteOne(id PK) (success bool) { 50 | m.mu.RLock() 51 | _, exist := m.cache[id] 52 | m.mu.RUnlock() 53 | 54 | if !exist { 55 | return false 56 | } else { 57 | m.mu.Lock() 58 | delete(m.cache, id) 59 | m.mu.Unlock() 60 | return true 61 | } 62 | } 63 | 64 | func newMemoryCache[PK comparable, T any]() MemoryCache[PK, T] { 65 | return &memoryCache[PK, T]{ 66 | mu: sync.RWMutex{}, 67 | cache: map[PK]T{}, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /model/localization/language.go: -------------------------------------------------------------------------------- 1 | package localization 2 | 3 | import "github.com/sunist-c/genius-invokation-simulator-backend/enum" 4 | 5 | type MultipleLanguagePack struct { 6 | Languages map[enum.Language]map[string]string `json:"languages"` 7 | SupportedLanguages []enum.Language `json:"supported_languages"` 8 | } 9 | 10 | // LanguagePack 语言包接口 11 | type LanguagePack interface { 12 | Translate(original string, destLanguage enum.Language) (ok bool, result string) 13 | Pack() MultipleLanguagePack 14 | } 15 | 16 | // languagePack 语言包 17 | type languagePack struct { 18 | translations map[enum.Language]map[string]string 19 | languages []enum.Language 20 | } 21 | 22 | func (l languagePack) Translate(original string, destLanguage enum.Language) (ok bool, result string) { 23 | if pack, hasLanguage := l.translations[destLanguage]; !hasLanguage { 24 | return false, "" 25 | } else { 26 | result, ok = pack[original] 27 | return ok, result 28 | } 29 | } 30 | 31 | func (l languagePack) Pack() MultipleLanguagePack { 32 | pack := MultipleLanguagePack{ 33 | Languages: l.translations, 34 | SupportedLanguages: l.languages, 35 | } 36 | 37 | return pack 38 | } 39 | 40 | func NewLanguagePack(translations map[enum.Language]map[string]string) LanguagePack { 41 | pack := languagePack{ 42 | translations: translations, 43 | languages: []enum.Language{}, 44 | } 45 | 46 | for language := range translations { 47 | pack.languages = append(pack.languages, language) 48 | } 49 | 50 | return pack 51 | } 52 | -------------------------------------------------------------------------------- /protocol/http/middleware/trace.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/config" 7 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/util" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func GetIPTrace(ctx *gin.Context, conf config.MiddlewareConfig) (has bool, ip uint) { 13 | if result, gotten := ctx.Get(conf.IPTranceKey); !gotten { 14 | return false, 0 15 | } else if ipResult, ok := result.(uint); !ok { 16 | return false, 0 17 | } else { 18 | return true, ipResult 19 | } 20 | } 21 | 22 | func ConvertIPToUint(ip string) (success bool, result uint) { 23 | splits := strings.Split(ip, ".") 24 | if len(splits) != 4 { 25 | return false, 0 26 | } else { 27 | for _, s := range splits { 28 | if intResult, err := strconv.Atoi(s); err != nil { 29 | return false, 0 30 | } else if intResult > 256 || intResult < 0 { 31 | return false, 0 32 | } else { 33 | result = result<<8 + uint(intResult) 34 | } 35 | } 36 | return true, result 37 | } 38 | } 39 | 40 | func ConvertUintToIP(ip uint) (result string) { 41 | bytes := make([]byte, 4) 42 | for i := 0; i < 4; i++ { 43 | bytes[i] = byte(ip % 256) 44 | ip = ip >> 8 45 | } 46 | return fmt.Sprintf("%d.%d.%d.%d", bytes[0], bytes[1], bytes[2], bytes[3]) 47 | } 48 | 49 | func NewIPTracer(conf config.MiddlewareConfig) func(ctx *gin.Context) { 50 | return func(ctx *gin.Context) { 51 | if success, ip := ConvertIPToUint(util.GetClientIP(ctx)); !success { 52 | // 无法成功获取客户端IP,返回BadRequest 53 | ctx.AbortWithStatus(400) 54 | } else { 55 | ctx.Set(conf.IPTranceKey, ip) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /util/os.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type SystemBits byte 11 | 12 | const ( 13 | BitsUnknown SystemBits = 0 14 | Bits32 SystemBits = 32 15 | Bits64 SystemBits = 64 16 | ) 17 | 18 | var ( 19 | zeroTimeStamp = new(time.Time) 20 | ) 21 | 22 | func init() { 23 | // 初始化“零点”,为本项目的创建时间 24 | utc8timeZone := time.FixedZone("UTC+8", 8*60*60) 25 | *zeroTimeStamp = time.Date(2022, 12, 9, 23, 35, 0, 0, utc8timeZone) 26 | } 27 | 28 | // GetZeroTimeStamp 获取系统的零时 29 | func GetZeroTimeStamp() time.Time { 30 | return *zeroTimeStamp 31 | } 32 | 33 | // GetSystemBits 获取操作系统的位数,结果为SystemBits 34 | func GetSystemBits() SystemBits { 35 | bit := 32 << ((^uint(0)) >> 63) 36 | 37 | switch bit { 38 | case 32: 39 | return Bits32 40 | case 64: 41 | return Bits64 42 | default: 43 | return BitsUnknown 44 | } 45 | } 46 | 47 | // GetMacAddresses 使用net包获取本机mac地址 48 | func GetMacAddresses() (macAddr []net.Interface, err error) { 49 | return net.Interfaces() 50 | } 51 | 52 | // GetUintMacAddress 从net.Interface中获取一个uint64类型的地址,长度为48位 53 | func GetUintMacAddress(mac net.Interface) (addr uint64, err error) { 54 | macAddrArr := strings.Split(mac.HardwareAddr.String(), ":") 55 | macAddr := strings.Join(macAddrArr, "") 56 | 57 | // mac地址为空,考虑到权限问题,设置mac地址为随机变量 58 | if macAddr == "" { 59 | macPartBits := uint64(1<<48) - 1 60 | addr = GenerateHashWithOpts[net.Interface, uint64](mac) 61 | return addr & macPartBits, nil 62 | } 63 | 64 | // mac地址非空,则正常计算mac的48位数值 65 | if addr, err = strconv.ParseUint(macAddr, 16, 64); err != nil { 66 | return 0, err 67 | } else { 68 | macPartBits := uint64(1<<48) - 1 69 | return addr & macPartBits, nil 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /mod/adapter/event.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | definition "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 7 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/implement" 8 | "github.com/sunist-c/genius-invokation-simulator-backend/model/adapter" 9 | "github.com/sunist-c/genius-invokation-simulator-backend/model/context" 10 | ) 11 | 12 | type eventAdapterLayer struct { 13 | implement.EntityImpl 14 | triggerAt enum.TriggerType 15 | canTriggered func(callbackContext context.CallbackContext) bool 16 | needClear func() bool 17 | callback func(context *context.CallbackContext) 18 | } 19 | 20 | func (e *eventAdapterLayer) TriggerAt() enum.TriggerType { 21 | return e.triggerAt 22 | } 23 | 24 | func (e *eventAdapterLayer) CanTriggered(context context.CallbackContext) bool { 25 | return e.canTriggered(context) 26 | } 27 | 28 | func (e *eventAdapterLayer) NeedClear() bool { 29 | return e.needClear() 30 | } 31 | 32 | func (e *eventAdapterLayer) Callback(context *context.CallbackContext) { 33 | e.callback(context) 34 | } 35 | 36 | type EventAdapter struct{} 37 | 38 | func (e EventAdapter) Convert(source definition.Event) (success bool, result model.Event) { 39 | adapterLayer := &eventAdapterLayer{ 40 | EntityImpl: implement.EntityImpl{}, 41 | triggerAt: source.TriggerAt(), 42 | canTriggered: source.TriggeredNow, 43 | needClear: source.ClearNow, 44 | callback: source.CallBack, 45 | } 46 | 47 | return true, adapterLayer 48 | } 49 | 50 | func NewEventAdapter() adapter.Adapter[definition.Event, model.Event] { 51 | return EventAdapter{} 52 | } 53 | -------------------------------------------------------------------------------- /mod/adapter/single.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/model/adapter" 7 | ) 8 | 9 | var ( 10 | eventAdapter = NewEventAdapter() 11 | 12 | cardAdapter = NewCardAdapter() 13 | eventCardAdapter = NewEventCardAdapter() 14 | supportCardAdapter = NewSupportCardAdapter() 15 | equipmentCardAdapter = NewEquipmentCardAdapter() 16 | weaponCardAdapter = NewWeaponCardAdapter() 17 | ruleSetAdapter = NewRuleSetAdapter() 18 | 19 | attackModifierAdapter = NewAttackModifierAdapter() 20 | costModifierAdapter = NewCostModifierAdapter() 21 | healModifierAdapter = NewHealModifierAdapter() 22 | chargeModifierAdapter = NewChargeModifierAdapter() 23 | ) 24 | 25 | func GetEventAdapter() adapter.Adapter[definition.Event, model.Event] { 26 | return eventAdapter 27 | } 28 | 29 | func GetCardAdapter() adapter.Adapter[definition.Card, model.Card] { 30 | return cardAdapter 31 | } 32 | 33 | func GetEventCardAdapter() adapter.Adapter[definition.EventCard, model.EventCard] { 34 | return eventCardAdapter 35 | } 36 | 37 | func GetSupportCardAdapter() adapter.Adapter[definition.SupportCard, model.SupportCard] { 38 | return supportCardAdapter 39 | } 40 | 41 | func GetEquipmentCardAdapter() adapter.Adapter[definition.EquipmentCard, model.EquipmentCard] { 42 | return equipmentCardAdapter 43 | } 44 | 45 | func GetWeaponCardAdapter() adapter.Adapter[definition.WeaponCard, model.WeaponCard] { 46 | return weaponCardAdapter 47 | } 48 | 49 | func GetRuleSetAdapter() adapter.Adapter[definition.Rule, model.RuleSet] { 50 | return ruleSetAdapter 51 | } 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sunist-c/genius-invokation-simulator-backend 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/bytedance/sonic v1.9.1 // indirect 7 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 8 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 9 | github.com/gin-contrib/sse v0.1.0 // indirect 10 | github.com/gin-gonic/gin v1.9.1 // indirect 11 | github.com/go-playground/locales v0.14.1 // indirect 12 | github.com/go-playground/universal-translator v0.18.1 // indirect 13 | github.com/go-playground/validator/v10 v10.14.0 // indirect 14 | github.com/go-xorm/xorm v0.7.9 // indirect 15 | github.com/goccy/go-json v0.10.2 // indirect 16 | github.com/gorilla/websocket v1.5.0 // indirect 17 | github.com/json-iterator/go v1.1.12 // indirect 18 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 19 | github.com/leodido/go-urn v1.2.4 // indirect 20 | github.com/mattn/go-isatty v0.0.19 // indirect 21 | github.com/mattn/go-sqlite3 v1.14.16 // indirect 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 23 | github.com/modern-go/reflect2 v1.0.2 // indirect 24 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 25 | github.com/satori/go.uuid v1.2.0 // indirect 26 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 27 | github.com/ugorji/go/codec v1.2.11 // indirect 28 | golang.org/x/arch v0.3.0 // indirect 29 | golang.org/x/crypto v0.9.0 // indirect 30 | golang.org/x/net v0.10.0 // indirect 31 | golang.org/x/sys v0.8.0 // indirect 32 | golang.org/x/text v0.9.0 // indirect 33 | google.golang.org/protobuf v1.30.0 // indirect 34 | gopkg.in/yaml.v2 v2.4.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | xorm.io/builder v0.3.12 // indirect 37 | xorm.io/core v0.7.3 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /util/unit_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestUIDGenerator(t *testing.T) { 11 | t.Run("TestUIDGenerator-1", func(t *testing.T) { 12 | timeStamp := GetZeroTimeStamp().Add(time.Second) 13 | macString := "b4:0e:de:fc:1b:ca" 14 | macAddrArr := strings.Split(macString, ":") 15 | macAddr := strings.Join(macAddrArr, "") 16 | mac, _ := strconv.ParseUint(macAddr, 16, 64) 17 | uid := GenerateUID(mac, timeStamp) 18 | if uid != 109951162778600 { 19 | t.Errorf("unexpected uid: %d, expected 109951162778600", uid) 20 | } 21 | }) 22 | t.Run("TestUIDGenerator-2", func(t *testing.T) { 23 | timeStamp := GetZeroTimeStamp().Add(time.Second) 24 | macString := "b40edefc1bca" 25 | macAddrArr := strings.Split(macString, ":") 26 | macAddr := strings.Join(macAddrArr, "") 27 | mac, _ := strconv.ParseUint(macAddr, 16, 64) 28 | uid := GenerateUID(mac, timeStamp) 29 | if uid != 109951162778600 { 30 | t.Errorf("unexpected uid: %d, expected 109951162778600", uid) 31 | } 32 | }) 33 | t.Run("TestUIDGenerator-3", func(t *testing.T) { 34 | timeStamp := GetZeroTimeStamp().Add(time.Second) 35 | macString := "b40edefc1b:ca" 36 | macAddrArr := strings.Split(macString, ":") 37 | macAddr := strings.Join(macAddrArr, "") 38 | mac, _ := strconv.ParseUint(macAddr, 16, 64) 39 | uid := GenerateUID(mac, timeStamp) 40 | if uid != 109951162778600 { 41 | t.Errorf("unexpected uid: %d, expected 109951162778600", uid) 42 | } 43 | }) 44 | } 45 | 46 | func TestRealIDGenerator(t *testing.T) { 47 | t.Run("TestRealIDGenerator", func(t *testing.T) { 48 | realID := GenerateRealID(109951162778600, 1) 49 | if realID != 7205759403858329601 { 50 | t.Errorf("unexpected real-id: %v, expected 7205759403858329601", realID) 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /entity/model/card.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | ) 6 | 7 | type Card interface { 8 | BaseEntity 9 | Type() enum.CardType 10 | Cost() map[enum.ElementType]uint 11 | } 12 | 13 | type EventCard interface { 14 | Card 15 | Event() (event Event) 16 | } 17 | 18 | type SupportCard interface { 19 | Card 20 | Support() (event Event) 21 | } 22 | 23 | type EquipmentCard interface { 24 | Card 25 | EquipmentType() enum.EquipmentType 26 | Modify() (event Event) 27 | } 28 | 29 | type WeaponCard interface { 30 | EquipmentCard 31 | WeaponType() enum.WeaponType 32 | } 33 | 34 | func ConvertToEventCard(original Card) (success bool, result EventCard) { 35 | cardType := original.Type() 36 | if cardType != enum.CardEvent && cardType != enum.CardElementalResonance && cardType != enum.CardFood { 37 | return false, result 38 | } else if convertedCard, convertSuccess := original.(EventCard); !convertSuccess { 39 | return false, result 40 | } else { 41 | return true, convertedCard 42 | } 43 | } 44 | 45 | func ConvertToSupportCard(original Card) (success bool, result SupportCard) { 46 | cardType := original.Type() 47 | if cardType != enum.CardSupport && cardType != enum.CardCompanion && cardType != enum.CardLocation && cardType != enum.CardItem { 48 | return false, result 49 | } else if convertedCard, convertSuccess := original.(SupportCard); !convertSuccess { 50 | return false, result 51 | } else { 52 | return true, convertedCard 53 | } 54 | } 55 | 56 | func ConvertToEquipmentCard(original Card) (success bool, result EquipmentCard) { 57 | cardType := original.Type() 58 | if cardType != enum.CardEquipment && cardType != enum.CardTalent && cardType != enum.CardWeapon && cardType != enum.CardArtifact { 59 | return false, result 60 | } else if convertedCard, convertSuccess := original.(EquipmentCard); !convertSuccess { 61 | return false, result 62 | } else { 63 | return true, convertedCard 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /protocol/http/middleware/interdictor.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/model/kv" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/config" 7 | "time" 8 | ) 9 | 10 | func Interdict(ctx *gin.Context, conf config.MiddlewareConfig) { 11 | ctx.Set(conf.InterdictorTraceKey, true) 12 | } 13 | 14 | func NewInterdictor(conf config.MiddlewareConfig) func(ctx *gin.Context) { 15 | limiter := kv.NewSyncMap[kv.Pair[uint, time.Time]]() 16 | return func(ctx *gin.Context) { 17 | success, ip := GetIPTrace(ctx, conf) 18 | if !success { 19 | // 无法成功获取客户端IP,返回BadRequest 20 | ctx.AbortWithStatus(400) 21 | return 22 | } 23 | 24 | if limiter.Exists(ip) { 25 | if pair := limiter.Get(ip); pair.Key() > conf.InterdictorTriggerCount { 26 | if blockedTime := pair.Value(); !blockedTime.IsZero() && blockedTime.Add(time.Second*time.Duration(conf.InterdictorBlockedTime)).After(time.Now()) { 27 | // 失败次数过多且未到解封时间,返回PreconditionFailed 28 | ctx.AbortWithStatus(412) 29 | return 30 | } else { 31 | // 已到解封时间,重置封禁状态 32 | limiter.Remove(ip) 33 | } 34 | } 35 | } 36 | 37 | ctx.Next() 38 | 39 | if f, exist := ctx.Get(conf.InterdictorTraceKey); exist { 40 | if result, ok := f.(bool); ok && result { 41 | if limiter.Exists(ip) { 42 | // 存在标记且被封禁器记录了 43 | pair := limiter.Get(ip) 44 | pair.SetKey(pair.Key() + 1) 45 | 46 | if pair.Key() >= conf.InterdictorTriggerCount { 47 | // 达到封禁标准,封禁 48 | pair.SetValue(time.Now()) 49 | } 50 | 51 | limiter.Set(ip, pair) 52 | } else { 53 | // 存在标记但没被封禁器记录,增加记录 54 | limiter.Set(ip, kv.NewPair(uint(1), time.Time{})) 55 | } 56 | } else if limiter.Exists(ip) { 57 | // 标记为假但以前被封禁器记录过,重置状态 58 | limiter.Remove(ip) 59 | } 60 | } else if limiter.Exists(ip) { 61 | // 没有标记但以前被封禁器记录过,重置状态 62 | limiter.Remove(ip) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /persistence/adapter.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/model/adapter" 6 | ) 7 | 8 | type cacheableImpl struct { 9 | id uint64 10 | } 11 | 12 | func (c cacheableImpl) ID() uint64 { 13 | return c.id 14 | } 15 | 16 | func newCacheableImpl(id uint64) cacheableImpl { 17 | return cacheableImpl{id: id} 18 | } 19 | 20 | type CardPersistenceAdapter struct{} 21 | 22 | func (c CardPersistenceAdapter) Convert(source model.Card) (success bool, result Card) { 23 | return true, Card{ 24 | Cacheable: newCacheableImpl(source.TypeID()), 25 | Card: source, 26 | } 27 | } 28 | 29 | func NewCardPersistenceAdapter() adapter.Adapter[model.Card, Card] { 30 | return CardPersistenceAdapter{} 31 | } 32 | 33 | type SkillPersistenceAdapter struct{} 34 | 35 | func (s SkillPersistenceAdapter) Convert(source model.Skill) (success bool, result Skill) { 36 | return true, Skill{ 37 | Cacheable: newCacheableImpl(source.TypeID()), 38 | Skill: source, 39 | } 40 | } 41 | 42 | func NewSkillPersistenceAdapter() adapter.Adapter[model.Skill, Skill] { 43 | return SkillPersistenceAdapter{} 44 | } 45 | 46 | type EventPersistenceAdapter struct{} 47 | 48 | func (e EventPersistenceAdapter) Convert(source model.Event) (success bool, result Event) { 49 | return true, Event{ 50 | Cacheable: newCacheableImpl(source.TypeID()), 51 | Event: source, 52 | } 53 | } 54 | 55 | func NewEventPersistenceAdapter() adapter.Adapter[model.Event, Event] { 56 | return EventPersistenceAdapter{} 57 | } 58 | 59 | type RuleSetPersistenceAdapter struct{} 60 | 61 | func (r RuleSetPersistenceAdapter) Convert(source model.RuleSet) (success bool, result RuleSet) { 62 | return true, RuleSet{ 63 | Cacheable: newCacheableImpl(source.ID), 64 | Rule: source, 65 | } 66 | } 67 | 68 | func NewRuleSetAdapter() adapter.Adapter[model.RuleSet, RuleSet] { 69 | return RuleSetPersistenceAdapter{} 70 | } 71 | -------------------------------------------------------------------------------- /mod/implement/event.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/model/context" 7 | ) 8 | 9 | type EventImpl struct { 10 | EntityImpl 11 | triggerAt enum.TriggerType 12 | triggerNow func(ctx context.CallbackContext) bool 13 | clearNow func() bool 14 | callback func(ctx *context.CallbackContext) 15 | } 16 | 17 | func (impl *EventImpl) TriggerAt() enum.TriggerType { 18 | return impl.triggerAt 19 | } 20 | 21 | func (impl *EventImpl) TriggeredNow(ctx context.CallbackContext) bool { 22 | return impl.triggerNow(ctx) 23 | } 24 | 25 | func (impl *EventImpl) ClearNow() bool { 26 | return impl.clearNow() 27 | } 28 | 29 | func (impl *EventImpl) CallBack(callbackContext *context.CallbackContext) { 30 | impl.callback(callbackContext) 31 | } 32 | 33 | type EventOptions func(option *EventImpl) 34 | 35 | func WithEventID(id uint16) EventOptions { 36 | return func(option *EventImpl) { 37 | option.InjectTypeID(uint64(id)) 38 | } 39 | } 40 | 41 | func WithEventTriggerAt(triggerAt enum.TriggerType) EventOptions { 42 | return func(option *EventImpl) { 43 | option.triggerAt = triggerAt 44 | } 45 | } 46 | 47 | func WithEventTriggerNow(triggerNow func(ctx context.CallbackContext) bool) EventOptions { 48 | return func(option *EventImpl) { 49 | option.triggerNow = triggerNow 50 | } 51 | } 52 | 53 | func WithEventClearNow(clearNow func() bool) EventOptions { 54 | return func(option *EventImpl) { 55 | option.clearNow = clearNow 56 | } 57 | } 58 | 59 | func WithEventCallback(callback func(ctx *context.CallbackContext)) EventOptions { 60 | return func(option *EventImpl) { 61 | option.callback = callback 62 | } 63 | } 64 | 65 | func NewEventWithOpts(options ...EventOptions) definition.Event { 66 | impl := &EventImpl{} 67 | for _, option := range options { 68 | option(impl) 69 | } 70 | 71 | return impl 72 | } 73 | -------------------------------------------------------------------------------- /persistence/interface.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import "time" 4 | 5 | // FactoryPersistenceRecord 抽象工厂的持久化结构记录 6 | type FactoryPersistenceRecord struct { 7 | ID uint64 `json:"id"` 8 | UID string `json:"uid"` 9 | } 10 | 11 | // FactoryPersistence 持久化接口,抽象工厂集合的持久化封装 12 | type FactoryPersistence[T any] interface { 13 | Serve(flushFrequency time.Duration, flushPath, flushFile string, errChan chan error) 14 | Exit() 15 | Load(filePath string) (err error) 16 | QueryByID(id uint64) (has bool, result Factory[T]) 17 | QueryByUID(uid string) (has bool, result Factory[T]) 18 | Register(ctor func() T) (success bool) 19 | Flush(flushPath string, flushFile string) (err error) 20 | } 21 | 22 | // Increasable 可增长的,用于数据库主键 23 | type Increasable interface { 24 | int | uint | byte | rune | int8 | int16 | int64 | uint16 | uint32 | uint64 25 | } 26 | 27 | // DatabasePersistence 数据库持久化 28 | type DatabasePersistence[PK Increasable, T any] interface { 29 | QueryByID(id PK) (has bool, result T) 30 | UpdateByID(id PK, newEntity T) (success bool) 31 | InsertOne(entity *T) (success bool, result *T) 32 | DeleteOne(id PK) (success bool) 33 | FindOne(condition T) (has bool, result T) 34 | Find(condition T) (results []T) 35 | } 36 | 37 | // MemoryCache 内存缓存,不进行持久化 38 | type MemoryCache[PK comparable, T any] interface { 39 | QueryByID(id PK) (has bool, result T) 40 | UpdateByID(id PK, newEntity T) (success bool) 41 | InsertOne(id PK, entity T) (success bool) 42 | DeleteOne(id PK) (success bool) 43 | } 44 | 45 | // TimingMemoryCache 带超时系统的内存缓存,不进行持久化,类redis 46 | type TimingMemoryCache[PK comparable, T any] interface { 47 | QueryByID(id PK) (has bool, result T, timeoutAt time.Time) 48 | UpdateByID(id PK, entity T) (success bool, timeoutAt time.Time) 49 | RefreshByID(id PK, timeout time.Duration) (success bool, timeoutAt time.Time) 50 | InsertOne(id PK, entity T, timeout time.Duration) (success bool, timeoutAt time.Time) 51 | DeleteByID(id PK) (success bool) 52 | Serve(proactivelyCleanTime time.Duration, proactivelyCleanIndex float64) 53 | Exit() 54 | } 55 | -------------------------------------------------------------------------------- /entity/model/rule.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/model/context" 6 | ) 7 | 8 | type ReactionCalculator interface { 9 | // ReactionCalculate 计算当前角色身上附着的元素之间能否发生反应,返回反应类型和剩余元素 10 | ReactionCalculate([]enum.ElementType) (reaction enum.Reaction, elementRemains []enum.ElementType) 11 | 12 | // DamageCalculate 根据反应类型计算对应的伤害修正 13 | DamageCalculate(reaction enum.Reaction, targetCharacter Character, ctx *context.DamageContext) 14 | 15 | // EffectCalculate 根据反应类型计算对应的反应效果 16 | EffectCalculate(reaction enum.Reaction, targetPlayer Player) (ctx *context.CallbackContext) 17 | 18 | // Attach 尝试让新元素附着在现有元素集合内,此时不触发元素反应,返回尝试附着后的元素集合 19 | Attach(originalElements []enum.ElementType, newElement enum.ElementType) (resultElements []enum.ElementType) 20 | 21 | // Relative 判断某种反应是否是某元素的相关反应 22 | Relative(reaction enum.Reaction, relativeElement enum.ElementType) bool 23 | } 24 | 25 | type VictorCalculator interface { 26 | // CalculateVictors 计算游戏的胜利者 27 | CalculateVictors(players []Player) (has bool, victors []Player) 28 | } 29 | 30 | type GameOptions struct { 31 | ReRollTimes uint // ReRollTimes 所有玩家的基础可重掷次数 32 | StaticCost map[enum.ElementType]uint // StaticCost 所有玩家的基础固定持有骰子 33 | RollAmount uint // RollAmount 所有玩家的投掷阶段生成元素骰子数量 34 | GetCards uint // GetCards 所有玩家在回合开始时可以获得的卡牌数量 35 | SwitchCost map[enum.ElementType]uint // SwitchCost 切换角色所需要的消费 36 | } 37 | 38 | var ( 39 | nullReactionCalculator ReactionCalculator = nil 40 | nullVictorCalculator VictorCalculator = nil 41 | ) 42 | 43 | type RuleSet struct { 44 | ID uint64 45 | ReactionCalculator ReactionCalculator 46 | VictorCalculator VictorCalculator 47 | } 48 | 49 | func (r RuleSet) ImplementationCheck() bool { 50 | if r.ReactionCalculator == nullReactionCalculator { 51 | return false 52 | } 53 | 54 | if r.VictorCalculator == nullVictorCalculator { 55 | return false 56 | } 57 | 58 | return true 59 | } 60 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 22 |
23 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /protocol/http/middleware/authenticator.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/persistence" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/config" 7 | "github.com/sunist-c/genius-invokation-simulator-backend/util" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | // AttachToken 将Token信息附加给响应 13 | func AttachToken(ctx *gin.Context, conf config.MiddlewareConfig, player uint64) (success bool) { 14 | uuid := GetUUID(ctx, conf) 15 | ok, ip := GetIPTrace(ctx, conf) 16 | if !ok { 17 | return false 18 | } 19 | tokenID, token := util.GenerateMD5(strconv.Itoa(int(ip))), util.GenerateMD5(uuid) 20 | if success, _ = persistence.TokenPersistence.InsertOne(token, persistence.Token{UID: player, ID: tokenID}, time.Second*time.Duration(conf.TokenRefreshTime)); !success { 21 | return false 22 | } else { 23 | ctx.SetCookie(conf.TokenIDKey, tokenID, int(conf.TokenRefreshTime), "/", conf.CookieDomain, false, true) 24 | ctx.SetCookie(conf.TokenKey, token, int(conf.TokenRefreshTime), "/", conf.CookieDomain, false, true) 25 | return true 26 | } 27 | } 28 | 29 | // GetToken 获取Cookie里的Token信息 30 | func GetToken(ctx *gin.Context, conf config.MiddlewareConfig) (success bool, token persistence.Token) { 31 | if result, err := ctx.Cookie(conf.TokenKey); err != nil { 32 | return false, token 33 | } else if has, tokenStruct, _ := persistence.TokenPersistence.QueryByID(result); !has { 34 | return false, token 35 | } else { 36 | return true, tokenStruct 37 | } 38 | } 39 | 40 | // NewAuthenticator 新建一个认证器,只有Cookie中的Token和ID正确时才会放行,但是没有具体处理是否有权限 41 | func NewAuthenticator(conf config.MiddlewareConfig) func(ctx *gin.Context) { 42 | return func(ctx *gin.Context) { 43 | if tokenID, err := ctx.Cookie(conf.TokenIDKey); err != nil { 44 | ctx.AbortWithStatus(403) 45 | } else if token, err := ctx.Cookie(conf.TokenKey); err != nil { 46 | ctx.AbortWithStatus(403) 47 | } else if has, result, timeoutAt := persistence.TokenPersistence.QueryByID(token); !has { 48 | ctx.AbortWithStatus(403) 49 | } else if result.ID != tokenID { 50 | ctx.AbortWithStatus(403) 51 | } else if time.Now().Add(time.Duration(conf.TokenRemainingTime) * time.Second).After(timeoutAt) { 52 | persistence.TokenPersistence.RefreshByID(token, time.Second*time.Duration(conf.TokenRefreshTime)) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /model/modifier/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import "testing" 4 | 5 | // BenchmarkTestContextRemove 6 | func BenchmarkTestContextRemove(b *testing.B) { 7 | add := func(ctx *Context[int]) { 8 | *ctx.data += 1 9 | } 10 | addTwice := func(ctx *Context[int]) { 11 | *ctx.data += 1 12 | ctx.Next() 13 | *ctx.data += 1 14 | } 15 | addBack := func(ctx *Context[int]) { 16 | ctx.Next() 17 | *ctx.data += 1 18 | } 19 | handlers := NewChain[int]() 20 | for j := uint64(0); j <= 128; j++ { 21 | switch j % 3 { 22 | case 0: 23 | handlers.Append(newModifier(j, add)) 24 | case 1: 25 | handlers.Append(newModifier(j, addTwice)) 26 | case 2: 27 | handlers.Append(newModifier(j, addBack)) 28 | } 29 | } 30 | b.ResetTimer() 31 | 32 | for i := 0; i < b.N; i++ { 33 | handlers.Remove(128) 34 | } 35 | } 36 | 37 | // BenchmarkTestContextAppend 38 | func BenchmarkTestContextAppend(b *testing.B) { 39 | add := func(ctx *Context[int]) { 40 | *ctx.data += 1 41 | } 42 | addTwice := func(ctx *Context[int]) { 43 | *ctx.data += 1 44 | ctx.Next() 45 | *ctx.data += 1 46 | } 47 | addBack := func(ctx *Context[int]) { 48 | ctx.Next() 49 | *ctx.data += 1 50 | } 51 | handlers := NewChain[int]() 52 | for j := uint64(0); j <= 128; j++ { 53 | switch j % 3 { 54 | case 0: 55 | handlers.Append(newModifier(j, add)) 56 | case 1: 57 | handlers.Append(newModifier(j, addTwice)) 58 | case 2: 59 | handlers.Append(newModifier(j, addBack)) 60 | } 61 | } 62 | b.ResetTimer() 63 | for i := 0; i < b.N; i++ { 64 | handlers.Append(newModifier(129, add)) 65 | } 66 | } 67 | 68 | // BenchmarkTestContextExecute 69 | func BenchmarkTestContextExecute(b *testing.B) { 70 | add := func(ctx *Context[int]) { 71 | *ctx.data += 1 72 | } 73 | addTwice := func(ctx *Context[int]) { 74 | *ctx.data += 1 75 | ctx.Next() 76 | *ctx.data += 1 77 | } 78 | addBack := func(ctx *Context[int]) { 79 | ctx.Next() 80 | *ctx.data += 1 81 | } 82 | handlers := NewChain[int]() 83 | for j := uint64(0); j < 1; j++ { 84 | switch j % 3 { 85 | case 0: 86 | handlers.Append(newModifier(j, add)) 87 | case 1: 88 | handlers.Append(newModifier(j, addTwice)) 89 | case 2: 90 | handlers.Append(newModifier(j, addBack)) 91 | } 92 | } 93 | b.ResetTimer() 94 | 95 | for i := 0; i < b.N; i++ { 96 | data := 0 97 | handlers.Execute(&data) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /protocol/http/service/roominfo.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/persistence" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http" 7 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/middleware" 8 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/util" 9 | ) 10 | 11 | var ( 12 | roomInfoRouter *gin.RouterGroup 13 | roomInfoCache map[uint64]bool 14 | ) 15 | 16 | func initRoomInfoService() { 17 | roomInfoRouter = http.RegisterServices("/room") 18 | roomInfoCache = map[uint64]bool{} 19 | for i := uint64(0); i < serviceConfig.MaxRooms; i++ { 20 | roomInfoCache[i] = false 21 | } 22 | 23 | roomInfoRouter.Use( 24 | append( 25 | http.EngineMiddlewares, 26 | middleware.NewQPSLimiter(middlewareConfig), 27 | )..., 28 | ) 29 | 30 | roomInfoRouter.GET("", 31 | listRoomServiceHandler(), 32 | ) 33 | roomInfoRouter.GET(":room_id", 34 | queryRoomServiceHandler(), 35 | ) 36 | } 37 | 38 | type ListRoomResponse struct { 39 | Rooms []persistence.RoomInfo `json:"rooms"` 40 | } 41 | 42 | type QueryRoomResponse struct { 43 | RoomInfo persistence.RoomInfo `json:"room_info"` 44 | } 45 | 46 | func listRoomServiceHandler() func(ctx *gin.Context) { 47 | return func(ctx *gin.Context) { 48 | response := &ListRoomResponse{Rooms: []persistence.RoomInfo{}} 49 | 50 | // 查询RoomInfo状态 51 | for roomID, roomValid := range roomInfoCache { 52 | // 将可用的房间进行查询 53 | if roomValid { 54 | if success, appendRoom := persistence.RoomInfoPersistence.QueryByID(roomID); !success { 55 | // 查找RoomInfo失败,InternalError 56 | ctx.AbortWithStatus(500) 57 | return 58 | } else { 59 | // 查找RoomInfo成功,加入到响应中 60 | response.Rooms = append(response.Rooms, appendRoom) 61 | } 62 | } 63 | } 64 | 65 | // 查询完毕,Success 66 | ctx.JSON(200, response) 67 | } 68 | } 69 | 70 | func queryRoomServiceHandler() func(ctx *gin.Context) { 71 | return func(ctx *gin.Context) { 72 | if existRoomID, roomID := util.QueryPathUint64(ctx, ":room_id"); !existRoomID { 73 | // 缺少必要的URL参数,BadRequest 74 | ctx.AbortWithStatus(400) 75 | } else if roomValid, existRoom := roomInfoCache[roomID]; !existRoom || !roomValid { 76 | // 请求的房间不存在或无效,NotFound 77 | ctx.AbortWithStatus(404) 78 | } else if existRoomInfo, roomInfo := persistence.RoomInfoPersistence.QueryByID(roomID); !existRoomInfo { 79 | // 查询房间信息失败,InternalError 80 | ctx.AbortWithStatus(500) 81 | } else { 82 | // 查询成功,Success 83 | response := QueryRoomResponse{RoomInfo: roomInfo} 84 | ctx.JSON(200, response) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /mod/implement/character.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 6 | ) 7 | 8 | type CharacterImpl struct { 9 | EntityImpl 10 | name string 11 | affiliation enum.Affiliation 12 | vision enum.ElementType 13 | weapon enum.WeaponType 14 | skills []definition.Skill 15 | hp uint 16 | mp uint 17 | } 18 | 19 | func (c CharacterImpl) Name() string { 20 | return c.name 21 | } 22 | 23 | func (c CharacterImpl) Affiliation() enum.Affiliation { 24 | return c.affiliation 25 | } 26 | 27 | func (c CharacterImpl) Vision() enum.ElementType { 28 | return c.vision 29 | } 30 | 31 | func (c CharacterImpl) Weapon() enum.WeaponType { 32 | return c.weapon 33 | } 34 | 35 | func (c CharacterImpl) Skills() []definition.Skill { 36 | return c.skills 37 | } 38 | 39 | func (c CharacterImpl) HP() uint { 40 | return c.hp 41 | } 42 | 43 | func (c CharacterImpl) MP() uint { 44 | return c.mp 45 | } 46 | 47 | func WithCharacterID(id uint16) CharacterOptions { 48 | return func(option *CharacterImpl) { 49 | option.InjectTypeID(uint64(id)) 50 | } 51 | } 52 | 53 | func WithCharacterName(name string) CharacterOptions { 54 | return func(option *CharacterImpl) { 55 | option.name = name 56 | } 57 | } 58 | 59 | func WithCharacterAffiliation(affiliation enum.Affiliation) CharacterOptions { 60 | return func(option *CharacterImpl) { 61 | option.affiliation = affiliation 62 | } 63 | } 64 | 65 | func WithCharacterVision(vision enum.ElementType) CharacterOptions { 66 | return func(option *CharacterImpl) { 67 | option.vision = vision 68 | } 69 | } 70 | 71 | func WithCharacterWeapon(weapon enum.WeaponType) CharacterOptions { 72 | return func(option *CharacterImpl) { 73 | option.weapon = weapon 74 | } 75 | } 76 | 77 | func WithCharacterSkills(skills ...definition.Skill) CharacterOptions { 78 | return func(option *CharacterImpl) { 79 | option.skills = skills 80 | } 81 | } 82 | 83 | func WithCharacterHP(hp uint) CharacterOptions { 84 | return func(option *CharacterImpl) { 85 | option.hp = hp 86 | } 87 | } 88 | 89 | func WithCharacterMP(mp uint) CharacterOptions { 90 | return func(option *CharacterImpl) { 91 | option.mp = mp 92 | } 93 | } 94 | 95 | type CharacterOptions func(option *CharacterImpl) 96 | 97 | func NewCharacterWithOpts(opts ...CharacterOptions) definition.Character { 98 | character := &CharacterImpl{} 99 | for _, opt := range opts { 100 | opt(character) 101 | } 102 | 103 | return character 104 | } 105 | -------------------------------------------------------------------------------- /protocol/http/service/localization.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/persistence" 7 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http" 8 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/message" 9 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/middleware" 10 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/http/util" 11 | ) 12 | 13 | var ( 14 | localizationRouter *gin.RouterGroup 15 | ) 16 | 17 | func initLocalizeService() { 18 | localizationRouter = http.RegisterServices("/localization") 19 | 20 | localizationRouter.Use( 21 | append( 22 | http.EngineMiddlewares, 23 | middleware.NewQPSLimiter(middlewareConfig), 24 | )..., 25 | ) 26 | localizationRouter.GET( 27 | "/language_pack/:id", 28 | queryLanguagePackServiceHandler(), 29 | ) 30 | localizationRouter.GET( 31 | "/translate", 32 | translateServiceServiceHandler(), 33 | ) 34 | } 35 | 36 | func queryLanguagePackServiceHandler() func(ctx *gin.Context) { 37 | return func(ctx *gin.Context) { 38 | if exist, result := util.QueryPath(ctx, ":id"); !exist { 39 | ctx.AbortWithStatus(400) 40 | } else if has, record := persistence.LocalizationPersistence.QueryByID(result); !has { 41 | ctx.AbortWithStatus(404) 42 | } else { 43 | response := message.LocalizationQueryResponse{LanguagePack: record.Pack()} 44 | ctx.JSON(200, response) 45 | } 46 | } 47 | } 48 | 49 | func translateServiceServiceHandler() func(ctx *gin.Context) { 50 | return func(ctx *gin.Context) { 51 | var ( 52 | exist bool 53 | languagePack string 54 | destLanguage int 55 | request message.TranslationRequest 56 | ) 57 | if exist, languagePack = util.QueryPath(ctx, "language_package"); !exist { 58 | ctx.AbortWithStatus(400) 59 | } else if exist, destLanguage = util.QueryPathInt(ctx, "target_language"); !exist { 60 | ctx.AbortWithStatus(400) 61 | } else if !util.BindJson(ctx, &request) { 62 | ctx.AbortWithStatus(400) 63 | } else { 64 | if has, dictionary := persistence.LocalizationPersistence.QueryByID(languagePack); !has { 65 | ctx.AbortWithStatus(404) 66 | } else { 67 | response := message.TranslationResponse{Translation: map[string]string{}} 68 | language := enum.Language(destLanguage) 69 | for _, word := range request.Words { 70 | if ok, result := dictionary.Translate(word, language); ok { 71 | response.Translation[word] = result 72 | } 73 | } 74 | ctx.JSON(200, response) 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /persistence/sqlite.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "github.com/go-xorm/xorm" 5 | _ "github.com/mattn/go-sqlite3" 6 | ) 7 | 8 | var ( 9 | sqlite3DB *xorm.Engine = nil 10 | ) 11 | 12 | type sqliteTable[PK Increasable, T any] struct { 13 | session *xorm.Session 14 | errChan chan error 15 | } 16 | 17 | func (s *sqliteTable[PK, T]) QueryByID(id PK) (has bool, result T) { 18 | if has, err := s.session.ID(id).Get(&result); err != nil { 19 | s.errChan <- err 20 | return false, result 21 | } else if !has { 22 | return false, result 23 | } else { 24 | return true, result 25 | } 26 | } 27 | 28 | func (s *sqliteTable[PK, T]) UpdateByID(id PK, newEntity T) (success bool) { 29 | var condition T 30 | if has, err := s.session.ID(id).Get(&condition); err != nil { 31 | s.errChan <- err 32 | return false 33 | } else if !has { 34 | return false 35 | } else if _, err = s.session.ID(id).Update(newEntity); err != nil { 36 | s.errChan <- err 37 | return false 38 | } else { 39 | return true 40 | } 41 | } 42 | 43 | func (s *sqliteTable[PK, T]) InsertOne(entity *T) (success bool, result *T) { 44 | if _, err := s.session.InsertOne(entity); err != nil { 45 | s.errChan <- err 46 | return false, nil 47 | } else { 48 | return true, entity 49 | } 50 | } 51 | 52 | func (s *sqliteTable[PK, T]) DeleteOne(id PK) (success bool) { 53 | var condition T 54 | if exist, err := s.session.ID(id).Get(&condition); err != nil { 55 | s.errChan <- err 56 | return false 57 | } else if !exist { 58 | return false 59 | } else if _, err = s.session.ID(id).Delete(condition); err != nil { 60 | s.errChan <- err 61 | return false 62 | } else { 63 | return true 64 | } 65 | } 66 | 67 | func (s *sqliteTable[PK, T]) FindOne(condition T) (has bool, result T) { 68 | var results []T 69 | if err := s.session.Find(&results, condition); err != nil { 70 | s.errChan <- err 71 | return false, result 72 | } else if len(results) != 1 { 73 | return false, result 74 | } else { 75 | return true, results[0] 76 | } 77 | } 78 | 79 | func (s *sqliteTable[PK, T]) Find(condition T) (results []T) { 80 | if err := s.session.Find(&results, condition); err != nil { 81 | s.errChan <- err 82 | return []T{} 83 | } else { 84 | return results 85 | } 86 | } 87 | 88 | func newDatabasePersistence[PK Increasable, T any](errChan chan error) (success bool, persistence DatabasePersistence[PK, T]) { 89 | var entity T 90 | if err := sqlite3DB.Sync2(entity); err != nil { 91 | errChan <- err 92 | return false, persistence 93 | } else { 94 | table := &sqliteTable[PK, T]{ 95 | session: sqlite3DB.Table(entity), 96 | errChan: errChan, 97 | } 98 | return true, table 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /docs/development/api/README.md: -------------------------------------------------------------------------------- 1 | 这里是GISB的API文档,此文档为前后端开发人员提供关于GISB的 `HTTP` 服务的说明与解释 2 | 3 | # 1. 状态码定义 4 | 5 | GISB的后端使用HTTP状态码来表示请求的响应结果,大部分情况下,非 `200(Success)` 状态码都不会返回除状态码的任何信息 6 | 7 | 下面是GISB的HTTP服务对其返回状态码的说明: 8 | 9 | **200(Success)** 10 | 11 | 此状态表明HTTP请求合法,且服务端响应过程正常,且返回数据正常 12 | 13 | **400(BadRequest)** 14 | 15 | 此状态表明HTTP请求非法,您可以试着分析您的请求,是否将必须的参数提供完整提供,且请求体的格式正确无误 16 | 17 | **403(Forbidden)** 18 | 19 | 此状态表明HTTP请求合法,但发起者对所请求的资源不具备访问权限,您可以试着分析您的请求,是否将鉴权相关的参数/cookie完整提供,或是请求的资源是否正确 20 | 21 | **404(NotFound)** 22 | 23 | 此状态表明HTTP请求合法,但服务端不持有所请求资源,您可以检查您访问的资源是否正确 24 | 25 | **412(PreconditionFailed)** 26 | 27 | 此状态表明HTTP请求合法,但发起者被服务器所限制,一般是由于您的请求频率过快或认证失败次数过多导致的,您可以等待一段时间后重新发起请求 28 | 29 | **500(InternalError)** 30 | 31 | 此状态表明HTTP请求合法,但服务器在响应请求的过程中出现了错误,您可以将此错误反馈给服务器管理人员或本项目的开发者以寻求解决方法 32 | 33 | # 2. 配置说明 34 | 35 | GISB可以在启动前对其进行配置,此处对其进行说明与解释,配置的声明如下: 36 | 37 | ```go 38 | type MiddlewareConfig struct { 39 | UUIDKey string `json:"uuid_key" yaml:"uuid_key" xml:"uuid_key"` 40 | IPTranceKey string `json:"ip_trace_key" yaml:"ip_trance_key" xml:"ip_trance_key"` 41 | InterdictorTraceKey string `json:"interdictor_trace_key" yaml:"interdictor_trace_key" xml:"interdictor_trace_key"` 42 | InterdictorBlockedTime uint `json:"interdictor_blocked_time" yaml:"interdictor_blocked_time" xml:"interdictor_blocked_time"` 43 | InterdictorTriggerCount uint `json:"interdictor_trigger_count" yaml:"interdictor_trigger_count" xml:"interdictor_trigger_count"` 44 | QPSLimitTime uint `json:"qps_limit_time" yaml:"qps_limit_time" xml:"qps_limit_time"` 45 | TokenIDKey string `json:"token_id_key" yaml:"token_id_key" xml:"token_id_key"` 46 | TokenKey string `json:"token_key" yaml:"token_key" xml:"token_key"` 47 | TokenRefreshTime uint `json:"token_refresh_time" yaml:"token_refresh_time" xml:"token_refresh_time"` 48 | TokenRemainingTime uint `json:"token_remaining_time" yaml:"token_remaining_time" xml:"token_remaining_time"` 49 | CookieDomain string `json:"cookie_domain" yaml:"cookie_domain" xml:"cookie_domain"` 50 | } 51 | ``` 52 | 53 | 默认的配置如下: 54 | 55 | ```go 56 | var ( 57 | config = &EngineConfig{ 58 | Middleware: MiddlewareConfig{ 59 | UUIDKey: "uuid", 60 | IPTranceKey: "ip", 61 | InterdictorTraceKey: "interdicted", 62 | InterdictorBlockedTime: 600, 63 | InterdictorTriggerCount: 5, 64 | QPSLimitTime: 1, 65 | TokenIDKey: "gisb_token_id", 66 | TokenKey: "gisb_token", 67 | TokenRefreshTime: 7200, 68 | TokenRemainingTime: 1800, 69 | CookieDomain: "localhost", 70 | }, 71 | Service: ServiceConfig{ 72 | MaxRooms: 100, 73 | }, 74 | } 75 | } 76 | ) 77 | ``` 78 | -------------------------------------------------------------------------------- /entity/carddeck.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | "math/rand" 7 | "time" 8 | ) 9 | 10 | var ( 11 | random = rand.New(rand.NewSource(time.Now().UnixMilli())) 12 | ) 13 | 14 | // CardDeck 牌堆,记录顺序的是一个数组,正常情况下已取出的牌永远在队列的一端 15 | type CardDeck struct { 16 | cards map[uint64]model.Card 17 | used map[uint64]bool 18 | queue []uint64 19 | offset int 20 | remain uint 21 | } 22 | 23 | // arrange 整理CardDeck中offset之后的部分 24 | func (c *CardDeck) arrange() { 25 | for i := c.offset; i < len(c.queue); i++ { 26 | if c.used[c.queue[i]] { 27 | c.queue[c.offset], c.queue[i] = c.queue[i], c.queue[c.offset] 28 | c.offset++ 29 | c.remain-- 30 | } 31 | } 32 | } 33 | 34 | // takeOne 将CardDeck的队列中第index张牌标记为已取出 35 | func (c *CardDeck) takeOne(index int) { 36 | c.used[c.queue[index]] = true 37 | c.queue[index], c.queue[c.offset] = c.queue[c.offset], c.queue[index] 38 | c.offset++ 39 | c.remain-- 40 | } 41 | 42 | // Shuffle 将CardDeck中的未取出部分进行洗牌 43 | func (c *CardDeck) Shuffle() { 44 | random.Shuffle(len(c.queue)-c.offset, func(i, j int) { 45 | c.queue[i+c.offset], c.queue[j+c.offset] = c.queue[j+c.offset], c.queue[i+c.offset] 46 | }) 47 | } 48 | 49 | // GetOne 从CardDeck中取出一张牌 50 | func (c *CardDeck) GetOne() (result model.Card, success bool) { 51 | if c.remain != 0 { 52 | for i := c.offset; i < len(c.queue); i++ { 53 | if !c.used[c.queue[i]] { 54 | result = c.cards[c.queue[i]] 55 | c.takeOne(i) 56 | return result, true 57 | } 58 | } 59 | } 60 | 61 | return nil, false 62 | } 63 | 64 | // FindOne 从CardDeck中取出一张指定类型的牌 65 | func (c *CardDeck) FindOne(cardType enum.CardType) (result model.Card, success bool) { 66 | for i := c.offset; i < len(c.queue); i++ { 67 | if !c.used[c.queue[i]] && c.cards[c.queue[i]].Type() == cardType { 68 | result = c.cards[c.queue[i]] 69 | c.takeOne(i) 70 | return result, true 71 | } 72 | } 73 | 74 | return nil, false 75 | } 76 | 77 | // Reset 将牌堆中除了holding之外的牌全部标记为未取出,此方法没有洗牌逻辑 78 | func (c *CardDeck) Reset(holding []uint64) { 79 | for card := range c.used { 80 | c.used[card] = false 81 | } 82 | 83 | c.remain = uint(len(c.queue)) 84 | c.offset = 0 85 | for _, id := range holding { 86 | c.used[id] = true 87 | } 88 | 89 | c.arrange() 90 | } 91 | 92 | // Remain 获取CardDeck还可以获取多少张牌 93 | func (c CardDeck) Remain() uint { 94 | return c.remain 95 | } 96 | 97 | func NewCardDeck(cards []model.Card) *CardDeck { 98 | cardDeck := &CardDeck{ 99 | cards: map[uint64]model.Card{}, 100 | used: map[uint64]bool{}, 101 | queue: []uint64{}, 102 | offset: 0, 103 | remain: 0, 104 | } 105 | 106 | for _, card := range cards { 107 | cardDeck.cards[card.TypeID()] = card 108 | cardDeck.used[card.TypeID()] = false 109 | cardDeck.queue = append(cardDeck.queue, card.TypeID()) 110 | cardDeck.remain++ 111 | } 112 | 113 | return cardDeck 114 | } 115 | -------------------------------------------------------------------------------- /persistence/model.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/websocket/message" 7 | "time" 8 | ) 9 | 10 | // Card 被持久化模块托管的Card工厂的产品 11 | type Card struct { 12 | Cacheable 13 | Card model.Card 14 | } 15 | 16 | // Skill 被持久化模块托管的Skill工厂的产品 17 | type Skill struct { 18 | Cacheable 19 | Skill model.Skill 20 | } 21 | 22 | // RuleSet 被持久化模块托管的RuleSet工厂的产品 23 | type RuleSet struct { 24 | Cacheable 25 | Rule model.RuleSet 26 | } 27 | 28 | // Summon 被持久化模块托管的Summon工厂的产品 29 | type Summon struct { 30 | Cacheable 31 | // todo: add summon interfaces 32 | } 33 | 34 | // Event 被持久化模块托管的Event工厂的产品 35 | type Event struct { 36 | Cacheable 37 | Event model.Event 38 | } 39 | 40 | // Character 被持久化模块托管的CharacterInfo工厂的产品 41 | type Character struct { 42 | Cacheable 43 | TypeID uint64 44 | Affiliation enum.Affiliation 45 | Vision enum.ElementType 46 | Weapon enum.WeaponType 47 | MaxHP uint 48 | MaxMP uint 49 | Skills []uint64 50 | } 51 | 52 | func (c Character) ID() uint64 { return c.TypeID } 53 | 54 | // Player 被持久化模块托管的Player信息 55 | type Player struct { 56 | UID uint64 `xorm:"pk autoincr notnull unique index"` // UID Player的UID,主键 57 | NickName string `xorm:"notnull varchar(64)"` // NickName Player的昵称 58 | Password string `xorm:"notnull varchar(64)"` // Password Player的密码Hash 59 | } 60 | 61 | // CardDeck 被持久化模块托管的CardDeck信息 62 | type CardDeck struct { 63 | ID uint64 `xorm:"pk autoincr notnull unique index" json:"id"` // ID CardDeck的记录ID,主键 64 | OwnerUID uint64 `xorm:"notnull index" json:"owner_uid"` // OwnerUID CardDeck的持有者 65 | RequiredPackages []string `xorm:"notnull json" json:"required_packages"` // RequiredPackages CardDeck需要的包 66 | Cards []uint64 `xorm:"notnull json" json:"cards"` // Cards CardDeck包含的卡组 67 | Characters []uint64 `xorm:"notnull json" json:"characters"` // Characters CardDeck包含的角色 68 | } 69 | 70 | // Token 被持久化模块托管的Token缓存 71 | type Token struct { 72 | UID uint64 // UID Token持有者的UID 73 | ID string // ID Token的ID 74 | } 75 | 76 | // RoomInfo 被持久化模块托管的RoomInfo缓存 77 | type RoomInfo struct { 78 | RoomID uint64 `json:"room_id"` 79 | CreatedAt time.Time `json:"created_at"` 80 | CreatorID uint64 `json:"creator_id"` 81 | Token string `json:"token"` 82 | Players []uint64 `json:"players"` 83 | Viewers uint64 `json:"viewers"` 84 | RequiredPackages []string `json:"required_packages"` 85 | GameOptions message.GameOptions `json:"game_options"` 86 | } 87 | 88 | // ModInfo 被持久化模块托管的ModInfo缓存 89 | type ModInfo struct { 90 | PackageName string `json:"package_name"` 91 | Events []uint `json:"events"` 92 | Characters []uint `json:"characters"` 93 | Cards []uint `json:"cards"` 94 | Skills []uint `json:"skills"` 95 | Summons []uint `json:"summons"` 96 | } 97 | -------------------------------------------------------------------------------- /util/generate.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | uuid "github.com/satori/go.uuid" 7 | "reflect" 8 | "time" 9 | "unsafe" 10 | ) 11 | 12 | type UintLike interface { 13 | uint | uint8 | uint16 | uint32 | uint64 14 | } 15 | 16 | // GenerateUUID 生成一个UUID,长度为36 17 | func GenerateUUID() string { 18 | return uuid.Must(uuid.NewV4(), nil).String() 19 | } 20 | 21 | // GenerateTypeID 根据entity的包和结构名,生成类型ID 22 | func GenerateTypeID[T any](entity T) (uid string) { 23 | typesOfT := reflect.TypeOf(entity) 24 | return fmt.Sprintf("%s@%s", typesOfT.PkgPath(), typesOfT.Name()) 25 | } 26 | 27 | // GeneratePackageID 根据给定的包名,生成其Hash,不保证不会冲突 28 | func GeneratePackageID[ID UintLike](packageName string) (id ID) { 29 | return GenerateHashWithOpts[string, ID](packageName) 30 | } 31 | 32 | // GenerateHash 将任意结构进行哈希,使用SDBM算法作为实现 33 | func GenerateHash[Key any](key Key) (hash uint) { 34 | return GenerateHashWithOpts[Key, uint](key) 35 | } 36 | 37 | // GeneratePrefixHash 将任意结构的前offset字节的内容进行哈希,越界部份不会被计算,使用SDBM算法作为实现 38 | func GeneratePrefixHash[Key any](key Key, offset uintptr) (hash uint) { 39 | return GeneratePrefixHashWithOpts[Key, uint](key, offset) 40 | } 41 | 42 | // GenerateHashWithOpts 将任意结构进行哈希,生成一个指定类型(无符号整形)的哈希值,使用SDBM算法作为实现 43 | func GenerateHashWithOpts[Key any, Hash UintLike](key Key) (hash Hash) { 44 | entityPtr := &key 45 | sum := uint64(0) 46 | start := uintptr(unsafe.Pointer(entityPtr)) 47 | end := unsafe.Sizeof(key) + start 48 | offset := unsafe.Sizeof(byte(0)) 49 | 50 | for i := start; i < end; i += offset { 51 | byteData := *(*byte)(unsafe.Pointer(i)) 52 | sum = uint64(byteData) + (sum << 6) + (sum << 16) - sum 53 | } 54 | 55 | return Hash(sum) 56 | } 57 | 58 | // GeneratePrefixHashWithOpts 将任意结构的前offset字节的内容进行哈希,生成一个指定类型(无符号整形)的哈希值,越界部份不会被计算,使用SDBM算法作为实现 59 | func GeneratePrefixHashWithOpts[Key any, Hash UintLike](key Key, offset uintptr) (hash Hash) { 60 | entityPtr := &key 61 | sum := uint64(0) 62 | start := uintptr(unsafe.Pointer(entityPtr)) 63 | end := start + offset 64 | if end > unsafe.Sizeof(key)+start { 65 | end = unsafe.Sizeof(key) + start 66 | } 67 | 68 | byteOffset := unsafe.Sizeof(byte(0)) 69 | for i := start; i < end; i += byteOffset { 70 | b := *(*byte)(unsafe.Pointer(i)) 71 | sum = uint64(b) + (sum << 6) + (sum << 16) - sum 72 | } 73 | 74 | return Hash(sum) 75 | } 76 | 77 | // GenerateMD5 将给定的字符串使用MD5摘要算法生成摘要 78 | func GenerateMD5(source string) (md5CheckSum string) { 79 | return fmt.Sprintf("%x", md5.Sum([]byte(source))) 80 | } 81 | 82 | // GenerateUID 根据时间戳生成一个UID,毫秒级,48位,其中前6位为设备序列号,后42位为毫秒时间戳,可使用138年 83 | func GenerateUID(mac uint64, timeStamp time.Time) (uid uint64) { 84 | timeStampID := uint64(timeStamp.Sub(GetZeroTimeStamp()).Milliseconds()) 85 | timePartBits := uint64(1<<42) - 1 86 | deviceID := GenerateHashWithOpts[uint64, uint8](mac) 87 | devicePart := uint64(1<<6) - 1 88 | return ((uint64(deviceID) & devicePart) << 42) + (timeStampID & timePartBits) 89 | } 90 | 91 | // GenerateRealID 根据MOD的ID和MOD中的ID,拼接出真实ID 92 | func GenerateRealID(uid uint64, sub uint16) (realID uint64) { 93 | uidPartBits := uint64(1<<48) - 1 94 | subPartBits := uint64(1<<16) - 1 95 | return ((uid & uidPartBits) << 16) + (uint64(sub) & subPartBits) 96 | } 97 | -------------------------------------------------------------------------------- /mod/implement/rule.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 7 | ) 8 | 9 | var necessaryImplements = []enum.RuleType{ 10 | enum.RuleTypeReactionCalculator, 11 | enum.RuleTypeVictorCalculator, 12 | } 13 | 14 | func RuleConvert[dest any](source interface{}) (success bool, result dest) { 15 | converted, ok := source.(dest) 16 | return ok, converted 17 | } 18 | 19 | type RuleImpl struct { 20 | EntityImpl 21 | ruleImpl map[enum.RuleType]interface{} 22 | } 23 | 24 | func (r *RuleImpl) CopyFrom(source definition.Rule, filter ...enum.RuleType) { 25 | if r.ruleImpl == nil { 26 | r.ruleImpl = map[enum.RuleType]interface{}{} 27 | } 28 | 29 | for _, ruleType := range filter { 30 | r.ruleImpl[ruleType] = source.Implements(ruleType) 31 | } 32 | } 33 | 34 | func (r *RuleImpl) Implements(ruleType enum.RuleType) interface{} { 35 | if r.ruleImpl == nil { 36 | r.ruleImpl = map[enum.RuleType]interface{}{} 37 | } 38 | 39 | return r.ruleImpl[ruleType] 40 | } 41 | 42 | func (r *RuleImpl) CheckImplements() (success bool) { 43 | if r.ruleImpl == nil { 44 | return false 45 | } 46 | 47 | if reactionCalculator := r.Implements(enum.RuleTypeReactionCalculator); !debugFlag && reactionCalculator == nil { 48 | return false 49 | } else if success, _ := RuleConvert[model.ReactionCalculator](reactionCalculator); !debugFlag && !success { 50 | return false 51 | } 52 | 53 | if victorCalculator := r.Implements(enum.RuleTypeVictorCalculator); !debugFlag && victorCalculator == nil { 54 | return false 55 | } else if success, _ := RuleConvert[model.VictorCalculator](victorCalculator); !debugFlag && !success { 56 | return false 57 | } 58 | 59 | return true 60 | } 61 | 62 | type RuleImplOptions func(option *RuleImpl) 63 | 64 | func WithRuleID(id uint16) RuleImplOptions { 65 | return func(option *RuleImpl) { 66 | option.EntityImpl.InjectTypeID(uint64(id)) 67 | } 68 | } 69 | 70 | func WithRuleImplement(ruleType enum.RuleType, implement interface{}) RuleImplOptions { 71 | return func(option *RuleImpl) { 72 | if option.ruleImpl == nil { 73 | option.ruleImpl = map[enum.RuleType]interface{}{} 74 | } 75 | 76 | option.ruleImpl[ruleType] = implement 77 | } 78 | } 79 | 80 | func WithRuleCopyFrom(another definition.Rule, filter ...enum.RuleType) RuleImplOptions { 81 | return func(option *RuleImpl) { 82 | if option.ruleImpl == nil { 83 | option.ruleImpl = map[enum.RuleType]interface{}{} 84 | } 85 | 86 | for _, ruleType := range filter { 87 | option.ruleImpl[ruleType] = another.Implements(ruleType) 88 | } 89 | } 90 | } 91 | 92 | func NewRuleWithOpts(options ...RuleImplOptions) definition.Rule { 93 | impl := &RuleImpl{ 94 | ruleImpl: map[enum.RuleType]interface{}{}, 95 | } 96 | 97 | for _, option := range options { 98 | option(impl) 99 | } 100 | 101 | if !impl.CheckImplements() && !debugFlag { 102 | // 未完全实现规则集合,避免在运行时抛出panic,在初始化时抛出 103 | panic("implement check failed") 104 | } 105 | 106 | return impl 107 | } 108 | -------------------------------------------------------------------------------- /entity/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/model/context" 7 | "testing" 8 | ) 9 | 10 | func BenchmarkTestCostEquals(b *testing.B) { 11 | tt := struct { 12 | name string 13 | origin map[enum.ElementType]uint 14 | cost map[enum.ElementType]uint 15 | want bool 16 | }{ 17 | name: "ElementSetEquals-Mixed-4", 18 | origin: map[enum.ElementType]uint{enum.ElementCryo: 2, enum.ElementDendro: 2, enum.ElementCurrency: 1}, 19 | cost: map[enum.ElementType]uint{enum.ElementCurrency: 3, enum.ElementNone: 2}, 20 | want: true, 21 | } 22 | originCost := model.NewCostFromMap(tt.origin) 23 | otherCost := model.NewCostFromMap(tt.cost) 24 | b.ResetTimer() 25 | for i := 0; i < b.N; i++ { 26 | if got := originCost.Equals(*otherCost); got != tt.want { 27 | b.Errorf("Error: %v, want %v", got, tt.want) 28 | } 29 | } 30 | } 31 | 32 | func BenchmarkTestPlayerChainNext(b *testing.B) { 33 | pc := newPlayerChain() 34 | for i := uint64(0); i < 100; i++ { 35 | pc.add(i) 36 | } 37 | 38 | b.ResetTimer() 39 | for i := 0; i < b.N; i++ { 40 | pc.next() 41 | } 42 | } 43 | 44 | func BenchmarkTestPlayerChainNextWithComplete(b *testing.B) { 45 | pc := newPlayerChain() 46 | for i := uint64(0); i < 11451419; i++ { 47 | pc.add(i) 48 | } 49 | 50 | b.ResetTimer() 51 | for i := 0; i < b.N; i++ { 52 | exist, _ := pc.next() 53 | pc.complete(uint64(i)) 54 | if !exist { 55 | break 56 | } 57 | } 58 | } 59 | 60 | func BenchmarkTestEventMapPreview(b *testing.B) { 61 | m := NewEventMap() 62 | for i := 0; i < 128; i++ { 63 | m.AddEvent(newTestEvent(uint64(i), enum.AfterAttack, true, func(ctx *context.CallbackContext) { ctx.SwitchCharacter(114514) })) 64 | } 65 | m.AddEvent(newTestEvent(uint64(1), enum.AfterSwitch, true, func(ctx *context.CallbackContext) { ctx.ChangeOperated(false) })) 66 | ctx := context.NewCallbackContext() 67 | b.ResetTimer() 68 | 69 | for i := 0; i < b.N; i++ { 70 | m.Preview(enum.AfterSwitch, ctx) 71 | } 72 | } 73 | 74 | func BenchmarkTestEventMapExecute(b *testing.B) { 75 | m := NewEventMap() 76 | for i := 0; i < 128; i++ { 77 | m.AddEvent(newTestEvent(uint64(i), enum.AfterAttack, true, func(ctx *context.CallbackContext) { ctx.SwitchCharacter(114514) })) 78 | } 79 | m.AddEvent(newTestEvent(uint64(1), enum.AfterSwitch, true, func(ctx *context.CallbackContext) { ctx.ChangeOperated(false) })) 80 | ctx := context.NewCallbackContext() 81 | b.ResetTimer() 82 | 83 | for i := 0; i < b.N; i++ { 84 | m.Call(enum.AfterSwitch, ctx) 85 | } 86 | } 87 | 88 | func BenchmarkTestEventMapAdd(b *testing.B) { 89 | m := NewEventMap() 90 | b.ResetTimer() 91 | 92 | for i := 0; i < b.N; i++ { 93 | m.AddEvent(newTestEvent(uint64(1), enum.AfterSwitch, true, func(ctx *context.CallbackContext) { ctx.ChangeOperated(false) })) 94 | } 95 | } 96 | 97 | func BenchmarkTestEventMapRemove(b *testing.B) { 98 | m := NewEventMap() 99 | event := newTestEvent(uint64(1), enum.AfterSwitch, true, func(ctx *context.CallbackContext) { ctx.ChangeOperated(false) }) 100 | m.AddEvent(event) 101 | b.ResetTimer() 102 | 103 | for i := 0; i < b.N; i++ { 104 | m.RemoveEvent(event) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /docs/development/api/localization.md: -------------------------------------------------------------------------------- 1 | 此处为GISB的本地化相关服务,包含语言包的获取、特定词语的翻译等接口 2 | 3 | # 变量与参数 4 | 5 | + `:language_pack_id`/`{language_pack_id}`: 您要获取的语言包的ID,字符串类型,由mod提供者注册 6 | 7 | # 说明 8 | 9 | 本接口使用了硬编码确定语言的ID,其定义如下,从上到下值递增,从0开始: 10 | 11 | ```go 12 | type Language uint 13 | 14 | const ( 15 | ChineseSimplified Language = iota // ChineseSimplified 简体中文 16 | ChineseTraditional // ChineseTraditional 繁體中文 17 | English // English 18 | French // French Français 19 | German // German Deutsch 20 | Japanese // Japanese 日本語 21 | Korean // Korean 한국어 22 | Russian // Russian Русский язык 23 | Unknown // Unknown ያንሽቤ 24 | ) 25 | ``` 26 | 27 | 本文档的所有接口均不需要鉴权 28 | 29 | # 获取语言包 30 | 31 | ## 请求方法与地址 32 | 33 | ```plain text 34 | GET {BaseURL}/localization/language_pack/:language_pack_id 35 | ``` 36 | 37 | ## 请求/响应 38 | 39 | 40 | 41 | 42 | 43 | ### 请求 44 | 45 | 本接口不需要提供请求体 46 | 47 | #### 请求示例 48 | 49 | ```http 50 | GET /localization/language_pack/:language_pack_id HTTP/1.1 51 | Host: {BaseURL} 52 | ``` 53 | 54 | 55 | 56 | ### 响应 57 | 58 | 此接口将根据您给定的语言包ID,查询并返回语言包 59 | 60 | #### 响应体定义 61 | 62 | ```go 63 | type LocalizationQueryResponse struct { 64 | LanguagePack struct{ 65 | Languages map[uint]map[string]string `json:"languages"` 66 | SupportedLanguages []enum.Language `json:"supported_languages"` 67 | } `json:"language_pack"` 68 | } 69 | ``` 70 | 71 | #### 响应示例 72 | 73 | ```json 74 | { 75 | "language_pack": { 76 | "languages": { 77 | "0": { 78 | "keqing": "刻晴", 79 | "zhongli": "钟离" 80 | }, 81 | "3": { 82 | "keqing": "keqing", 83 | "zhongli": "zhongli" 84 | } 85 | }, 86 | "supported_languages": [ 87 | 3, 88 | 0 89 | ] 90 | } 91 | } 92 | ``` 93 | 94 | 95 | 96 | # 翻译给定词语 97 | 98 | ## 请求方法与地址 99 | 100 | ```plain text 101 | GET {BaseURL}/localization/translate?target_language={target_language}&language_package={language_pack_id} 102 | ``` 103 | 104 | ## 查询参数 105 | 106 | + `target_language`: 目标语言 107 | + `language_package`: 词语所在的翻译包ID 108 | 109 | ## 请求/响应 110 | 111 | 112 | 113 | 114 | 115 | ### 请求 116 | 117 | 您需要提供需要查询的词语 118 | 119 | #### 请求体定义 120 | 121 | ```go 122 | type TranslationRequest struct { 123 | Words []string `json:"words"` 124 | } 125 | ``` 126 | 127 | #### 请求示例 128 | 129 | ```http 130 | GET /localization/translate?target_language={target_language}&language_package={language_pack_id} HTTP/1.1 131 | Host: {BaseURL} 132 | Content-Type: application/json 133 | Content-Length: 40 134 | 135 | { 136 | "words": ["keqing", "zhongli"] 137 | } 138 | ``` 139 | 140 | 141 | 142 | ### 响应 143 | 144 | 此接口将根据您给定的词语与要求的语言包,尝试将其翻译为您指定的语言 145 | 146 | #### 响应体定义 147 | 148 | ```go 149 | type TranslationResponse struct { 150 | Translation map[string]string `json:"translation"` 151 | } 152 | ``` 153 | 154 | #### 响应示例 155 | 156 | ```json 157 | { 158 | "translation": { 159 | "keqing": "刻晴", 160 | "zhongli": "钟离" 161 | } 162 | } 163 | ``` 164 | 165 | -------------------------------------------------------------------------------- /mod/implement/skill.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 6 | ) 7 | 8 | type SkillImpl struct { 9 | EntityImpl 10 | skillType enum.SkillType 11 | } 12 | 13 | func (impl *SkillImpl) SkillType() enum.SkillType { 14 | return impl.skillType 15 | } 16 | 17 | type SkillOptions func(option *SkillImpl) 18 | 19 | func WithSkillID(id uint16) SkillOptions { 20 | return func(option *SkillImpl) { 21 | option.InjectTypeID(uint64(id)) 22 | } 23 | } 24 | 25 | func WithSkillType(skillType enum.SkillType) SkillOptions { 26 | return func(option *SkillImpl) { 27 | option.skillType = skillType 28 | } 29 | } 30 | 31 | func NewSkillWithOpts(options ...SkillOptions) definition.Skill { 32 | impl := &SkillImpl{} 33 | for _, option := range options { 34 | option(impl) 35 | } 36 | 37 | return impl 38 | } 39 | 40 | type AttackSkillImpl struct { 41 | SkillImpl 42 | skillCost map[enum.ElementType]uint 43 | activeDamage func(ctx definition.Context) definition.Damage 44 | backgroundDamage func(ctx definition.Context) definition.Damage 45 | } 46 | 47 | func (impl *AttackSkillImpl) SkillCost() map[enum.ElementType]uint { 48 | return impl.skillCost 49 | } 50 | 51 | func (impl *AttackSkillImpl) ActiveDamage(ctx definition.Context) definition.Damage { 52 | return impl.activeDamage(ctx) 53 | } 54 | 55 | func (impl *AttackSkillImpl) BackgroundDamage(ctx definition.Context) definition.Damage { 56 | return impl.backgroundDamage(ctx) 57 | } 58 | 59 | type AttackSkillOptions func(option *AttackSkillImpl) 60 | 61 | func WithAttackSkillID(id uint16) AttackSkillOptions { 62 | return func(option *AttackSkillImpl) { 63 | opt := WithSkillID(id) 64 | opt(&option.SkillImpl) 65 | } 66 | } 67 | 68 | func WithAttackSkillType(skillType enum.SkillType) AttackSkillOptions { 69 | return func(option *AttackSkillImpl) { 70 | opt := WithSkillType(skillType) 71 | opt(&option.SkillImpl) 72 | } 73 | } 74 | 75 | func WithAttackSkillCost(skillCost map[enum.ElementType]uint) AttackSkillOptions { 76 | return func(option *AttackSkillImpl) { 77 | option.skillCost = skillCost 78 | } 79 | } 80 | 81 | func WithAttackSkillActiveDamageHandler(handler func(ctx definition.Context) (elementType enum.ElementType, damageAmount uint)) AttackSkillOptions { 82 | return func(option *AttackSkillImpl) { 83 | option.activeDamage = func(ctx definition.Context) definition.Damage { 84 | elementType, damageAmount := handler(ctx) 85 | return NewDamageWithOpts( 86 | WithDamageElementType(elementType), 87 | WithDamageAmount(damageAmount), 88 | ) 89 | } 90 | } 91 | } 92 | 93 | func WithAttackSkillBackgroundDamageHandler(handler func(ctx definition.Context) (damageAmount uint)) AttackSkillOptions { 94 | return func(option *AttackSkillImpl) { 95 | option.backgroundDamage = func(ctx definition.Context) definition.Damage { 96 | damageAmount := handler(ctx) 97 | return NewDamageWithOpts( 98 | WithDamageElementType(enum.ElementNone), 99 | WithDamageAmount(damageAmount), 100 | ) 101 | } 102 | } 103 | } 104 | 105 | func NewAttackSkillWithOpts(options ...AttackSkillOptions) definition.AttackSkill { 106 | impl := &AttackSkillImpl{} 107 | for _, option := range options { 108 | option(impl) 109 | } 110 | 111 | return impl 112 | } 113 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/exec/backend" 7 | "github.com/sunist-c/genius-invokation-simulator-backend/exec/cli" 8 | "github.com/sunist-c/genius-invokation-simulator-backend/persistence" 9 | "os" 10 | "os/signal" 11 | "path" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | const ( 17 | aiMode = "ai" 18 | cliMode = "cli" 19 | backendMode = "backend" 20 | ) 21 | 22 | var ( 23 | args = &argument{ 24 | mode: new(string), 25 | port: new(uint), 26 | conf: new(string), 27 | save: new(bool), 28 | flush: new(uint), 29 | storage: new(string), 30 | } 31 | sig = make(chan os.Signal, 4) 32 | ) 33 | 34 | type argument struct { 35 | mode *string 36 | conf *string 37 | port *uint 38 | save *bool 39 | flush *uint 40 | storage *string 41 | } 42 | 43 | func initArgs() { 44 | flag.StringVar(args.conf, "conf", "", "setup the backend configuration file, highest priority") 45 | flag.StringVar(args.mode, "mode", "backend", "setup the startup mode, available [backend, cli, ai]") 46 | flag.UintVar(args.port, "port", 8086, "setup the http protocol port") 47 | flag.BoolVar(args.save, "save", true, "setup if to enable the persistence module") 48 | flag.StringVar(args.storage, "storage", path.Join(os.Args[0], "../data/persistence"), "setup the persistence storage filepath") 49 | flag.UintVar(args.flush, "flush", 3600, "setup the flush frequency(second) of persistence module") 50 | } 51 | 52 | func callQuit() { 53 | fmt.Printf("[main.log] main.callQuit(): daemon running\n") 54 | s := <-sig 55 | fmt.Printf("[main.log] main.callQuit(): quit signal received %v\n", s.String()) 56 | fmt.Printf("[main.log] main.callQuit(): quiting persistence\n") 57 | persistence.Quit() 58 | fmt.Printf("[main.log] main.callQuit(): quited persistence\n") 59 | 60 | switch *args.mode { 61 | case backendMode: 62 | backend.Quit() 63 | case cliMode: 64 | cli.Quit() 65 | case aiMode: 66 | panic("not implemented yet") 67 | } 68 | 69 | fmt.Printf("[main.log] main.callQuit(): wait 10 seconds for quit task\n") 70 | time.Sleep(time.Second * 1) 71 | os.Exit(114) 72 | } 73 | 74 | func init() { 75 | errChan := make(chan error, 10) 76 | fmt.Printf("[main.log] main.init(): initializing main package\n") 77 | signal.Notify(sig, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGHUP) 78 | fmt.Printf("[main.log] main.init(): setuped signal channel\n") 79 | initArgs() 80 | flag.Parse() 81 | fmt.Printf("[main.log] main.init(): parsed command line arguments\n") 82 | if err := persistence.SetStoragePath(*args.storage); err != nil { 83 | errChan <- err 84 | panic(err) 85 | } 86 | fmt.Printf("[main.log] main.init(): initialized storage path\n") 87 | persistence.Load(errChan) 88 | fmt.Printf("[main.log] main.init(): initializing persistent storage\n") 89 | go func() { 90 | fmt.Printf("[main.log] main.errorHandler(): error handler running\n") 91 | for err := range errChan { 92 | fmt.Println(err) 93 | } 94 | }() 95 | persistence.Serve(time.Second*time.Duration(*args.flush), errChan) 96 | fmt.Printf("[main.log] main.init(): initialize completed\n") 97 | } 98 | 99 | func main() { 100 | go callQuit() 101 | switch *args.mode { 102 | case backendMode: 103 | backend.Run(*args.port) 104 | case cliMode: 105 | cli.Run() 106 | case aiMode: 107 | panic("not implemented yet") 108 | default: 109 | os.Exit(0) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /entity/eventmap.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/model/context" 7 | ) 8 | 9 | type Map struct { 10 | sets map[enum.TriggerType]*set 11 | } 12 | 13 | // Call 调用某种类型的Event 14 | func (m *Map) Call(triggerType enum.TriggerType, ctx *context.CallbackContext) { 15 | if set, exist := m.sets[triggerType]; exist { 16 | set.call(ctx) 17 | } 18 | } 19 | 20 | // Preview 预览某种类型的Event 21 | func (m Map) Preview(triggerType enum.TriggerType, ctx *context.CallbackContext) { 22 | if set, exist := m.sets[triggerType]; exist { 23 | set.preview(ctx) 24 | } 25 | } 26 | 27 | // AddEvent 添加一个Event 28 | func (m *Map) AddEvent(event model.Event) { 29 | if set, exist := m.sets[event.TriggerAt()]; !exist || set == nil { 30 | set = newEventSet() 31 | set.add(event) 32 | m.sets[event.TriggerAt()] = set 33 | } else { 34 | m.sets[event.TriggerAt()].add(event) 35 | } 36 | } 37 | 38 | // RemoveEvent 移除一个Event 39 | func (m *Map) RemoveEvent(event model.Event) { 40 | if set, exist := m.sets[event.TriggerAt()]; exist && set != nil { 41 | set.remove(event.TypeID()) 42 | } 43 | } 44 | 45 | // AddEvents 添加多个Event,只有类型为filter的Event会生效 46 | func (m *Map) AddEvents(filter enum.TriggerType, events []model.Event) { 47 | if set, exist := m.sets[filter]; !exist || set == nil { 48 | set = newEventSet() 49 | for _, event := range events { 50 | if event.TriggerAt() == filter { 51 | set.add(event) 52 | } 53 | } 54 | m.sets[filter] = set 55 | } else { 56 | for _, event := range events { 57 | if event.TriggerAt() == filter { 58 | set.add(event) 59 | } 60 | } 61 | } 62 | } 63 | 64 | // RemoveEvents 移除类型为filter的Event 65 | func (m *Map) RemoveEvents(filter enum.TriggerType) { 66 | delete(m.sets, filter) 67 | } 68 | 69 | func (m Map) Expose(trigger enum.TriggerType) (events []uint64) { 70 | if set, exist := m.sets[trigger]; !exist { 71 | return []uint64{} 72 | } else { 73 | events = []uint64{} 74 | for id := range set.events { 75 | events = append(events, id) 76 | } 77 | return events 78 | } 79 | } 80 | 81 | func NewEventMap() *Map { 82 | return &Map{sets: map[enum.TriggerType]*set{}} 83 | } 84 | 85 | type set struct { 86 | events map[uint64]model.Event 87 | } 88 | 89 | // call 调用EventSet中的所有Event,并在调用完成后清理需要清理的Event 90 | func (s *set) call(ctx *context.CallbackContext) { 91 | for _, e := range s.events { 92 | if e.CanTriggered(*ctx) { 93 | e.Callback(ctx) 94 | delete(s.events, e.TypeID()) 95 | } 96 | } 97 | } 98 | 99 | // preview 调用EventSet中的所有Event,但调用完成后不清理调用过的Event 100 | func (s set) preview(ctx *context.CallbackContext) { 101 | for _, e := range s.events { 102 | if e.CanTriggered(*ctx) { 103 | e.Callback(ctx) 104 | } 105 | } 106 | } 107 | 108 | // append 合并两个EventSet,若新Set中有同id的Event,则会覆盖现有Event 109 | func (s *set) append(another *set) { 110 | for id, event := range another.events { 111 | s.events[id] = event 112 | } 113 | } 114 | 115 | // add 向EventSet中加入一个Event 116 | func (s *set) add(event model.Event) { 117 | s.events[event.TypeID()] = event 118 | } 119 | 120 | // remove 从EventSet中移除一个指定id的Event 121 | func (s *set) remove(id uint64) { 122 | delete(s.events, id) 123 | } 124 | 125 | // newEventSet 创建一个空EventSet 126 | func newEventSet() *set { 127 | return &set{events: map[uint64]model.Event{}} 128 | } 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### VisualStudioCode template 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | *.code-workspace 8 | 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | ### JetBrains template 13 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 14 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 15 | 16 | # User-specific stuff 17 | .idea/**/workspace.xml 18 | .idea/**/tasks.xml 19 | .idea/**/usage.statistics.xml 20 | .idea/**/dictionaries 21 | .idea/**/shelf 22 | 23 | # Generated files 24 | .idea/**/contentModel.xml 25 | 26 | # Sensitive or high-churn files 27 | .idea/**/dataSources/ 28 | .idea/**/dataSources.ids 29 | .idea/**/dataSources.local.xml 30 | .idea/**/sqlDataSources.xml 31 | .idea/**/dynamic.xml 32 | .idea/**/uiDesigner.xml 33 | .idea/**/dbnavigator.xml 34 | 35 | # Gradle 36 | .idea/**/gradle.xml 37 | .idea/**/libraries 38 | 39 | # Gradle and Maven with auto-import 40 | # When using Gradle or Maven with auto-import, you should exclude module files, 41 | # since they will be recreated, and may cause churn. Uncomment if using 42 | # auto-import. 43 | # .idea/artifacts 44 | # .idea/compiler.xml 45 | # .idea/jarRepositories.xml 46 | # .idea/modules.xml 47 | # .idea/*.iml 48 | # .idea/modules 49 | # *.iml 50 | # *.ipr 51 | 52 | # CMake 53 | cmake-build-*/ 54 | 55 | # Mongo Explorer plugin 56 | .idea/**/mongoSettings.xml 57 | 58 | # File-based project format 59 | *.iws 60 | 61 | # IntelliJ 62 | out/ 63 | 64 | # mpeltonen/sbt-idea plugin 65 | .idea_modules/ 66 | 67 | # JIRA plugin 68 | atlassian-ide-plugin.xml 69 | 70 | # Cursive Clojure plugin 71 | .idea/replstate.xml 72 | 73 | # Crashlytics plugin (for Android Studio and IntelliJ) 74 | com_crashlytics_export_strings.xml 75 | crashlytics.properties 76 | crashlytics-build.properties 77 | fabric.properties 78 | 79 | # Editor-based Rest Client 80 | .idea/httpRequests 81 | 82 | # Android studio 3.1+ serialized cache file 83 | .idea/caches/build_file_checksums.ser 84 | 85 | ### Go template 86 | # Binaries for programs and plugins 87 | *.exe 88 | *.exe~ 89 | *.dll 90 | *.so 91 | *.dylib 92 | 93 | # Test binary, built with `go test -c` 94 | *.test 95 | 96 | # Output of the go coverage tool, specifically when used with LiteIDE 97 | *.out 98 | 99 | # Dependency directories (remove the comment below to include it) 100 | # vendor/ 101 | 102 | ### Windows template 103 | # Windows thumbnail cache files 104 | Thumbs.db 105 | Thumbs.db:encryptable 106 | ehthumbs.db 107 | ehthumbs_vista.db 108 | 109 | # Dump file 110 | *.stackdump 111 | 112 | # Folder config file 113 | [Dd]esktop.ini 114 | 115 | # Recycle Bin used on file shares 116 | $RECYCLE.BIN/ 117 | 118 | # Windows Installer files 119 | *.cab 120 | *.msi 121 | *.msix 122 | *.msm 123 | *.msp 124 | 125 | # Windows shortcuts 126 | *.lnk 127 | 128 | ### macOS template 129 | # General 130 | .DS_Store 131 | .AppleDouble 132 | .LSOverride 133 | 134 | # Icon must end with two \r 135 | Icon 136 | 137 | # Thumbnails 138 | ._* 139 | 140 | # Files that might appear in the root of a volume 141 | .DocumentRevisions-V100 142 | .fseventsd 143 | .Spotlight-V100 144 | .TemporaryItems 145 | .Trashes 146 | .VolumeIcon.icns 147 | .com.apple.timemachine.donotpresent 148 | 149 | # Directories potentially created on remote AFP share 150 | .AppleDB 151 | .AppleDesktop 152 | Network Trash Folder 153 | Temporary Items 154 | .apdisk 155 | 156 | # Full idea configuration 157 | .idea 158 | 159 | # go.sum 160 | go.sum 161 | 162 | # metadata 163 | /**/metadata.yml -------------------------------------------------------------------------------- /protocol/websocket/engine.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/gorilla/websocket" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/protocol/websocket/config" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func checkOriginHandler() func(r *http.Request) bool { 12 | conf := config.GetConfig() 13 | if conf.AllowCrossOrigin { 14 | return func(r *http.Request) bool { 15 | return true 16 | } 17 | } else { 18 | return func(r *http.Request) bool { 19 | // todo: optimize check origin logic 20 | origin := r.Header.Get("Origin") 21 | for _, allow := range conf.AllowOriginDomains { 22 | if origin == allow { 23 | return true 24 | } 25 | } 26 | 27 | return false 28 | } 29 | } 30 | } 31 | 32 | func newUpgrader() *websocket.Upgrader { 33 | conf := config.GetConfig() 34 | return &websocket.Upgrader{ 35 | HandshakeTimeout: time.Second * time.Duration(conf.HandshakeTimeout), 36 | ReadBufferSize: int(conf.WebsocketReaderBufferSize), 37 | WriteBufferSize: int(conf.WebsocketWriterBufferSize), 38 | CheckOrigin: checkOriginHandler(), 39 | } 40 | } 41 | 42 | type Connection struct { 43 | conn *websocket.Conn 44 | exitChan chan struct{} 45 | errChan chan error 46 | iStream chan []byte // iStream 输入管道 47 | oStream chan []byte // oStream 输出管道 48 | } 49 | 50 | func (c *Connection) readLoop() { 51 | for { 52 | select { 53 | case <-c.exitChan: 54 | // 向exitChan写入信息通知其他协程 55 | c.exitChan <- struct{}{} 56 | return 57 | default: 58 | if messageType, reader, err := c.conn.ReadMessage(); err != nil { 59 | // 发生错误,关闭WebSocket连接 60 | c.errChan <- err 61 | c.Close() 62 | } else if messageType == websocket.TextMessage { 63 | // 传输格式为json,只监听TextMessage 64 | c.iStream <- reader 65 | } 66 | } 67 | } 68 | } 69 | 70 | func (c *Connection) writeLoop() { 71 | for { 72 | select { 73 | case <-c.exitChan: 74 | // 向exitChan写入信息通知其他协程 75 | c.exitChan <- struct{}{} 76 | return 77 | case outMessage := <-c.oStream: 78 | if err := c.conn.WriteMessage(websocket.TextMessage, outMessage); err != nil { 79 | // 发生错误,关闭WebSocket连接 80 | c.errChan <- err 81 | c.Close() 82 | } 83 | } 84 | } 85 | } 86 | 87 | // Serve 启动WebsocketConnection的服务协程 88 | func (c *Connection) Serve() { 89 | go c.readLoop() 90 | go c.writeLoop() 91 | } 92 | 93 | func (c *Connection) Close() { 94 | // 关闭客户端连接 95 | if err := c.conn.Close(); err != nil { 96 | c.errChan <- err 97 | } 98 | 99 | // 通知readLoop和writeLoop退出 100 | c.exitChan <- struct{}{} 101 | 102 | // 释放WebSocketConnection 103 | c.conn = nil 104 | close(c.iStream) 105 | close(c.oStream) 106 | close(c.exitChan) 107 | } 108 | 109 | func (c *Connection) Write(jsonBytes []byte) { 110 | c.oStream <- jsonBytes 111 | } 112 | 113 | func (c *Connection) Read() <-chan []byte { 114 | return c.iStream 115 | } 116 | 117 | func NewConnection(conn *websocket.Conn, errChan chan error) *Connection { 118 | conf := config.GetConfig() 119 | return &Connection{ 120 | conn: conn, 121 | iStream: make(chan []byte, conf.ServerMessageBufferSize), 122 | oStream: make(chan []byte, conf.ServerMessageBufferSize), 123 | exitChan: make(chan struct{}, 2), 124 | errChan: errChan, 125 | } 126 | } 127 | 128 | // Upgrade 将HTTP连接升级为Websocket连接 129 | func Upgrade(ctx *gin.Context, errChan chan error) (success bool, connection *Connection) { 130 | if conn, err := newUpgrader().Upgrade(ctx.Writer, ctx.Request, ctx.Request.Header); err != nil { 131 | return false, nil 132 | } else { 133 | return true, NewConnection(conn, errChan) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 1. 综述 2 | 3 | ## 1.1 概况 4 | 5 | 这里是原神(Genshin Impact)的《七圣召唤》模拟器,是参考原神3.3版本的「七圣召唤」玩法重新实现的后端(服务端),包括所有的原神内的游戏内容,并拓展一些米哈游没有做的内容。 6 | 7 | ## 1.2 声明 8 | 9 | 本项目的交流群(QQ)为`530924402`,欢迎讨论与PR 10 | 11 | 本模拟器侧重于提供自定义的七圣召唤对局,比如每次投十二个骰子/每回合摸四张牌/加入自定义角色、卡牌等功能,~~短期内没有~~ 项目稳定后尽快针对ai训练进行优化、适配。 12 | 考虑设计接口时兼容RLcard 13 | 14 | 相关的[genius-invokation-gym](https://github.com/paladin1013/genius-invokation-gym)项目侧重于提供ai相关接口,请根据需求选择 15 | 16 | **本模拟器接口尽量和[genius-invokation-gym](https://github.com/paladin1013/genius-invokation-gym)保持一致,其项目完善后本项目也尽量拓展相应的ai接口** 17 | 18 | 同时感谢[@Leng Yue](https://github.com/leng-yue)实现的前端项目[genius-invokation-webui](https://github.com/leng-yue/genius-invokation-webui) 19 | 20 | > **本项目不含任何原神(Genshin Impact)的美术素材,原神(Genshin Impact)的相关版权归米哈游(miHoYo)所有** 21 | 22 | ## 1.3 技术栈与组件 23 | 24 | + golang 1.19: 这是本项目的编码语言与运行环境 25 | + mingw-gcc/clang/gcc: 因为内嵌sqlite3产生的cgo编译需求 26 | + sqlite3: 这是本项目用于存储玩家信息、玩家卡组信息的持久化组件,已内嵌,无需下载 27 | 28 | # 2. 徽章 29 | 30 | ## 2.1 构建情况 31 | 32 | | Branch | master | dev | release | 33 | | :--: | :--: | :--: | :--: | 34 | | drone-ci | [![Build Status](https://drone.sunist.cn/api/badges/sunist-c/genius-invokation-simulator-backend/status.svg?ref=refs/heads/master)](https://drone.sunist.cn/sunist-c/genius-invokation-simulator-backend) | [![Build Status](https://drone.sunist.cn/api/badges/sunist-c/genius-invokation-simulator-backend/status.svg?ref=refs/heads/dev)](https://drone.sunist.cn/sunist-c/genius-invokation-simulator-backend) | nil | 35 | | github-action | [![gisb-test](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml) | [![gisb-test](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml/badge.svg?branch=dev)](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml) | nil | 36 | 37 | ## 2.2 代码质量 38 | 39 | [![Go Report Card](https://goreportcard.com/badge/github.com/sunist-c/genius-invokation-simulator-backend)](https://goreportcard.com/report/github.com/sunist-c/genius-invokation-simulator-backend) 40 | 41 | # 3. 分支说明 42 | 43 | - master 主要的分支,将在发生重要功能修改时与dev分支进行同步 44 | - dev 开发中的分支,将频繁地进行更改与新功能测试 45 | - release 稳定的分支,仅会在重大版本更新时进行功能合并 46 | 47 | # 4. 开发进度 48 | 49 | 请转到[gisb's feature development](https://github.com/users/sunist-c/projects/2)查看项目进度 50 | 51 | # 5. 参与项目 52 | 53 | 如果您想增加一个功能或想法: 54 | 55 | 1. 加入本项目的交流群或[genius-invokation-gym](https://github.com/paladin1013/genius-invokation-gym)的交流群或在本项目的[Discussion/Ideas](https://github.com/sunist-c/genius-invokation-simulator-backend/discussions/categories/ideas)中分享您的想法 56 | 2. 在取得Contributor的广泛认可后将为您创建一个WIP的issue 57 | 3. 按照正常的流程fork->coding->pull request您的修改 58 | 59 | 如果您想为项目贡献代码,您可以转到[gisb's feature development](https://github.com/users/sunist-c/projects/2)查看这个项目目前在干什么,标有`help wanted`的内容可能需要一些帮助,处于`design`阶段的内容目前还没有进行开发,您可以直接在GitHub Projects/Project Issue页面与我们交流 60 | 61 | 本项目有意向从Jetbrains申请开源项目的All Products License,将会提供给Code Contributors 62 | 63 | # 6. 功能特性 64 | 65 | - [x] 游戏基本玩法 66 | - [ ] 元素反应 67 | - [x] 角色与技能 68 | - [ ] 圣遗物与武器 69 | - [ ] 场景和伙伴 70 | - [ ] 召唤物 71 | - [x] 卡牌与元素转化 72 | - [ ] 游戏拓展玩法 73 | - [x] 多人游戏(玩家`N>=2`,队伍`N>=2`) 74 | - [ ] 创建对局 75 | - [ ] 匹配对局 76 | - [ ] 游戏内通信 77 | - [ ] 观战模式 78 | - [ ] 比赛模式 79 | - [ ] 作战记录 80 | - [x] 自定义Mod支持 81 | - [x] Go/Lua支持 82 | - [x] 自定义角色与卡牌 83 | - [x] 自定义规则 84 | - [x] 多种通信协议连接 85 | - [x] websocket接口 86 | - [x] http/https接口 87 | - [ ] udp/kcp接口 88 | - [ ] rpc接口 89 | - [ ] 分布式支持 90 | - [ ] 自动化部署 91 | - [ ] 服务注册与服务发现 92 | - [ ] 服务端负载均衡 93 | - [ ] 管理功能 94 | - [ ] 公告与通知 95 | - [ ] IP追踪/封禁 96 | - [ ] QPS/TPS限制器 97 | -------------------------------------------------------------------------------- /entity/model/cost.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 8 | ) 9 | 10 | var ( 11 | random = rand.New(rand.NewSource(time.Now().UnixMilli())) 12 | ) 13 | 14 | func init() { 15 | random.Seed(time.Now().UnixNano()) 16 | } 17 | 18 | type Cost struct { 19 | costs map[enum.ElementType]uint 20 | total uint 21 | } 22 | 23 | // sub 从Cost中减少amount个element类型的元素骰子 24 | func (c *Cost) sub(element enum.ElementType, amount uint) { 25 | if c.costs[element] >= amount { 26 | c.total -= amount 27 | c.costs[element] -= amount 28 | } else { 29 | c.total -= c.costs[element] 30 | c.costs[element] = 0 31 | } 32 | } 33 | 34 | // add 向Cost中增加amount个element类型的元素骰子 35 | func (c *Cost) add(element enum.ElementType, amount uint) { 36 | c.total += amount 37 | c.costs[element] += amount 38 | } 39 | 40 | // Pay 从Cost中减去other中的元素骰子,不含等价判断 41 | func (c *Cost) Pay(other Cost) { 42 | for element, amount := range other.costs { 43 | c.sub(element, amount) 44 | } 45 | } 46 | 47 | // Add 想Cost中增加other中的所有元素骰子,不含等价判断 48 | func (c *Cost) Add(other Cost) { 49 | for element, amount := range other.costs { 50 | c.add(element, amount) 51 | } 52 | } 53 | 54 | // Contains 判断Cost中是否包含other中的所有元素,含等价判断 55 | func (c Cost) Contains(other Cost) bool { 56 | // 判断费用总数 57 | if other.total > c.total { 58 | return false 59 | } 60 | 61 | // 先减去确定类型的费用 62 | for element := enum.ElementStartIndex; element <= enum.ElementEndIndex; element++ { 63 | if other.costs[element] > c.costs[element]+c.costs[enum.ElementCurrency] { 64 | return false 65 | } else { 66 | if other.costs[element] > c.costs[element] { 67 | c.costs[enum.ElementCurrency] -= other.costs[element] - c.costs[element] 68 | c.costs[element] = 0 69 | } else { 70 | c.costs[element] -= other.costs[element] 71 | } 72 | } 73 | } 74 | 75 | // 判断是否满足同色元素要求 76 | for element := enum.ElementStartIndex; element <= enum.ElementEndIndex; element++ { 77 | if other.costs[enum.ElementSame] <= c.costs[element]+c.costs[enum.ElementCurrency] && element != enum.ElementCurrency { 78 | return true 79 | } 80 | } 81 | 82 | return false 83 | } 84 | 85 | // Equals 判断Cost中的元素是否和other中的元素完全等价 86 | func (c Cost) Equals(other Cost) bool { 87 | return c.Contains(other) && c.total == other.total 88 | } 89 | 90 | func (c Cost) Costs() map[enum.ElementType]uint { 91 | return c.costs 92 | } 93 | 94 | func (c Cost) Total() uint { 95 | return c.total 96 | } 97 | 98 | // NewCost 创建一个空Cost 99 | func NewCost() *Cost { 100 | return &Cost{ 101 | costs: map[enum.ElementType]uint{}, 102 | total: 0, 103 | } 104 | } 105 | 106 | // NewCostFromMap 从一个map创建Cost 107 | func NewCostFromMap(m map[enum.ElementType]uint) *Cost { 108 | result := &Cost{ 109 | costs: map[enum.ElementType]uint{}, 110 | total: 0, 111 | } 112 | 113 | for elementType, amount := range m { 114 | result.costs[elementType] = amount 115 | result.total += amount 116 | } 117 | 118 | return result 119 | } 120 | 121 | // NewRandomCost 创建一个随机费用的Cost 122 | func NewRandomCost(elementAmount uint) *Cost { 123 | costMap := map[enum.ElementType]uint{} 124 | for elementAmount > 0 { 125 | if elementAmount <= 21 { 126 | source := random.Uint64() 127 | for i := uint(0); i < elementAmount; i++ { 128 | element := enum.ElementType(source % 8) 129 | costMap[element] += 1 130 | source = source >> 3 131 | } 132 | elementAmount = 0 133 | } else { 134 | source := random.Uint64() 135 | for i := 0; i < 21; i++ { 136 | element := enum.ElementType(source % 8) 137 | costMap[element] += 1 138 | source = source >> 3 139 | } 140 | elementAmount -= 21 141 | } 142 | } 143 | return NewCostFromMap(costMap) 144 | } 145 | -------------------------------------------------------------------------------- /model/context/callback.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/model/kv" 6 | ) 7 | 8 | type CallbackContext struct { 9 | changeElements kv.Pair[bool, *CostContext] 10 | changeCharge kv.Pair[bool, *ChargeContext] 11 | changeModifiers kv.Pair[bool, *ModifierContext] 12 | attachElement kv.Pair[bool, map[uint64]enum.ElementType] 13 | getCards kv.Pair[bool, uint] 14 | findCard kv.Pair[bool, enum.CardType] 15 | switchCharacter kv.Pair[bool, uint64] 16 | operated kv.Pair[bool, bool] 17 | } 18 | 19 | func (c *CallbackContext) ChangeElements(f func(ctx *CostContext)) { 20 | if !c.changeElements.Key() { 21 | c.changeElements.SetKey(true) 22 | } 23 | f(c.changeElements.Value()) 24 | } 25 | 26 | func (c *CallbackContext) ChangeCharge(f func(ctx *ChargeContext)) { 27 | if !c.changeCharge.Key() { 28 | c.changeCharge.SetKey(true) 29 | } 30 | f(c.changeCharge.Value()) 31 | } 32 | 33 | func (c *CallbackContext) ChangeModifiers(f func(ctx *ModifierContext)) { 34 | if !c.changeModifiers.Key() { 35 | c.changeModifiers.SetKey(true) 36 | } 37 | f(c.changeModifiers.Value()) 38 | } 39 | 40 | func (c *CallbackContext) AttachElement(target uint64, element enum.ElementType) { 41 | if !c.attachElement.Key() { 42 | c.attachElement.SetKey(true) 43 | } 44 | value := c.attachElement.Value() 45 | value[target] = element 46 | c.attachElement.SetValue(value) 47 | } 48 | 49 | func (c *CallbackContext) GetCards(amount uint) { 50 | if !c.getCards.Key() { 51 | c.getCards.SetKey(true) 52 | } 53 | c.getCards.SetValue(amount) 54 | } 55 | 56 | func (c *CallbackContext) FindCard(cardType enum.CardType) { 57 | if !c.findCard.Key() { 58 | c.findCard.SetKey(true) 59 | } 60 | c.findCard.SetValue(cardType) 61 | } 62 | 63 | func (c *CallbackContext) SwitchCharacter(target uint64) { 64 | if !c.switchCharacter.Key() { 65 | c.switchCharacter.SetKey(true) 66 | } 67 | c.switchCharacter.SetValue(target) 68 | } 69 | 70 | func (c *CallbackContext) ChangeOperated(operated bool) { 71 | if !c.operated.Key() { 72 | c.operated.SetKey(true) 73 | } 74 | c.operated.SetValue(operated) 75 | } 76 | 77 | func (c CallbackContext) ChangeElementsResult() (changed bool, result *CostContext) { 78 | return c.changeElements.Key(), c.changeElements.Value() 79 | } 80 | 81 | func (c CallbackContext) ChangeChargeResult() (changed bool, result *ChargeContext) { 82 | return c.changeCharge.Key(), c.changeCharge.Value() 83 | } 84 | 85 | func (c CallbackContext) ChangeModifiersResult() (changed bool, result *ModifierContext) { 86 | return c.changeModifiers.Key(), c.changeModifiers.Value() 87 | } 88 | 89 | func (c CallbackContext) AttachElementResult() (changed bool, result map[uint64]enum.ElementType) { 90 | return c.attachElement.Key(), c.attachElement.Value() 91 | } 92 | 93 | func (c CallbackContext) GetCardsResult() (changed bool, result uint) { 94 | return c.getCards.Key(), c.getCards.Value() 95 | } 96 | 97 | func (c CallbackContext) GetFindCardResult() (find bool, cardType enum.CardType) { 98 | return c.findCard.Key(), c.findCard.Value() 99 | } 100 | 101 | func (c CallbackContext) SwitchCharacterResult() (switched bool, target uint64) { 102 | return c.switchCharacter.Key(), c.switchCharacter.Value() 103 | } 104 | 105 | func (c CallbackContext) ChangeOperatedResult() (switched, operated bool) { 106 | return c.operated.Key(), c.operated.Value() 107 | } 108 | 109 | func NewCallbackContext() *CallbackContext { 110 | return &CallbackContext{ 111 | changeElements: kv.NewPair(false, NewCostContext()), 112 | changeCharge: kv.NewPair(false, NewChargeContext()), 113 | changeModifiers: kv.NewPair(false, NewModifierContext()), 114 | attachElement: kv.NewPair(false, map[uint64]enum.ElementType{}), 115 | getCards: kv.NewPair(false, uint(0)), 116 | switchCharacter: kv.NewPair(false, uint64(0)), 117 | operated: kv.NewPair(false, false), 118 | findCard: kv.NewPair(false, enum.CardType(0)), 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /model/context/damage.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | ) 6 | 7 | type Damage struct { 8 | elementType enum.ElementType 9 | reaction enum.Reaction 10 | amount uint 11 | } 12 | 13 | func (d *Damage) add(amount uint) { 14 | d.amount += amount 15 | } 16 | 17 | func (d *Damage) sub(amount uint) { 18 | if d.amount > amount { 19 | d.amount -= amount 20 | } else { 21 | d.amount = 0 22 | } 23 | } 24 | 25 | func (d *Damage) change(element enum.ElementType) { 26 | d.elementType = element 27 | } 28 | 29 | // Amount 伤害的数值,只读 30 | func (d Damage) Amount() uint { 31 | return d.amount 32 | } 33 | 34 | // ElementType 伤害的元素类型,只读 35 | func (d Damage) ElementType() enum.ElementType { 36 | return d.elementType 37 | } 38 | 39 | // Reaction 伤害发生的反应,只读 40 | func (d Damage) Reaction() enum.Reaction { 41 | return d.reaction 42 | } 43 | 44 | type DamageContext struct { 45 | skillID uint64 46 | sendCharacter uint64 47 | targetCharacter uint64 48 | backgroundCharacters []uint64 49 | damages map[uint64]*Damage 50 | } 51 | 52 | // AddActiveDamage 增加对目标玩家前台角色的伤害数值 53 | func (d *DamageContext) AddActiveDamage(amount uint) { 54 | d.damages[d.targetCharacter].add(amount) 55 | } 56 | 57 | // AddPenetratedDamage 增加对目标玩家所有后台角色的穿透伤害数值 58 | func (d *DamageContext) AddPenetratedDamage(amount uint) { 59 | for _, character := range d.backgroundCharacters { 60 | d.damages[character].add(amount) 61 | } 62 | } 63 | 64 | // SubActiveDamage 降低对目标玩家前台角色的伤害数值 65 | func (d *DamageContext) SubActiveDamage(amount uint) { 66 | d.damages[d.targetCharacter].sub(amount) 67 | } 68 | 69 | // SubPenetratedDamage 降低对目标玩家所有后台角色的穿透伤害数值 70 | func (d *DamageContext) SubPenetratedDamage(amount uint) { 71 | for _, character := range d.backgroundCharacters { 72 | d.damages[character].sub(amount) 73 | } 74 | } 75 | 76 | // ChangeElementType 修改对目标玩家前台角色的伤害元素类型 77 | func (d *DamageContext) ChangeElementType(element enum.ElementType) { 78 | d.damages[d.targetCharacter].change(element) 79 | } 80 | 81 | // SetReaction 目标角色身上发生的元素反应类型,仅供框架使用 82 | func (d *DamageContext) SetReaction(targetCharacter uint64, reaction enum.Reaction) { 83 | d.damages[targetCharacter].reaction = reaction 84 | } 85 | 86 | // GetTargetCharacter 获取伤害的目标角色ID 87 | func (d DamageContext) GetTargetCharacter() uint64 { 88 | return d.targetCharacter 89 | } 90 | 91 | // GetBackgroundCharacters 获取伤害的目标后台角色ID 92 | func (d DamageContext) GetBackgroundCharacters() []uint64 { 93 | return d.backgroundCharacters 94 | } 95 | 96 | // GetTargetCharacterReaction 获取伤害的目标角色发生的元素反应 97 | func (d DamageContext) GetTargetCharacterReaction() enum.Reaction { 98 | return d.damages[d.targetCharacter].reaction 99 | } 100 | 101 | // Damage 返回DamageContext携带的伤害信息,只读 102 | func (d DamageContext) Damage() map[uint64]Damage { 103 | result := map[uint64]Damage{} 104 | for _, id := range d.backgroundCharacters { 105 | result[id] = Damage{elementType: enum.ElementNone, amount: 0} 106 | } 107 | 108 | for target, damage := range d.damages { 109 | result[target] = *damage 110 | } 111 | 112 | return result 113 | } 114 | 115 | // NewEmptyDamageContext 新建一个空的DamageContext 116 | func NewEmptyDamageContext(skill, from, target uint64, backgrounds []uint64) *DamageContext { 117 | return &DamageContext{ 118 | skillID: skill, 119 | sendCharacter: from, 120 | targetCharacter: target, 121 | backgroundCharacters: backgrounds, 122 | damages: map[uint64]*Damage{}, 123 | } 124 | } 125 | 126 | // NewDamageContext 新建一个带有基础伤害的DamageContext 127 | func NewDamageContext(skill, from, target uint64, backgrounds []uint64, elementType enum.ElementType, damageAmount uint) *DamageContext { 128 | return &DamageContext{ 129 | skillID: skill, 130 | sendCharacter: from, 131 | targetCharacter: target, 132 | backgroundCharacters: backgrounds, 133 | damages: map[uint64]*Damage{target: {elementType: elementType, amount: damageAmount, reaction: enum.ReactionNone}}, 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /entity/character.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/model/modifier/definition" 7 | ) 8 | 9 | type character struct { 10 | id uint64 // id 角色的ID,由框架确定 11 | player uint64 // player 所属玩家的ID,由框架确定 12 | affiliation enum.Affiliation // affiliation 角色的势力归属 13 | vision enum.ElementType // vision 角色的元素类型 14 | weapon enum.WeaponType // weapon 角色的武器类型 15 | skills map[uint64]model.Skill // skills 角色的技能 16 | 17 | maxHP uint // maxHP 角色的最大生命值 18 | currentHP uint // currentHP 角色的当前生命值 19 | maxMP uint // maxMP 角色的最大能量值 20 | currentMP uint // currentMP 角色的当前能量值 21 | status enum.CharacterStatus // status 角色的状态 22 | elements []enum.ElementType // elements 角色目前附着的元素 23 | satiety bool // satiety 角色的饱腹状态 24 | equipments map[enum.EquipmentType]uint64 // equipments 角色穿着的装备 25 | 26 | localDirectAttackModifiers definition.AttackModifiers // localDirectAttackModifiers 本地直接攻击修正 27 | localFinalAttackModifiers definition.AttackModifiers // localFinalAttackModifiers 本地最终攻击修正 28 | localDefenceModifiers definition.DefenceModifiers // localDefenceModifiers 本地防御修正 29 | localChargeModifiers definition.ChargeModifiers // localChargeModifiers 本地充能修正 30 | localHealModifiers definition.HealModifiers // localHealModifiers 本地治疗修正 31 | localCostModifiers definition.CostModifiers // localCostModifiers 本地费用修正 32 | } 33 | 34 | func (c character) GetID() (id uint64) { 35 | return c.id 36 | } 37 | 38 | func (c character) GetOwner() (owner uint64) { 39 | return c.player 40 | } 41 | 42 | func (c character) GetAffiliation() (affiliation enum.Affiliation) { 43 | return c.affiliation 44 | } 45 | 46 | func (c character) GetVision() (element enum.ElementType) { 47 | return c.vision 48 | } 49 | 50 | func (c character) GetWeaponType() (weaponType enum.WeaponType) { 51 | return c.weapon 52 | } 53 | 54 | func (c character) GetSkills() (skills []uint64) { 55 | skills = make([]uint64, 0) 56 | for id := range c.skills { 57 | skills = append(skills, id) 58 | } 59 | return skills 60 | } 61 | 62 | func (c character) GetHP() (hp uint) { 63 | return c.currentHP 64 | } 65 | 66 | func (c character) GetMaxHP() (maxHP uint) { 67 | return c.maxHP 68 | } 69 | 70 | func (c character) GetMP() (mp uint) { 71 | return c.currentMP 72 | } 73 | 74 | func (c character) GetMaxMP() (maxMP uint) { 75 | return c.maxMP 76 | } 77 | 78 | func (c character) GetEquipment(equipmentType enum.EquipmentType) (equipped bool, equipment uint64) { 79 | equipmentID, exist := c.equipments[equipmentType] 80 | return exist, equipmentID 81 | } 82 | 83 | func (c character) GetSatiety() (satiety bool) { 84 | return c.satiety 85 | } 86 | 87 | func (c character) GetAttachedElements() (elements []enum.ElementType) { 88 | return c.elements 89 | } 90 | 91 | func (c character) GetStatus() (status enum.CharacterStatus) { 92 | return c.status 93 | } 94 | 95 | func (c character) GetLocalModifiers(modifierType enum.ModifierType) (modifiers []uint64) { 96 | switch modifierType { 97 | case enum.ModifierTypeNone: 98 | return []uint64{} 99 | case enum.ModifierTypeAttack: 100 | modifiers = []uint64{} 101 | modifiers = append(modifiers, c.localDirectAttackModifiers.Expose()...) 102 | modifiers = append(modifiers, c.localFinalAttackModifiers.Expose()...) 103 | return modifiers 104 | case enum.ModifierTypeCharacter: 105 | return []uint64{} 106 | case enum.ModifierTypeCharge: 107 | return c.localChargeModifiers.Expose() 108 | case enum.ModifierTypeCost: 109 | return c.localCostModifiers.Expose() 110 | case enum.ModifierTypeDefence: 111 | return c.localDefenceModifiers.Expose() 112 | case enum.ModifierTypeHeal: 113 | return c.localHealModifiers.Expose() 114 | default: 115 | return []uint64{} 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /persistence/db.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/model/localization" 6 | "os" 7 | "path" 8 | "time" 9 | 10 | "github.com/go-xorm/xorm" 11 | "xorm.io/core" 12 | ) 13 | 14 | const ( 15 | ruleSetPersistenceFileName = "rule-set-persistence.psc" 16 | cardPersistenceFileName = "card-persistence.psc" 17 | characterPersistenceFileName = "character-persistence.psc" 18 | skillPersistenceFileName = "skill-persistence.psc" 19 | summonPersistenceFileName = "summon-persistence.psc" 20 | eventPersistenceFileName = "event-persistence.psc" 21 | sqlite3DBFileName = "gisb-sqlite3.db" 22 | ) 23 | 24 | var ( 25 | storagePath = "" 26 | loaded = false 27 | ) 28 | 29 | var ( 30 | RuleSetPersistence = newFactoryPersistence[RuleSet]() 31 | CardPersistence = newFactoryPersistence[Card]() 32 | CharacterPersistence = newFactoryPersistence[Character]() 33 | SkillPersistence = newFactoryPersistence[Skill]() 34 | SummonPersistence = newFactoryPersistence[Summon]() 35 | EventPersistence = newFactoryPersistence[Event]() 36 | 37 | LocalizationPersistence = newMemoryCache[string, localization.LanguagePack]() 38 | ModInfoPersistence = newMemoryCache[string, ModInfo]() 39 | RoomInfoPersistence = newMemoryCache[uint64, RoomInfo]() 40 | 41 | TokenPersistence = newTimingMemoryCache[string, Token]() 42 | 43 | CardDeckPersistence DatabasePersistence[uint64, CardDeck] 44 | PlayerPersistence DatabasePersistence[uint64, Player] 45 | ) 46 | 47 | // SetStoragePath 设置持久化文件的存放位置 48 | func SetStoragePath(path string) error { 49 | if s, err := os.Stat(path); err == nil && s.IsDir() { 50 | storagePath = path 51 | return nil 52 | } else { 53 | return fmt.Errorf("path %s is not a directory or is not exist", path) 54 | } 55 | } 56 | 57 | // Serve 开启持久化模块的服务 58 | func Serve(flushFeq time.Duration, errChan chan error) { 59 | RuleSetPersistence.Serve(flushFeq, storagePath, ruleSetPersistenceFileName, errChan) 60 | CardPersistence.Serve(flushFeq, storagePath, cardPersistenceFileName, errChan) 61 | CharacterPersistence.Serve(flushFeq, storagePath, characterPersistenceFileName, errChan) 62 | SkillPersistence.Serve(flushFeq, storagePath, skillPersistenceFileName, errChan) 63 | SummonPersistence.Serve(flushFeq, storagePath, summonPersistenceFileName, errChan) 64 | EventPersistence.Serve(flushFeq, storagePath, eventPersistenceFileName, errChan) 65 | TokenPersistence.Serve(time.Second*time.Duration(300), 0.5) 66 | } 67 | 68 | // Load 从持久化文件读取信息,写入持久化模块 69 | func Load(errChan chan error) { 70 | if !loaded { 71 | var err error 72 | 73 | // 初始化Sqlite3 74 | if sqlite3DB, err = xorm.NewEngine("sqlite3", path.Join(storagePath, sqlite3DBFileName)); err != nil { 75 | errChan <- err 76 | } else { 77 | sqlite3DB.SetMapper(core.SameMapper{}) 78 | 79 | var success bool 80 | if success, CardDeckPersistence = newDatabasePersistence[uint64, CardDeck](errChan); !success { 81 | errChan <- fmt.Errorf("failed to create database factoryPersistence with entity: %+v", CardDeck{}) 82 | } 83 | 84 | if success, PlayerPersistence = newDatabasePersistence[uint64, Player](errChan); !success { 85 | errChan <- fmt.Errorf("failed to create database factoryPersistence with entity: %+v", Player{}) 86 | } 87 | } 88 | 89 | // 初始化Factories 90 | { 91 | if err = RuleSetPersistence.Load(path.Join(storagePath, ruleSetPersistenceFileName)); err != nil { 92 | errChan <- err 93 | } 94 | 95 | if err = CardPersistence.Load(path.Join(storagePath, cardPersistenceFileName)); err != nil { 96 | errChan <- err 97 | } 98 | 99 | if err = CharacterPersistence.Load(path.Join(storagePath, characterPersistenceFileName)); err != nil { 100 | errChan <- err 101 | } 102 | 103 | if err = SkillPersistence.Load(path.Join(storagePath, skillPersistenceFileName)); err != nil { 104 | errChan <- err 105 | } 106 | 107 | if err = SummonPersistence.Load(path.Join(storagePath, summonPersistenceFileName)); err != nil { 108 | errChan <- err 109 | } 110 | 111 | if err = EventPersistence.Load(path.Join(storagePath, eventPersistenceFileName)); err != nil { 112 | errChan <- err 113 | } 114 | } 115 | 116 | loaded = true 117 | } 118 | } 119 | 120 | // Quit 退出持久化模块的各种持久化服务 121 | func Quit() { 122 | RuleSetPersistence.Exit() 123 | CardPersistence.Exit() 124 | CharacterPersistence.Exit() 125 | SkillPersistence.Exit() 126 | SummonPersistence.Exit() 127 | EventPersistence.Exit() 128 | TokenPersistence.Exit() 129 | } 130 | -------------------------------------------------------------------------------- /docs/en/README.md: -------------------------------------------------------------------------------- 1 | # 1. Summary 2 | 3 | ## 1.1 Overview 4 | 5 | Here is a simulator of the game Genshin Impact's `Genius Invokation TCG`, which is a backend implementation of the `Genius Invokation TCG`, playing methods referring to the version 3.3 of Genshin Impact, including all the game mechanisms in Genshin Impact, and expanding some content that miHoYo has not released. 6 | 7 | > **Attention: This readme file is for developers' reference. If you only want to use this project, you can explore the guide page of this website** 8 | 9 | ## 1.2 Announcement 10 | 11 | The community channel(QQ) of this project is `530924402`, you can also use the [GitHub Discussions](https://github.com/sunist-c/genius-invokation-simulator-backend/discussions) for communication. Welcome to discuss and PR :) 12 | 13 | This simulator focuses on providing customized `Genius Invokation TCG` game, such as: 14 | 15 | + Roll 12 dice each round 16 | + Obtain four cards each round 17 | + Play with customized roles, cards and rules 18 | 19 | As soon as the project is stable, we will optimize and adapt for AI training at first time, we consider to Compatible with `RLcard` when designing interfaces 20 | 21 | > **This project does not contain any art materials of Genshin Impact, and the relevant copyright of Genshin Impact belongs to miHoYo** 22 | 23 | ## 1.3 Technological Stacks and Components 24 | 25 | + golang 1.19 or higher: this is the coding language and running environment of the project 26 | + gcc: compile cgo requirements caused by embedded sqlite3 27 | + sqlite3: the persistent component used to store player information and player card set information in this project, it is embedded and does not need to be downloaded 28 | 29 | # 2. Badges 30 | 31 | ## 2.1 Build Status 32 | 33 | | Branch | master | dev | release | 34 | | :--: | :--: | :--: | :--: | 35 | | drone-ci | [![Build Status](https://drone.sunist.cn/api/badges/sunist-c/genius-invokation-simulator-backend/status.svg?ref=refs/heads/master)](https://drone.sunist.cn/sunist-c/genius-invokation-simulator-backend) | [![Build Status](https://drone.sunist.cn/api/badges/sunist-c/genius-invokation-simulator-backend/status.svg?ref=refs/heads/dev)](https://drone.sunist.cn/sunist-c/genius-invokation-simulator-backend) | nil | 36 | | github-action | [![gisb-test](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml) | [![gisb-test](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml/badge.svg?branch=dev)](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml) | nil | 37 | 38 | ## 2.2 Code Analysis 39 | 40 | [![Go Report Card](https://goreportcard.com/badge/github.com/sunist-c/genius-invokation-simulator-backend)](https://goreportcard.com/report/github.com/sunist-c/genius-invokation-simulator-backend) 41 | 42 | # 3. Branches 43 | 44 | - master: the main branch, will synchronize with the dev branch when important function changes occur 45 | - dev: the branch under development, will undergo frequent changes and new function tests 46 | - release: the stable branch, will only merge functions when major version is updated 47 | - document: the document branch, the source file of this website 48 | 49 | # 4. Progress 50 | 51 | Please turn to [gisb's feature development](https://github.com/users/sunist-c/projects/2) to explore the progress of this project 52 | 53 | # 5. Contribution 54 | 55 | > // todo 56 | 57 | # 6. Features 58 | 59 | 60 | - [ ] Basic game playing method 61 | - [ ] Elemental reaction 62 | - [x] Roles and skills 63 | - [ ] Artifacts and weapons 64 | - [ ] Scenarios and partners 65 | - [ ] Summons 66 | - [ ] Cards 67 | - [x] Element transformation 68 | - [ ] Game expansion 69 | - [ ] Multiplayer game(players more than 2) 70 | - [ ] Match game 71 | - [ ] Creat room 72 | - [ ] Join room 73 | - [ ] In-game communication 74 | - [ ] Viewer mode 75 | - [ ] Competition mode 76 | - [ ] Operational record 77 | - [x] Custom Mod support 78 | - [ ] Go/Lua/Python support 79 | - [x] Custom roles and cards 80 | - [x] Custom rules 81 | - [x] Multiple communication protocol connections 82 | - [x] websocket interface 83 | - [x] http/https interface 84 | - [ ] udp/kcp interface 85 | - [ ] rpc interface 86 | - [ ] Distributed support 87 | - [ ] Automated deployment 88 | - [ ] Service registration and service discovery 89 | - [ ] Server load balancing 90 | - [ ] Management function 91 | - [ ] Announcements and notices 92 | - [x] IP tracking/blocking 93 | - [x] QPS/TPS limiter -------------------------------------------------------------------------------- /entity/player.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/model/modifier/definition" 7 | ) 8 | 9 | type PlayerInfo struct { 10 | UID uint64 11 | Cards []model.Card 12 | Characters []*character 13 | } 14 | 15 | type player struct { 16 | uid uint64 // uid 玩家的UID,由其他模块托管 17 | status enum.PlayerStatus // status 玩家的状态 18 | 19 | operated bool // operated 本回合玩家是否操作过 20 | reRollTimes uint // reRollTimes 重新投掷的次数 21 | staticCost *model.Cost // staticCost 每回合投掷阶段固定产出的骰子 22 | 23 | holdingCost *model.Cost // holdingCost 玩家持有的骰子 24 | cardDeck *CardDeck // cardDeck 玩家的牌堆 25 | holdingCards map[uint64]model.Card // holdingCards 玩家持有的卡牌 26 | activeCharacter uint64 // activeCharacter 玩家当前的前台角色 27 | 28 | characters map[uint64]*character // characters 玩家出战的角色 29 | characterList []uint64 // characterList 玩家的角色列表 30 | summons map[uint64]Summon // summons 玩家在场的召唤物 31 | summonList []uint64 // summonList 玩家的召唤物列表 32 | supports map[uint64]Support // supports 玩家在场的支援 33 | supportList []uint64 // supportList 玩家的支援物列表 34 | 35 | globalDirectAttackModifiers definition.AttackModifiers // globalDirectAttackModifiers 全局直接攻击修正 36 | globalFinalAttackModifiers definition.AttackModifiers // globalFinalAttackModifiers 全局最终攻击修正 37 | globalDefenceModifiers definition.DefenceModifiers // globalDefenceModifiers 全局防御修正 38 | globalHealModifiers definition.HealModifiers // globalHealModifiers 全局治疗修正 39 | globalChargeModifiers definition.ChargeModifiers // globalChargeModifiers 全局充能修正 40 | globalCostModifiers definition.CostModifiers // globalCostModifiers 全局费用修正 41 | 42 | cooperativeAttacks map[enum.TriggerType]model.CooperativeSkill // cooperativeAttacks 协同攻击技能 43 | callbackEvents *Map // callbackEvents 回调事件集合 44 | } 45 | 46 | func (p player) GetUID() (uid uint64) { 47 | return p.uid 48 | } 49 | 50 | func (p player) GetCost() (cost map[enum.ElementType]uint) { 51 | return p.holdingCost.Costs() 52 | } 53 | 54 | func (p player) GetCards() (cards []uint64) { 55 | cards = []uint64{} 56 | for i := range p.holdingCards { 57 | cards = append(cards, i) 58 | } 59 | 60 | return cards 61 | } 62 | 63 | func (p player) GetSummons() (summons []uint64) { 64 | return p.summonList 65 | } 66 | 67 | func (p player) GetSupports() (supports []uint64) { 68 | return p.summonList 69 | } 70 | 71 | func (p player) CardDeckRemain() (remain uint) { 72 | return p.cardDeck.remain 73 | } 74 | 75 | func (p player) GetActiveCharacter() (character uint64) { 76 | return p.activeCharacter 77 | } 78 | 79 | func (p player) GetBackgroundCharacters() (characters []uint64) { 80 | characters = []uint64{} 81 | for _, character := range p.characters { 82 | if character.status != enum.CharacterStatusDefeated && character.id != p.activeCharacter { 83 | characters = append(characters, character.id) 84 | } 85 | } 86 | 87 | return characters 88 | } 89 | 90 | func (p player) GetCharacter(character uint64) (has bool, entity model.Character) { 91 | characterEntity, exist := p.characters[character] 92 | return exist, characterEntity 93 | } 94 | 95 | func (p player) GetStatus() (status enum.PlayerStatus) { 96 | return p.status 97 | } 98 | 99 | func (p player) GetGlobalModifiers(modifierType enum.ModifierType) (modifiers []uint64) { 100 | switch modifierType { 101 | case enum.ModifierTypeNone: 102 | return []uint64{} 103 | case enum.ModifierTypeAttack: 104 | modifiers = p.globalDirectAttackModifiers.Expose() 105 | modifiers = append(modifiers, p.globalFinalAttackModifiers.Expose()...) 106 | return modifiers 107 | case enum.ModifierTypeCharacter: 108 | return []uint64{} 109 | case enum.ModifierTypeCharge: 110 | return p.globalChargeModifiers.Expose() 111 | case enum.ModifierTypeCost: 112 | return p.globalCostModifiers.Expose() 113 | case enum.ModifierTypeDefence: 114 | return p.globalDefenceModifiers.Expose() 115 | case enum.ModifierTypeHeal: 116 | return p.globalHealModifiers.Expose() 117 | default: 118 | return []uint64{} 119 | } 120 | } 121 | 122 | func (p player) GetCooperativeSkills(trigger enum.TriggerType) (skills []uint64) { 123 | return []uint64{} 124 | } 125 | 126 | func (p player) GetEvents(trigger enum.TriggerType) (events []uint64) { 127 | return p.callbackEvents.Expose(trigger) 128 | } 129 | -------------------------------------------------------------------------------- /mod/implement/mod.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 6 | ) 7 | 8 | type ModImpl struct { 9 | characters map[uint64]definition.Character 10 | skills map[uint64]definition.Skill 11 | events map[uint64]definition.Event 12 | summons map[uint64]definition.Summon 13 | cards map[uint64]definition.Card 14 | rules map[uint64]definition.Rule 15 | languages map[enum.Language]definition.LanguagePack 16 | } 17 | 18 | func (impl *ModImpl) ProduceCharacters() []definition.Character { 19 | if impl.characters == nil { 20 | impl.characters = map[uint64]definition.Character{} 21 | } 22 | 23 | result := make([]definition.Character, 0, len(impl.characters)) 24 | for _, character := range impl.characters { 25 | result = append(result, character) 26 | } 27 | 28 | return result 29 | } 30 | 31 | func (impl *ModImpl) ProduceSkills() []definition.Skill { 32 | if impl.skills == nil { 33 | impl.skills = map[uint64]definition.Skill{} 34 | } 35 | 36 | result := make([]definition.Skill, 0, len(impl.skills)) 37 | for _, skill := range impl.skills { 38 | result = append(result, skill) 39 | } 40 | 41 | return result 42 | } 43 | 44 | func (impl *ModImpl) ProduceEvents() []definition.Event { 45 | if impl.events == nil { 46 | impl.events = map[uint64]definition.Event{} 47 | } 48 | 49 | result := make([]definition.Event, 0, len(impl.events)) 50 | for _, event := range impl.events { 51 | result = append(result, event) 52 | } 53 | 54 | return result 55 | } 56 | 57 | func (impl *ModImpl) ProduceSummons() []definition.Summon { 58 | if impl.summons == nil { 59 | impl.summons = map[uint64]definition.Summon{} 60 | } 61 | 62 | result := make([]definition.Summon, 0, len(impl.summons)) 63 | for _, summon := range impl.summons { 64 | result = append(result, summon) 65 | } 66 | 67 | return result 68 | } 69 | 70 | func (impl *ModImpl) ProduceCards() []definition.Card { 71 | if impl.cards == nil { 72 | impl.cards = map[uint64]definition.Card{} 73 | } 74 | 75 | result := make([]definition.Card, 0, len(impl.cards)) 76 | for _, card := range impl.cards { 77 | result = append(result, card) 78 | } 79 | 80 | return result 81 | } 82 | 83 | func (impl *ModImpl) ProduceRules() []definition.Rule { 84 | if impl.rules == nil { 85 | impl.rules = map[uint64]definition.Rule{} 86 | } 87 | 88 | result := make([]definition.Rule, 0, len(impl.rules)) 89 | for _, rule := range impl.rules { 90 | result = append(result, rule) 91 | } 92 | 93 | return result 94 | } 95 | 96 | func (impl *ModImpl) ProduceLanguagePacks() []definition.LanguagePack { 97 | if impl.languages == nil { 98 | impl.languages = map[enum.Language]definition.LanguagePack{} 99 | } 100 | 101 | result := make([]definition.LanguagePack, 0, len(impl.languages)) 102 | for _, languagePack := range impl.languages { 103 | result = append(result, languagePack) 104 | } 105 | 106 | return result 107 | } 108 | 109 | func (impl *ModImpl) RegisterCharacter(character definition.Character) { 110 | if impl.characters == nil { 111 | impl.characters = map[uint64]definition.Character{} 112 | } 113 | 114 | impl.characters[character.TypeID()] = character 115 | } 116 | 117 | func (impl *ModImpl) RegisterSkill(skill definition.Skill) { 118 | if impl.skills == nil { 119 | impl.skills = map[uint64]definition.Skill{} 120 | } 121 | 122 | impl.skills[skill.TypeID()] = skill 123 | } 124 | 125 | func (impl *ModImpl) RegisterEvent(event definition.Event) { 126 | if impl.events == nil { 127 | impl.events = map[uint64]definition.Event{} 128 | } 129 | 130 | impl.events[event.TypeID()] = event 131 | } 132 | 133 | func (impl *ModImpl) RegisterSummon(summon definition.Summon) { 134 | if impl.summons == nil { 135 | impl.summons = map[uint64]definition.Summon{} 136 | } 137 | 138 | impl.summons[summon.TypeID()] = summon 139 | } 140 | 141 | func (impl *ModImpl) RegisterCard(card definition.Card) { 142 | impl.cards[card.TypeID()] = card 143 | } 144 | 145 | func (impl *ModImpl) RegisterRule(rule definition.Rule) { 146 | impl.rules[rule.TypeID()] = rule 147 | } 148 | 149 | func (impl *ModImpl) AttachLanguagePack(languagePack definition.LanguagePack) { 150 | impl.languages[languagePack.Language()] = languagePack 151 | } 152 | 153 | func NewMod() definition.Mod { 154 | return &ModImpl{ 155 | characters: map[uint64]definition.Character{}, 156 | skills: map[uint64]definition.Skill{}, 157 | events: map[uint64]definition.Event{}, 158 | summons: map[uint64]definition.Summon{}, 159 | cards: map[uint64]definition.Card{}, 160 | rules: map[uint64]definition.Rule{}, 161 | languages: map[enum.Language]definition.LanguagePack{}, 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Genius Invokation Simulator Backend 2 | 3 | > 5.29: 项目还在更新,稍安勿躁,目前在研究老米的新版本 4 | 5 | ## 1. 综述 6 | 7 | ### 1.1 概况 8 | 9 | 这里是原神(Genshin Impact)的《七圣召唤》模拟器,是参考原神3.3版本的「七圣召唤」玩法重新实现的后端(服务端),包括所有的原神内的游戏内容,并拓展一些米哈游没有做的内容。 10 | 11 | **本自述文件供开发者参考,若您只是使用本项目,您可以转到我们的[文档](https://sunist-c.github.io/genius-invokation-simulator-backend/)** 12 | 13 | [English Version](https://sunist-c.github.io/genius-invokation-simulator-backend/#/en/) 14 | 15 | ### 1.2 声明 16 | 17 | 本项目的交流群(QQ)为`530924402`,欢迎讨论与PR 18 | 19 | 本模拟器侧重于提供自定义的七圣召唤对局,比如每次投十二个骰子/每回合摸四张牌/加入自定义角色、卡牌等功能,~~短期内没有~~ 项目稳定后尽快针对ai训练进行优化、适配。 20 | 考虑设计接口时兼容RLcard 21 | 22 | 相关的[genius-invokation-gym](https://github.com/paladin1013/genius-invokation-gym)项目侧重于提供ai相关接口,请根据需求选择 23 | 24 | **本模拟器接口尽量和[genius-invokation-gym](https://github.com/paladin1013/genius-invokation-gym)保持一致,其项目完善后本项目也尽量拓展相应的ai接口** 25 | 26 | 同时感谢[@Leng Yue](https://github.com/leng-yue)实现的前端项目[genius-invokation-webui](https://github.com/leng-yue/genius-invokation-webui) 27 | 28 | **本项目不含任何原神(Genshin Impact)的美术素材,原神(Genshin Impact)的相关版权归米哈游(miHoYo)所有** 29 | 30 | ### 1.3 技术栈与组件 31 | 32 | + golang 1.19: 这是本项目的编码语言与运行环境 33 | + mingw-gcc/clang/gcc: 因为内嵌sqlite3产生的cgo编译需求 34 | + sqlite3: 这是本项目用于存储玩家信息、玩家卡组信息的持久化组件,已内嵌,无需下载 35 | 36 | ## 2. 徽章 37 | 38 | ### 2.1 构建情况 39 | 40 | | Branch | master | dev | release | 41 | | :--: | :--: | :--: | :--: | 42 | | drone-ci | [![Build Status](https://drone.sunist.cn/api/badges/sunist-c/genius-invokation-simulator-backend/status.svg?ref=refs/heads/master)](https://drone.sunist.cn/sunist-c/genius-invokation-simulator-backend) | [![Build Status](https://drone.sunist.cn/api/badges/sunist-c/genius-invokation-simulator-backend/status.svg?ref=refs/heads/dev)](https://drone.sunist.cn/sunist-c/genius-invokation-simulator-backend) | [![Build Status](https://drone.sunist.cn/api/badges/sunist-c/genius-invokation-simulator-backend/status.svg?ref=refs/heads/release)](https://drone.sunist.cn/sunist-c/genius-invokation-simulator-backend) | 43 | | github-action | [![gisb-test](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml/badge.svg?branch=master)](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml) | [![gisb-test](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml/badge.svg?branch=dev)](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml) | [![gisb-test](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml/badge.svg?branch=release)](https://github.com/sunist-c/genius-invokation-simulator-backend/actions/workflows/go.yml) | 44 | 45 | ### 2.2 代码质量 46 | 47 | [![Go Report Card](https://goreportcard.com/badge/github.com/sunist-c/genius-invokation-simulator-backend)](https://goreportcard.com/report/github.com/sunist-c/genius-invokation-simulator-backend) 48 | 49 | ## 3. 分支说明 50 | 51 | - master 主要的分支,将在发生重要功能修改时与dev分支进行同步 52 | - dev 开发中的分支,将频繁地进行更改与新功能测试 53 | - release 稳定的分支,仅会在重大版本更新时进行功能合并 54 | - cli 脚手架分支,将提供一个代码生成脚手架,用于快速实现mod 55 | 56 | ## 4. 开发进度 57 | 58 | 请转到[gisb's feature development](https://github.com/users/sunist-c/projects/2)查看项目进度 59 | 60 | ## 5. 开发文档 61 | 62 | 您可以转到[genius-invokation-simulator-mod-template](https://github.com/sunist-c/genius-invokation-simulator-mod-template)查看我们的示例mod,并从该模板创建您的mod 63 | 64 | + 战斗框架: [Battle Framework of GISB](https://github.com/sunist-c/genius-invokation-simulator-backend/wiki/Battle-Framework) 65 | + 事件框架: Mkdir... 66 | + MOD制作: Mkdir... 67 | 68 | ## 6. 参与项目 69 | 70 | 如果您想增加一个功能或想法: 71 | 72 | 1. 加入本项目的交流群或[genius-invokation-gym](https://github.com/paladin1013/genius-invokation-gym)的交流群或在本项目的[Discussion/Ideas](https://github.com/sunist-c/genius-invokation-simulator-backend/discussions/categories/ideas)中分享您的想法 73 | 2. 在取得Contributor的广泛认可后将为您创建一个WIP的issue 74 | 3. 按照正常的流程fork->coding->pull request您的修改 75 | 76 | 如果您想为项目贡献代码,您可以转到[gisb's feature development](https://github.com/users/sunist-c/projects/2)查看这个项目目前在干什么,标有`help wanted`的内容可能需要一些帮助,处于`design`阶段的内容目前还没有进行开发,您可以直接在GitHub Projects/Project Issue页面与我们交流 77 | 78 | ## 7. 功能特性 79 | 80 | 转到我们的[文档](https://sunist-c.github.io/genius-invokation-simulator-backend/)查看最新的功能特性 81 | 82 | ## 8. 开源许可 83 | 84 | [MIT License](license) 85 | 86 | ## 9. 鸣谢 87 | 88 | 感谢 [JetBrains](https://www.jetbrains.com) 为本项目提供的 [Open Source development license(s)](https://www.jetbrains.com/community/opensource/#support) 89 | 90 | GoLand logo.IntelliJ_IDEA logo.WebStorm logo.PyCharm logo. 91 | -------------------------------------------------------------------------------- /protocol/websocket/message/action.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 5 | ) 6 | 7 | // ActionMessageInterface 玩家操作信息的约束接口 8 | type ActionMessageInterface interface { 9 | AttackAction | BurnCardAction | UseCardAction | ReRollAction | SkipRoundAction | SwitchAction | ConcedeAction 10 | } 11 | 12 | // AttackAction 玩家的攻击操作信息 13 | type AttackAction struct { 14 | Sender uint64 `json:"sender" yaml:"sender" xml:"sender"` // Sender 发起操作的玩家 15 | Target uint64 `json:"target" yaml:"target" xml:"target"` // Target 被攻击的玩家 16 | Skill uint64 `json:"skill" yaml:"skill" xml:"skill"` // Skill 执行攻击的技能 17 | Paid map[enum.ElementType]uint `json:"paid" yaml:"paid" xml:"paid"` // Paid 支付发起攻击的费用 18 | } 19 | 20 | // BurnCardAction 玩家的元素转换操作信息 21 | type BurnCardAction struct { 22 | Sender uint64 `json:"sender" yaml:"sender" xml:"sender"` // Sender 发起操作的玩家 23 | Card uint64 `json:"card" yaml:"card" xml:"card"` // Card 被操作的卡牌 24 | Paid enum.ElementType `json:"paid" yaml:"paid" xml:"paid"` // Paid 支付元素转换的费用 25 | } 26 | 27 | // ReRollAction 玩家的重掷元素骰子操作信息 28 | type ReRollAction struct { 29 | Sender uint64 `json:"sender" yaml:"sender" xml:"sender"` // Sender 发起操作的玩家 30 | Dropped map[enum.ElementType]uint `json:"dropped" yaml:"dropped" xml:"dropped"` // Dropped 被舍弃的元素骰子 31 | } 32 | 33 | // UseCardAction 玩家的使用卡牌操作信息 34 | type UseCardAction struct { 35 | Sender uint64 `json:"sender" yaml:"sender" xml:"sender"` // Sender 发起操作的玩家 36 | Card uint64 `json:"card" yaml:"card" xml:"card"` // Card 玩家打出的卡牌 37 | Paid map[enum.ElementType]uint `json:"paid" yaml:"paid" xml:"paid"` // Paid 玩家打出卡牌支付的费用 38 | } 39 | 40 | // SkipRoundAction 玩家的跳过回合操作信息 41 | type SkipRoundAction struct { 42 | Sender uint64 `json:"sender" yaml:"sender" xml:"sender"` // Sender 发起操作的玩家 43 | } 44 | 45 | // SwitchAction 玩家的切换前台角色操作信息 46 | type SwitchAction struct { 47 | Sender uint64 `json:"sender" yaml:"sender" xml:"sender"` // Sender 发起操作的玩家 48 | Target uint64 `json:"target" yaml:"target" xml:"target"` // Target 切换到的目标角色 49 | Paid map[enum.ElementType]uint `json:"paid" yaml:"paid" xml:"paid"` // Paid 玩家切换角色支付的费用 50 | } 51 | 52 | // ConcedeAction 玩家的弃权操作信息 53 | type ConcedeAction struct { 54 | Sender uint64 `json:"sender" yaml:"sender" xml:"sender"` // Sender 发起操作的玩家 55 | } 56 | 57 | // ActionMessage 玩家的操作信息 58 | type ActionMessage struct { 59 | Type enum.ActionType `json:"type" yaml:"type" xml:"type"` // Type 玩家的操作类型 60 | Sender uint64 `json:"sender" yaml:"sender" xml:"sender"` // Sender 发送操作的玩家 61 | Args interface{} `json:"args" yaml:"args" xml:"args"` // Args 玩家的操作信息 62 | } 63 | 64 | func exchangeMessage[target ActionMessageInterface](message ActionMessage, conditions ...enum.ActionType) (success bool, result target) { 65 | typeCheck := false 66 | for _, condition := range conditions { 67 | if message.Type == condition { 68 | typeCheck = true 69 | break 70 | } 71 | } 72 | 73 | if !typeCheck { 74 | return false, result 75 | } else { 76 | result, success = message.Args.(target) 77 | return success, result 78 | } 79 | } 80 | 81 | // ToAttackMessage 将ActionMessage转换为AttackAction 82 | func (a ActionMessage) ToAttackMessage() (success bool, message AttackAction) { 83 | return exchangeMessage[AttackAction](a, enum.ActionNormalAttack, enum.ActionElementalSkill, enum.ActionElementalBurst) 84 | } 85 | 86 | // ToBurnCardMessage 将ActionMessage转换为BurnCardAction 87 | func (a ActionMessage) ToBurnCardMessage() (success bool, message BurnCardAction) { 88 | return exchangeMessage[BurnCardAction](a, enum.ActionBurnCard) 89 | } 90 | 91 | // ToUesCardMessage 将ActionMessage转换为UseCardAction 92 | func (a ActionMessage) ToUesCardMessage() (success bool, message UseCardAction) { 93 | return exchangeMessage[UseCardAction](a, enum.ActionUseCard) 94 | } 95 | 96 | // ToReRollMessage 将ActionMessage转换为ReRollAction 97 | func (a ActionMessage) ToReRollMessage() (success bool, message ReRollAction) { 98 | return exchangeMessage[ReRollAction](a, enum.ActionReRoll) 99 | } 100 | 101 | // ToSwitchMessage 将ActionMessage转换为SwitchAction 102 | func (a ActionMessage) ToSwitchMessage() (success bool, message SwitchAction) { 103 | return exchangeMessage[SwitchAction](a, enum.ActionSwitch) 104 | } 105 | 106 | // ToConcedeMessage 将ActionMessage转换为ConcedeAction 107 | func (a ActionMessage) ToConcedeMessage() (success bool, message ConcedeAction) { 108 | return exchangeMessage[ConcedeAction](a, enum.ActionConcede) 109 | } 110 | 111 | // ToSkipRoundMessage 将ActionMessage转换为SkipRoundAction 112 | func (a ActionMessage) ToSkipRoundMessage() (success bool, message SkipRoundAction) { 113 | return exchangeMessage[SkipRoundAction](a, enum.ActionSkipRound) 114 | } 115 | -------------------------------------------------------------------------------- /persistence/timing.go: -------------------------------------------------------------------------------- 1 | package persistence 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | var ( 10 | random = rand.New(rand.NewSource(time.Now().UnixNano())) 11 | ) 12 | 13 | type timingCacheRecord[T any] struct { 14 | data T 15 | timeoutAt time.Time 16 | } 17 | 18 | // timingMemoryCache 带超时系统的的内存缓存Map,实现同kv.syncMap 19 | type timingMemoryCache[PK comparable, T any] struct { 20 | mutex sync.RWMutex 21 | cache map[PK]*timingCacheRecord[T] 22 | size uint 23 | exit chan struct{} 24 | served bool 25 | } 26 | 27 | func (t *timingMemoryCache[PK, T]) proactivelyClean(index float64) { 28 | if index < 0 || index > 1 { 29 | index = 1 30 | } 31 | 32 | need, executeCount := uint(float64(t.size)/index), uint(0) 33 | executeList := make([]PK, need) 34 | 35 | // 统计需要删除的超时记录 36 | t.mutex.RLock() 37 | for pk, entity := range t.cache { 38 | if !entity.timeoutAt.IsZero() && entity.timeoutAt.Before(time.Now()) { 39 | executeList[executeCount] = pk 40 | executeCount++ 41 | } 42 | 43 | if need = need - 1; need == 0 { 44 | break 45 | } 46 | } 47 | t.mutex.RUnlock() 48 | 49 | // 执行删除操作,频繁加锁性能较差,此处不采用分段锁 50 | t.mutex.Lock() 51 | for i := uint(0); i < executeCount; i++ { 52 | delete(t.cache, executeList[i]) 53 | t.size -= 1 54 | } 55 | t.mutex.Unlock() 56 | } 57 | 58 | func (t *timingMemoryCache[PK, T]) serve(proactivelyCleanTime time.Duration, proactivelyCleanIndex float64) { 59 | // 起服务前先随机sleep一会,让一起初始化的多个组建主动清理时间错开 60 | randomNum := random.Int63n(int64(proactivelyCleanTime)) 61 | time.Sleep(time.Duration(randomNum)) 62 | 63 | for { 64 | select { 65 | case <-t.exit: 66 | return 67 | default: 68 | t.proactivelyClean(proactivelyCleanIndex) 69 | time.Sleep(proactivelyCleanTime) 70 | } 71 | } 72 | } 73 | 74 | func (t *timingMemoryCache[PK, T]) get(id PK) (exist bool, record *timingCacheRecord[T]) { 75 | t.mutex.RLock() 76 | r, has := t.cache[id] 77 | t.mutex.RUnlock() 78 | if !has { 79 | return false, record 80 | } else if !r.timeoutAt.IsZero() && r.timeoutAt.Before(time.Now()) { 81 | t.mutex.Lock() 82 | delete(t.cache, id) 83 | t.size -= 1 84 | t.mutex.Unlock() 85 | return false, record 86 | } else { 87 | return true, r 88 | } 89 | } 90 | 91 | func (t *timingMemoryCache[PK, T]) QueryByID(id PK) (has bool, result T, timeoutAt time.Time) { 92 | if exist, record := t.get(id); !exist { 93 | return false, result, time.Time{} 94 | } else { 95 | return true, record.data, record.timeoutAt 96 | } 97 | } 98 | 99 | func (t *timingMemoryCache[PK, T]) UpdateByID(id PK, entity T) (success bool, timeoutAt time.Time) { 100 | if exist, record := t.get(id); !exist { 101 | return false, time.Time{} 102 | } else { 103 | record.data = entity 104 | return true, record.timeoutAt 105 | } 106 | } 107 | 108 | func (t *timingMemoryCache[PK, T]) RefreshByID(id PK, timeout time.Duration) (success bool, timeoutAt time.Time) { 109 | if exist, record := t.get(id); !exist { 110 | return false, time.Time{} 111 | } else if timeout == 0 { 112 | record.timeoutAt = time.Time{} 113 | return true, record.timeoutAt 114 | } else { 115 | record.timeoutAt = time.Now().Add(timeout) 116 | return true, record.timeoutAt 117 | } 118 | } 119 | 120 | func (t *timingMemoryCache[PK, T]) InsertOne(id PK, entity T, timeout time.Duration) (success bool, timeoutAt time.Time) { 121 | if exist, _ := t.get(id); exist { 122 | return false, time.Time{} 123 | } else { 124 | if timeout == 0 { 125 | t.mutex.Lock() 126 | t.cache[id] = &timingCacheRecord[T]{data: entity, timeoutAt: time.Time{}} 127 | t.size += 1 128 | t.mutex.Unlock() 129 | return true, time.Time{} 130 | } else { 131 | timeoutAt = time.Now().Add(timeout) 132 | t.mutex.Lock() 133 | t.cache[id] = &timingCacheRecord[T]{data: entity, timeoutAt: timeoutAt} 134 | t.size += 1 135 | t.mutex.Unlock() 136 | return true, timeoutAt 137 | } 138 | } 139 | } 140 | 141 | func (t *timingMemoryCache[PK, T]) DeleteByID(id PK) (success bool) { 142 | if exist, _ := t.get(id); exist { 143 | t.mutex.Lock() 144 | delete(t.cache, id) 145 | t.size -= 1 146 | t.mutex.Unlock() 147 | return true 148 | } else { 149 | return false 150 | } 151 | } 152 | 153 | func (t *timingMemoryCache[PK, T]) Serve(proactivelyCleanTime time.Duration, proactivelyCleanIndex float64) { 154 | if proactivelyCleanTime > 0 { 155 | go t.serve(proactivelyCleanTime, proactivelyCleanIndex) 156 | t.served = true 157 | } else { 158 | t.served = false 159 | } 160 | } 161 | 162 | func (t *timingMemoryCache[PK, T]) Exit() { 163 | if t.served { 164 | t.exit <- struct{}{} 165 | } 166 | } 167 | 168 | func newTimingMemoryCache[PK comparable, T any]() TimingMemoryCache[PK, T] { 169 | return &timingMemoryCache[PK, T]{ 170 | mutex: sync.RWMutex{}, 171 | cache: map[PK]*timingCacheRecord[T]{}, 172 | size: 0, 173 | exit: make(chan struct{}, 1), 174 | served: false, 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /mod/adapter/card.go: -------------------------------------------------------------------------------- 1 | package adapter 2 | 3 | import ( 4 | "github.com/sunist-c/genius-invokation-simulator-backend/entity/model" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/enum" 6 | definition "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 7 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/implement" 8 | "github.com/sunist-c/genius-invokation-simulator-backend/model/adapter" 9 | ) 10 | 11 | type cardAdapterLayer struct { 12 | implement.EntityImpl 13 | cardType enum.CardType 14 | cost map[enum.ElementType]uint 15 | } 16 | 17 | func (c *cardAdapterLayer) Type() enum.CardType { 18 | return c.cardType 19 | } 20 | 21 | func (c *cardAdapterLayer) Cost() map[enum.ElementType]uint { 22 | return c.cost 23 | } 24 | 25 | type CardAdapter struct{} 26 | 27 | func (c CardAdapter) Convert(source definition.Card) (success bool, result model.Card) { 28 | adapterLayer := &cardAdapterLayer{ 29 | EntityImpl: implement.EntityImpl{}, 30 | cardType: source.CardType(), 31 | cost: source.Cost(), 32 | } 33 | 34 | return true, adapterLayer 35 | } 36 | 37 | func NewCardAdapter() adapter.Adapter[definition.Card, model.Card] { 38 | return CardAdapter{} 39 | } 40 | 41 | type eventCardAdapterLayer struct { 42 | cardAdapterLayer 43 | event definition.Event 44 | } 45 | 46 | func (e *eventCardAdapterLayer) Event() (event model.Event) { 47 | _, event = eventAdapter.Convert(e.event) 48 | return event 49 | } 50 | 51 | type EventCardAdapter struct{} 52 | 53 | func (e EventCardAdapter) Convert(source definition.EventCard) (success bool, result model.EventCard) { 54 | adapterLayer := &eventCardAdapterLayer{ 55 | cardAdapterLayer: cardAdapterLayer{ 56 | EntityImpl: implement.EntityImpl{}, 57 | cardType: source.CardType(), 58 | cost: source.Cost(), 59 | }, 60 | event: source.Event(), 61 | } 62 | 63 | return true, adapterLayer 64 | } 65 | 66 | func NewEventCardAdapter() adapter.Adapter[definition.EventCard, model.EventCard] { 67 | return EventCardAdapter{} 68 | } 69 | 70 | type supportCardAdapterLayer struct { 71 | cardAdapterLayer 72 | event definition.Event 73 | } 74 | 75 | func (s *supportCardAdapterLayer) Support() (event model.Event) { 76 | _, event = eventAdapter.Convert(s.event) 77 | return event 78 | } 79 | 80 | type SupportCardAdapter struct{} 81 | 82 | func (s SupportCardAdapter) Convert(source definition.SupportCard) (success bool, result model.SupportCard) { 83 | adapterLayer := &supportCardAdapterLayer{ 84 | cardAdapterLayer: cardAdapterLayer{ 85 | EntityImpl: implement.EntityImpl{}, 86 | cardType: source.CardType(), 87 | cost: source.Cost(), 88 | }, 89 | event: source.Support(), 90 | } 91 | 92 | return true, adapterLayer 93 | } 94 | 95 | func NewSupportCardAdapter() adapter.Adapter[definition.SupportCard, model.SupportCard] { 96 | return SupportCardAdapter{} 97 | } 98 | 99 | type equipmentCardAdapterLayer struct { 100 | cardAdapterLayer 101 | event definition.Event 102 | equipmentType enum.EquipmentType 103 | } 104 | 105 | func (e *equipmentCardAdapterLayer) EquipmentType() enum.EquipmentType { 106 | return e.equipmentType 107 | } 108 | 109 | func (e *equipmentCardAdapterLayer) Modify() (event model.Event) { 110 | _, event = eventAdapter.Convert(e.event) 111 | return event 112 | } 113 | 114 | type EquipmentCardAdapter struct{} 115 | 116 | func (e EquipmentCardAdapter) Convert(source definition.EquipmentCard) (success bool, result model.EquipmentCard) { 117 | adapterLayer := &equipmentCardAdapterLayer{ 118 | cardAdapterLayer: cardAdapterLayer{ 119 | EntityImpl: implement.EntityImpl{}, 120 | cardType: source.CardType(), 121 | cost: source.Cost(), 122 | }, 123 | event: source.Modify(), 124 | equipmentType: source.EquipmentType(), 125 | } 126 | 127 | return true, adapterLayer 128 | } 129 | 130 | func NewEquipmentCardAdapter() adapter.Adapter[definition.EquipmentCard, model.EquipmentCard] { 131 | return EquipmentCardAdapter{} 132 | } 133 | 134 | type weaponCardAdapterLayer struct { 135 | equipmentCardAdapterLayer 136 | weaponType enum.WeaponType 137 | } 138 | 139 | func (w *weaponCardAdapterLayer) WeaponType() enum.WeaponType { 140 | return w.weaponType 141 | } 142 | 143 | type WeaponCardAdapter struct{} 144 | 145 | func (w WeaponCardAdapter) Convert(source definition.WeaponCard) (success bool, result model.WeaponCard) { 146 | adapterLayer := &weaponCardAdapterLayer{ 147 | equipmentCardAdapterLayer: equipmentCardAdapterLayer{ 148 | cardAdapterLayer: cardAdapterLayer{ 149 | EntityImpl: implement.EntityImpl{}, 150 | cardType: source.CardType(), 151 | cost: source.Cost(), 152 | }, 153 | event: source.Modify(), 154 | equipmentType: source.EquipmentType(), 155 | }, 156 | weaponType: source.WeaponType(), 157 | } 158 | 159 | return true, adapterLayer 160 | } 161 | 162 | func NewWeaponCardAdapter() adapter.Adapter[definition.WeaponCard, model.WeaponCard] { 163 | return WeaponCardAdapter{} 164 | } 165 | -------------------------------------------------------------------------------- /protocol/http/util/preprocess.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | func convertToInt(source string) (success bool, result int) { 11 | if intResult, err := strconv.Atoi(source); err != nil { 12 | return false, 0 13 | } else { 14 | return true, intResult 15 | } 16 | } 17 | 18 | func convertToFloat(source string) (success bool, result float64) { 19 | if floatResult, err := strconv.ParseFloat(source, 64); err != nil { 20 | return false, float64(0) 21 | } else { 22 | return true, floatResult 23 | } 24 | } 25 | 26 | func convertToBool(source string) (success bool, result bool) { 27 | if boolResult, err := strconv.ParseBool(source); err != nil { 28 | return false, false 29 | } else { 30 | return true, boolResult 31 | } 32 | } 33 | 34 | func convertToUint64(source string) (success bool, result uint64) { 35 | if uint64Result, err := strconv.ParseUint(source, 10, 64); err != nil { 36 | return false, 0 37 | } else { 38 | return true, uint64Result 39 | } 40 | } 41 | 42 | func convertToStruct[T any](source string) (success bool, result T) { 43 | if err := json.Unmarshal([]byte(source), &result); err != nil { 44 | return false, result 45 | } else { 46 | return true, result 47 | } 48 | } 49 | 50 | // BindJson 将请求体中的数据绑定到给定的entity中,entity需要可寻址,失败直接返回BadRequest 51 | func BindJson[T any](ctx *gin.Context, entity T) (success bool) { 52 | if err := ctx.ShouldBindJSON(entity); err != nil { 53 | return false 54 | } else { 55 | return true 56 | } 57 | } 58 | 59 | // QueryPath 从URL进行查询,支持路径参数与查询参数 60 | func QueryPath(ctx *gin.Context, key string) (has bool, result string) { 61 | if strings.HasPrefix(key, ":") { 62 | result, has = ctx.Params.Get(strings.Trim(key, ":")) 63 | } else { 64 | result, has = ctx.GetQuery(key) 65 | } 66 | 67 | return has, result 68 | } 69 | 70 | // QueryPathInt 从URL中查询一个int类型的结果,调用QueryPath并进行转化 71 | func QueryPathInt(ctx *gin.Context, key string) (has bool, result int) { 72 | if gotten, stringResult := QueryPath(ctx, key); !gotten { 73 | return false, 0 74 | } else { 75 | return convertToInt(stringResult) 76 | } 77 | } 78 | 79 | func QueryPathUint64(ctx *gin.Context, key string) (has bool, result uint64) { 80 | if gotten, stringResult := QueryPath(ctx, key); !gotten { 81 | return false, 0 82 | } else { 83 | return convertToUint64(stringResult) 84 | } 85 | } 86 | 87 | // QueryPathFloat 从URL中查询一个float64类型的结果,调QueryPathQueryPath并进行转化 88 | func QueryPathFloat(ctx *gin.Context, key string) (has bool, result float64) { 89 | if gotten, stringResult := QueryPath(ctx, key); !gotten { 90 | return false, float64(0) 91 | } else { 92 | return convertToFloat(stringResult) 93 | } 94 | } 95 | 96 | // QueryPathBool 从URL中查询一个bool类型的结果,调QueryPathQueryPath并进行转化 97 | func QueryPathBool(ctx *gin.Context, key string) (has bool, result bool) { 98 | if gotten, stringResult := QueryPath(ctx, key); !gotten { 99 | return false, false 100 | } else { 101 | return convertToBool(stringResult) 102 | } 103 | } 104 | 105 | // QueryPathJson 从URL中查询一个json对象,调用QueryPath并进行转化 106 | func QueryPathJson[T any](ctx *gin.Context, key string) (has bool, result T) { 107 | if gotten, stringResult := QueryPath(ctx, key); !gotten { 108 | return false, result 109 | } else { 110 | return convertToStruct[T](stringResult) 111 | } 112 | } 113 | 114 | // QueryCookie 从cookie中获取指定key的值 115 | func QueryCookie(ctx *gin.Context, key string) (has bool, cookie string) { 116 | if cookieResult, err := ctx.Cookie(key); err != nil { 117 | return false, cookie 118 | } else { 119 | return true, cookieResult 120 | } 121 | } 122 | 123 | // QueryHeaders 从请求头中获取指定key的值 124 | func QueryHeaders(ctx *gin.Context, key string) (has bool, result string) { 125 | if result = ctx.GetHeader(key); result == "" { 126 | return false, "" 127 | } else { 128 | return true, result 129 | } 130 | } 131 | 132 | // QueryHeadersInt 从请求头中获取一个int类型的值 133 | func QueryHeadersInt(ctx *gin.Context, key string) (has bool, result int) { 134 | if gotten, stringResult := QueryHeaders(ctx, key); !gotten { 135 | return false, 0 136 | } else { 137 | return convertToInt(stringResult) 138 | } 139 | } 140 | 141 | // QueryHeadersFloat 从请求头中获取一个float类型的值 142 | func QueryHeadersFloat(ctx *gin.Context, key string) (has bool, result float64) { 143 | if gotten, stringResult := QueryHeaders(ctx, key); !gotten { 144 | return false, float64(0) 145 | } else { 146 | return convertToFloat(stringResult) 147 | } 148 | } 149 | 150 | // QueryHeadersBool 从请求头中获取一个bool类型的值 151 | func QueryHeadersBool(ctx *gin.Context, key string) (has bool, result bool) { 152 | if gotten, stringResult := QueryHeaders(ctx, key); !gotten { 153 | return false, false 154 | } else { 155 | return convertToBool(stringResult) 156 | } 157 | } 158 | 159 | // QueryHeadersJson 从请求头中获取一个对象 160 | func QueryHeadersJson[T any](ctx *gin.Context, key string) (has bool, result T) { 161 | if gotten, stringResult := QueryHeaders(ctx, key); !gotten { 162 | return false, result 163 | } else { 164 | return convertToStruct[T](stringResult) 165 | } 166 | } 167 | 168 | // GetClientIP 获取请求客户端的IP 169 | func GetClientIP(ctx *gin.Context) (result string) { 170 | if addr := ctx.ClientIP(); addr == "::1" { 171 | return "127.0.0.1" 172 | } else { 173 | return addr 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /docs/development/api/card_deck.md: -------------------------------------------------------------------------------- 1 | 此处为GISB的卡组相关服务,包含卡组的新增、修改、获取、删除等接口 2 | 3 | # 变量与参数 4 | 5 | + `:card_deck_id`: 表示卡组的ID 6 | + `{gisb_token_id_value}`: 表示token_id在cookie中的值,由登录接口获取 7 | + `{gisb_token_value}`: 表示token在cookie中的键,由登录接口获取 8 | 9 | # 说明 10 | 11 | 本文的所有接口均需要鉴权,您需要在请求中附带您的token 12 | 13 | # 上传卡组 14 | 15 | ## 请求方法与地址 16 | 17 | ```plain text 18 | POST {BaseURL}/card_deck 19 | ``` 20 | 21 | ## 请求/响应 22 | 23 | 24 | 25 | 26 | 27 | ### 请求 28 | 29 | 您需要提供所要上传卡组的信息 30 | 31 | #### 请求体定义 32 | 33 | ```go 34 | type UploadCardDeckRequest struct { 35 | Owner uint `json:"owner"` 36 | RequiredPackage []string `json:"required_package"` 37 | Cards []uint `json:"cards"` 38 | Characters []uint `json:"characters"` 39 | } 40 | ``` 41 | 42 | #### 请求示例 43 | 44 | ```http 45 | POST /card_deck HTTP/1.1 46 | Host: {BaseURL} 47 | Content-Type: application/json 48 | Cookie: gisb_token={gisb_token_value}; gisb_token_id={gisb_token_id_value} 49 | Content-Length: 98 50 | 51 | { 52 | "owner": 1, 53 | "required_package": ["base"], 54 | "cards": [1], 55 | "characters": [1] 56 | } 57 | ``` 58 | 59 | 60 | 61 | ### 响应 62 | 63 | 此接口将尝试把您提供的卡组上传至持久化模块,并同步更新玩家信息 64 | 65 | #### 响应体定义 66 | 67 | ```go 68 | type UploadCardDeckResponse struct { 69 | ID uint `json:"id"` 70 | Owner uint `json:"owner"` 71 | RequiredPackage []string `json:"required_package"` 72 | Cards []uint `json:"cards"` 73 | Characters []uint `json:"characters"` 74 | } 75 | ``` 76 | 77 | #### 响应示例 78 | 79 | ```json 80 | { 81 | "id": 3, 82 | "owner": 1, 83 | "required_package": [ 84 | "base" 85 | ], 86 | "cards": [ 87 | 1 88 | ], 89 | "characters": [ 90 | 1 91 | ] 92 | } 93 | ``` 94 | 95 | 96 | 97 | # 获取卡组 98 | 99 | ## 请求方法与地址 100 | 101 | ```plain text 102 | GET {BaseURL}/card_deck/:card_deck_id 103 | ``` 104 | 105 | ## 请求/响应 106 | 107 | 108 | 109 | 110 | 111 | ### 请求 112 | 113 | 本接口不需要提供请求体 114 | 115 | #### 请求示例 116 | 117 | ```http 118 | GET /card_deck/:card_deck_id HTTP/1.1 119 | Host: {BaseURL} 120 | Cookie: gisb_token={gisb_token_value}; gisb_token_id={gisb_token_id_value} 121 | ``` 122 | 123 | 124 | 125 | ### 响应 126 | 127 | 此接口将查询并返回您所给定id的卡组 128 | 129 | #### 响应体定义 130 | 131 | ```go 132 | type QueryCardDeckResponse struct { 133 | ID uint `json:"id"` 134 | Owner uint `json:"owner"` 135 | RequiredPackage []string `json:"required_package"` 136 | Cards []uint `json:"cards"` 137 | Characters []uint `json:"characters"` 138 | } 139 | ``` 140 | 141 | #### 响应示例 142 | 143 | ```json 144 | { 145 | "id": 3, 146 | "owner": 1, 147 | "required_package": [ 148 | "base" 149 | ], 150 | "cards": [ 151 | 1 152 | ], 153 | "characters": [ 154 | 1 155 | ] 156 | } 157 | ``` 158 | 159 | 160 | 161 | # 更新卡组 162 | 163 | ## 请求方法与地址 164 | 165 | ```plain text 166 | PUT {BaseURL}/card_deck/:card_deck_id 167 | ``` 168 | 169 | ## 请求/响应 170 | 171 | 172 | 173 | 174 | 175 | ### 请求 176 | 177 | 您需要提供所要更新卡组的信息 178 | 179 | #### 请求体定义 180 | 181 | ```go 182 | type UpdateCardDeckRequest struct { 183 | Owner uint `json:"owner"` 184 | RequiredPackage []string `json:"required_package"` 185 | Cards []uint `json:"cards"` 186 | Characters []uint `json:"characters"` 187 | } 188 | ``` 189 | 190 | #### 请求示例 191 | 192 | ```http 193 | PUT /card_deck/:card_deck_id HTTP/1.1 194 | Host: {BaseURL} 195 | Content-Type: application/json 196 | Cookie: gisb_token={gisb_token_value}; gisb_token_id={gisb_token_id_value} 197 | Content-Length: 101 198 | 199 | { 200 | "owner": 1, 201 | "required_package": ["enhance"], 202 | "cards": [1], 203 | "characters": [1] 204 | } 205 | ``` 206 | 207 | 208 | 209 | ### 响应 210 | 211 | 此接口将根据您所给定的id尝试更新卡组,并将结果返回 212 | 213 | #### 响应体定义 214 | 215 | ```go 216 | type UpdateCardDeckResponse struct { 217 | ID uint `json:"id"` 218 | Owner uint `json:"owner"` 219 | RequiredPackage []string `json:"required_package"` 220 | Cards []uint `json:"cards"` 221 | Characters []uint `json:"characters"` 222 | } 223 | ``` 224 | 225 | #### 响应示例 226 | 227 | ```json 228 | { 229 | "id": 3, 230 | "owner": 1, 231 | "required_package": [ 232 | "enhance" 233 | ], 234 | "cards": [ 235 | 1 236 | ], 237 | "characters": [ 238 | 1 239 | ] 240 | } 241 | ``` 242 | 243 | 244 | 245 | # 删除卡组 246 | 247 | ## 请求方法与地址 248 | 249 | ```plain text 250 | DELETE {BaseURL}/card_deck/:card_deck_id 251 | ``` 252 | 253 | ## 请求/响应 254 | 255 | 256 | 257 | 258 | 259 | ### 请求 260 | 261 | 本接口不需要提供请求体 262 | 263 | #### 请求示例 264 | 265 | ```http 266 | DELETE /card_deck/:card_deck_id HTTP/1.1 267 | Host: {BaseURL} 268 | Cookie: gisb_token={gisb_token_value}; gisb_token_id={gisb_token_id_value} 269 | ``` 270 | 271 | 272 | 273 | ### 响应 274 | 275 | 此接口将根据您给定的id尝试删除卡组 276 | 277 | #### 响应体定义 278 | 279 | 本接口没有响应体,返回HTTP Status 200即表明删除成功 280 | 281 | -------------------------------------------------------------------------------- /mod/implement/util.go: -------------------------------------------------------------------------------- 1 | package implement 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sunist-c/genius-invokation-simulator-backend/mod/definition" 6 | "github.com/sunist-c/genius-invokation-simulator-backend/util" 7 | "gopkg.in/yaml.v2" 8 | "os" 9 | "os/user" 10 | "path" 11 | "time" 12 | ) 13 | 14 | const ( 15 | maxID uint16 = 1<<16 - 1 16 | ) 17 | 18 | var ( 19 | debugFlag = false 20 | metadata = ModInfo{ 21 | ModID: 0, 22 | LastID: 0, 23 | UsedID: map[uint16]bool{}, 24 | CreatedAt: time.Time{}, 25 | UpdatedAt: time.Time{}, 26 | Author: "", 27 | } 28 | metadataPath = "" 29 | ) 30 | 31 | type ModInfo struct { 32 | ModID uint64 `yaml:"mod_id"` 33 | LastID uint16 `yaml:"last_id"` 34 | UsedID map[uint16]bool `yaml:"id_pool"` 35 | CreatedAt time.Time `yaml:"created_at"` 36 | UpdatedAt time.Time `yaml:"updated_at"` 37 | Author string `yaml:"author"` 38 | } 39 | 40 | func InitMetaData() { 41 | // 非debug模式,获取真实ModID 42 | if !debugFlag { 43 | // 尝试读取配置 44 | if pwd, err := os.Getwd(); err != nil { 45 | // 无法获取pwd,意味着无法写入也无法读取,报panic 46 | panic(fmt.Sprintf("cannot get current working directory: %v", err)) 47 | } else if bytes, err := os.ReadFile(path.Join(pwd, "metadata.yml")); err == nil { 48 | metadataPath = path.Join(pwd, "metadata.yml") 49 | 50 | // 存在metadata.yml,尝试加载 51 | if yaml.Unmarshal(bytes, &metadata) != nil { 52 | // 反序列化metadata.yml失败,报panic 53 | panic(fmt.Sprintf("cannot unmarshal metadata.yml: %v", err)) 54 | } else { 55 | // 反序列化metadata.yml成功,加载 56 | return 57 | } 58 | } else { 59 | // 不存在metadata.yml,生成 60 | metadataPath = path.Join(pwd, "metadata.yml") 61 | 62 | // 生成ModID 63 | if macAddresses, err := util.GetMacAddresses(); err != nil { 64 | // 无法获取mac地址,实际环境不存在这种情况,报panic 65 | panic(fmt.Sprintf("couldn't get mac addresses: %v", err)) 66 | } else if len(macAddresses) == 0 { 67 | // mac地址不存在,实际环境不存在这种情况,报panic 68 | panic(fmt.Sprintf("couldn't get mac addresses: %v", err)) 69 | } else if uintMacAddress, err := util.GetUintMacAddress(macAddresses[0]); err != nil { 70 | // mac地址不合法,实际环境不存在这种情况,报panic 71 | panic(fmt.Sprintf("couldn't get mac address: %v", err)) 72 | } else { 73 | // 成功获取mac地址,作为设备码使用自定义雪花算法生成UID 74 | metadata.ModID = util.GenerateUID(uintMacAddress, time.Now()) 75 | } 76 | 77 | // 生成Author 78 | if currentUser, err := user.Current(); err != nil { 79 | // 获取CurrentUser失败,设置为Unknown 80 | metadata.Author = "Unknown" 81 | } else { 82 | // 成功获取CurrentUser 83 | metadata.Author = currentUser.Name 84 | } 85 | 86 | // 生成metadata上下文 87 | metadata.LastID = 0 88 | metadata.UsedID = map[uint16]bool{} 89 | 90 | // 生成TimeStamp 91 | metadata.CreatedAt = time.Now() 92 | 93 | // 将生成的metadata写入metadata.yml 94 | if err := flushMetadata(); err != nil { 95 | // 写入metadata.yml失败,报panic 96 | panic(err) 97 | } 98 | } 99 | } 100 | } 101 | 102 | func flushMetadata() error { 103 | if !debugFlag { 104 | metadata.UpdatedAt = time.Now() 105 | if oStream, err := yaml.Marshal(metadata); err != nil { 106 | // 序列化metadata.yml失败,实际环境不存在这种情况 107 | return fmt.Errorf("marshal metadata.yml failed: %v", err) 108 | } else if file, err := os.OpenFile(metadataPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755); err != nil { 109 | // 无法打开metadata.yml,实际环境不存在这种情况 110 | return fmt.Errorf("cannot open metadata.yml: %v", err) 111 | } else if _, err := file.Write(oStream); err != nil { 112 | // 无法写入metadata.yml,实际环境不存在这种情况 113 | return fmt.Errorf("cannot write metadata.yml: %v", err) 114 | } else { 115 | // 成功写入metadata.yml 116 | return nil 117 | } 118 | } else { 119 | return nil 120 | } 121 | } 122 | 123 | func SetDebugFlag(flag bool) { 124 | debugFlag = flag 125 | } 126 | 127 | func ModID() uint64 { 128 | if debugFlag { 129 | return 109951162778600 130 | } else if metadata.ModID != 0 { 131 | return metadata.ModID 132 | } else { 133 | InitMetaData() 134 | return metadata.ModID 135 | } 136 | } 137 | 138 | // NextID 使用内置的ID分配工具获取一个可用的不与其余被托管ID冲突的自增ID 139 | func NextID() uint16 { 140 | defer func() { 141 | err := flushMetadata() 142 | if err != nil { 143 | panic(err) 144 | } 145 | }() 146 | 147 | if !metadata.UsedID[metadata.LastID+1] { 148 | metadata.LastID += 1 149 | metadata.UsedID[metadata.LastID] = true 150 | return metadata.LastID 151 | } else { 152 | for i := uint32(1); i <= uint32(maxID); i++ { 153 | if !metadata.UsedID[uint16(i)] { 154 | metadata.LastID = uint16(i) 155 | metadata.UsedID[uint16(i)] = true 156 | return uint16(i) 157 | } 158 | } 159 | } 160 | 161 | panic("entity id overflow") 162 | } 163 | 164 | // UseID 使用内置的ID分配工具分配一个指定的ID,若不可用,则分配一个自增的ID 165 | func UseID(want uint16) (success bool, result uint16) { 166 | defer func() { 167 | err := flushMetadata() 168 | if err != nil { 169 | panic(err) 170 | } 171 | }() 172 | 173 | if !metadata.UsedID[want] { 174 | // 成功分配want作为ID 175 | metadata.UsedID[want] = true 176 | return true, want 177 | } else { 178 | // 分配失败,从want开始查找下一个可用ID并分配 179 | for i := uint32(want); i <= uint32(maxID); i++ { 180 | if !metadata.UsedID[uint16(i)] { 181 | metadata.LastID = uint16(i) 182 | metadata.UsedID[uint16(i)] = true 183 | return false, uint16(i) 184 | } 185 | } 186 | 187 | // 分配失败,ID溢出,报panic 188 | panic("entity id overflow") 189 | } 190 | } 191 | 192 | func RegisterMod(mod definition.Mod) (success bool) { 193 | // todo: implement me 194 | panic("not implemented yet") 195 | } 196 | --------------------------------------------------------------------------------