Compare commits

...

No commits in common. "master" and "release-please--branches--master--components--minotaur" have entirely different histories.

677 changed files with 90872 additions and 151 deletions

2
.codacy.yml Normal file
View File

@ -0,0 +1,2 @@
exclude:
- "**/*.md"

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
.github/images/pod.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

1
.github/images/server-gdi.svg vendored Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

BIN
.github/images/yc-cpu.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
.github/images/yc-event.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
.github/images/yc-memory.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
.github/images/yc1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
.github/images/yc2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

1
.github/netpoll vendored Submodule

@ -0,0 +1 @@
Subproject commit bb9c3f74620c19b139d13c20793f9ff016d302c0

View File

@ -0,0 +1,37 @@
title This is a title
participantspacing equal
#participantspacing gives manual control of spacing between participants, equal: equal distance between all participants, number i.e. 20.5: minimum space
actor Client#lightgreen
#supported participant types: participant, actor, boundary, control, entity, database
participantgroup #lightgreen Minotaur Server
participant Server
participant Shunt
abox left of Shunt: System 消息将全局单线程执行Shunt 消息将会\n在连接当前所在分流渠道内执行。相同分流渠道的\n消息将串行处理不同分流渠道消息并行处理。\n\n连接可根据业务场景灵活的通过 srv.UseShunt 来\n切换当前所处的分流渠道
abox left of Shunt: 异步消息\n\n(SystemMessage) srv.PushAsyncMessage\n(ShuntMessage) srv.PushShuntAsyncMessage\n(SystemMessage) srv.PushUniqueAsyncMessage\n(ShuntMessage) srv.PushUniqueShuntAsyncMessage\n\n Unique 消息将会在上一个相同消息未执行完毕\n的情况下忽略后续消息
end
Client->Server:通过 WebSocket、TCP、UDP、KCP 等协议与服务器建立连接
loop Write Loop
Server ->Client:写入数据包
abox left of Server: 数据包将被写入对应连接的缓冲区内等待发送 ,写入\n缓冲区后逻辑视为处理完毕网络 IO 不会阻塞分流渠道
end
Server -->Shunt: (SystemMessage) OnConnectionOpenedEvent
Shunt --> Shunt: 消息处理
Server -->Shunt: (ShuntMessage) OnConnectionOpenedAfterEvent
Shunt --> Shunt: 消息处理
loop Read Loop
Client->Server:发送数据包
abox right of Client: 数据包将被发送到连接对应分流渠道的缓冲区内
Server -->Shunt: (ShuntMessage) OnConnectionReceivePacketEvent
Shunt --> Shunt: 消息处理
Shunt --> Server: 回复消息
Server --> Server: 写入 Write Loop
end
Client <->Server: 断开或关闭连接
Server -->Shunt: (ShuntMessage) OnConnectionClosedEvent
Shunt --> Shunt: 消息处理

61
.github/workflows/codacy.yml vendored Normal file
View File

@ -0,0 +1,61 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow checks out code, performs a Codacy security scan
# and integrates the results with the
# GitHub Advanced Security code scanning feature. For more information on
# the Codacy security scan action usage and parameters, see
# https://github.com/codacy/codacy-analysis-cli-action.
# For more information on Codacy Analysis CLI in general, see
# https://github.com/codacy/codacy-analysis-cli.
name: Codacy Security Scan
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '43 2 * * 4'
permissions:
contents: read
jobs:
codacy-security-scan:
permissions:
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
name: Codacy Security Scan
runs-on: ubuntu-latest
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout code
uses: actions/checkout@v3
# Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis
- name: Run Codacy Analysis CLI
uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b
with:
# Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository
# You can also omit the token and run the tools that support default configurations
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
verbose: true
output: results.sarif
format: sarif
# Adjust severity of non-security issues
gh-code-scanning-compat: true
# Force 0 exit code to allow SARIF file generation
# This will handover control about PR rejection to the GitHub side
max-allowed-issues: 2147483647
# Upload the SARIF file generated in the previous step
- name: Upload SARIF results file
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: results.sarif

76
.github/workflows/codeql.yml vendored Normal file
View File

@ -0,0 +1,76 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '39 18 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

20
.github/workflows/dependency-review.yml vendored Normal file
View File

@ -0,0 +1,20 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v3
- name: 'Dependency Review'
uses: actions/dependency-review-action@v2

21
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,21 @@
on:
push:
branches:
- master
name: Release
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: GoogleCloudPlatform/release-please-action@v3
id: release
with:
token: ${{ secrets.RELEASE_TOKEN }}
release-type: go
package-name: minotaur
bump-minor-pre-major: true
bump-patch-for-minor-pre-major: true
changelog-types: '[{"type":"other","section":"Other | 其他更改","hidden":false},{"type":"revert","section":"Reverts | 回退","hidden":false},{"type":"feat","section":"Features | 新特性","hidden":false},{"type":"fix","section":"Bug Fixes | 修复","hidden":false},{"type":"improvement","section":"Feature Improvements | 改进","hidden":false},{"type":"docs","section":"Docs | 文档优化","hidden":false},{"type":"style","section":"Styling | 可读性优化","hidden":false},{"type":"refactor","section":"Code Refactoring | 重构","hidden":false},{"type":"perf","section":"Performance Improvements | 性能优化","hidden":false},{"type":"test","section":"Tests | 新增或优化测试用例","hidden":false},{"type":"build","section":"Build System | 影响构建的修改","hidden":false},{"type":"ci","section":"CI | 更改我们的 CI 配置文件和脚本","hidden":false}]'
# release-as: 0.5.0

168
.gitignore vendored Normal file
View File

@ -0,0 +1,168 @@
### Minotaur
old/app/monopoly/
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
.refer
.refer/*

1771
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

128
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
kercylan@gmail.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

36
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,36 @@
# 贡献指南
感谢您对 Minotaur 项目的兴趣!
欢迎任何人的贡献,无论是新手还是有经验的开发者。以下是一些关于如何开始贡献的指导。
## 开始之前
1. **了解项目**:请先阅读项目的 [README.md](README.md) 和其他文档,了解项目的目的和结构。
2. **遵循行为准则**:请确保您的贡献符合我们的 [行为准则](CODE_OF_CONDUCT.md)。
## 报告问题
发现了问题或有新的想法?请通过 GitHub Issues 提交。
1. **查找现有问题**:在提交之前,请搜索现有的问题,看看是否有人已经报告了相同的问题。
2. **提供详细信息**:在报告问题时,请提供尽可能多的详细信息,包括错误消息、操作系统、版本号等。
## 贡献代码
想要为 Minotaur 项目贡献代码?请按照以下步骤操作:
1. **Fork 项目**:点击 GitHub 页面右上角的 "Fork" 按钮。
2. **克隆您的 Fork**:在您的本地计算机上克隆您的 Fork。
3. **创建新分支**:为您的修改创建一个新分支。
4. **提交您的更改**:在新分支上进行修改,并提交您的更改。
5. **发送 Pull Request**:返回您的 Fork 在 GitHub 上的页面,并点击 "New Pull Request" 按钮。
## 代码风格
请确保您的代码符合项目的编码风格和规范。
## 联系我们
如果您有任何问题或需要帮助,请通过以下方式与我取得联系:
- 发送电子邮件至kercylan@gmail.com

View File

@ -1,150 +0,0 @@
Originally published at https://rakyll.org/coredumps/.
---
Debugging is highly useful to examine the execution flow
and to understand the current state of a program.
A core file is a file that contains the memory dump of a running
process and its process status. It is primarily used for post-mortem
debugging of a program, as well as to understand a program's state
while it is still running. These two cases make debugging of core dumps
a good diagnostics aid to postmortem and analyze production
services.
I will use a simple hello world web server in this article,
but in real life our programs might get very
complicated easily.
The availability of core dump analysis gives you an
opportunity to resurrect a program from specific snapshot
and look into cases that might only reproducible in certain
conditions/environments.
__Note__: This flow only works on Linux at this point end-to-end,
I am not quite sure about the other Unixes but it is not
yet supported on macOS. Windows is not supported at this point.
Before we begin, you need to make sure that your ulimit
for core dumps are at a reasonable level. It is by default
0 which means the max core file size can only be zero.
I usually set it to unlimited on my development machine by typing:
```
$ ulimit -c unlimited
```
Then, make sure you have [delve](https://github.com/derekparker/delve)
installed on your machine.
Here is a `main.go` that contains a simple handler and it starts an HTTP server.
```
$ cat main.go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "hello world\n")
})
log.Fatal(http.ListenAndServe("localhost:7777", nil))
}
```
Let's build this and have a binary.
```
$ go build .
```
Lets assume, in the future, there is something messy going on with
this server but you are not so sure about what it might be.
You might have instrumented your program in various ways but it
might not be enough for getting any clue from the existing
instrumentation data.
Basically, in a situation like this, it would be nice to have a
snapshot of the current process, and then use that snapshot to dive
into to the current state of your program with your existing debugging
tools.
There are several ways to obtain a core file. You might have been
already familiar with crash dumps, these are basically core dumps
written to disk when a program is crashing. Go doesn't enable crash dumps
by default but gives you this option on Ctrl+backslash when
`GOTRACEBACK` env variable is set to "crash".
```
$ GOTRACEBACK=crash ./hello
(Ctrl+\)
```
It will crash the program with stack trace printed and core dump file
will be written.
Another option is to retrieve a core dump from a running process
without having to kill a process.
With `gcore`, it is possible to get the core
files without crashing. Lets start the server again:
```
$ ./hello &
$ gcore 546 # 546 is the PID of hello.
```
We have a dump without crashing the process. The next step
is to load the core file to delve and start analyzing.
```
$ dlv core ./hello core.546
```
Alright, this is it! This is no different than the typical delve interactive.
You can backtrace, list, see variables, and more. Some features will be disabled
given a core dump is a snapshot and not a currently running process, but
the execution flow and the program state will be entirely accessible.
```
(dlv) bt
0 0x0000000000457774 in runtime.raise
at /usr/lib/go/src/runtime/sys_linux_amd64.s:110
1 0x000000000043f7fb in runtime.dieFromSignal
at /usr/lib/go/src/runtime/signal_unix.go:323
2 0x000000000043f9a1 in runtime.crash
at /usr/lib/go/src/runtime/signal_unix.go:409
3 0x000000000043e982 in runtime.sighandler
at /usr/lib/go/src/runtime/signal_sighandler.go:129
4 0x000000000043f2d1 in runtime.sigtrampgo
at /usr/lib/go/src/runtime/signal_unix.go:257
5 0x00000000004579d3 in runtime.sigtramp
at /usr/lib/go/src/runtime/sys_linux_amd64.s:262
6 0x00007ff68afec330 in (nil)
at :0
7 0x000000000040f2d6 in runtime.notetsleep
at /usr/lib/go/src/runtime/lock_futex.go:209
8 0x0000000000435be5 in runtime.sysmon
at /usr/lib/go/src/runtime/proc.go:3866
9 0x000000000042ee2e in runtime.mstart1
at /usr/lib/go/src/runtime/proc.go:1182
10 0x000000000042ed04 in runtime.mstart
at /usr/lib/go/src/runtime/proc.go:1152
(dlv) ls
> runtime.raise() /usr/lib/go/src/runtime/sys_linux_amd64.s:110 (PC: 0x457774)
105: SYSCALL
106: MOVL AX, DI // arg 1 tid
107: MOVL sig+0(FP), SI // arg 2
108: MOVL $200, AX // syscall - tkill
109: SYSCALL
=> 110: RET
111:
112: TEXT runtime·raiseproc(SB),NOSPLIT,$0
113: MOVL $39, AX // syscall - getpid
114: SYSCALL
115: MOVL AX, DI // arg 1 pid
```

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 kercylan98
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

331
README.md
View File

@ -1,2 +1,331 @@
# vRp.CD2g_test # Minotaur
Minotaur 是一个用于服务端开发的支持库,其中采用了大量泛型设计,主要被用于游戏服务器开发,但由于拥有大量通用的功能,也常被用于 WEB 开发。
***
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![license](https://img.shields.io/github/license/kercylan98/minotaur)
<a target="_blank" href="https://goreportcard.com/report/github.com/kercylan98/minotaur"><img src="https://goreportcard.com/badge/github.com/kercylan98/minotaur?style=flat-square" /></a>
![go version](https://img.shields.io/github/go-mod/go-version/kercylan98/minotaur?logo=go&style=flat)
![tag](https://img.shields.io/github/v/tag/kercylan98/minotaur?logo=github&style=flat)
![views](https://komarev.com/ghpvc/?username=kercylan98&color=blue&style=flat)
![email](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat&logo=gmail&link=mailto:kercylan@gmail.com)
![qq-group](https://img.shields.io/badge/QQ%20Group-758219443-green.svg?style=flat&logo=tencent-qq&link=https://qm.qq.com/cgi-bin/qm/qr?k=WzRWJIDLzuJbH6-VjdFiTCd1_qA_Ug-D&jump_from=webapi&authKey=ktLEw3XyY9yO+i9rPbI6Fk0UA0uEhACcUidOFdblaiToZtbHcXyU7sFb31FEc9JJ&noverify=0)
![telegram](https://img.shields.io/badge/Telegram-ziv__siren-green.svg?style=flat&logo=telegram&link=https://telegram.me/ziv_siren)
> - 这是支持快速搭建多功能游戏服务器及 HTTP 服务器的 `Golang` 服务端框架;
> - 网络传输基于 [`gorilla/websocket`](https://github.com/gorilla/websocket)、[`gin-gonic/gin`](https://github.com/gin-gonic/gin)、[`grpc/grpc-go`](https://github.com/grpc/grpc-go)、[`panjf2000/gnet`](https://github.com/panjf2000/gnet)、[`xtaci/kcp-go`](https://github.com/xtaci/kcp-go) 构建;
> - 该项目的目标是提供一个简单、高效、可扩展的游戏服务器框架,让开发者可以专注于游戏逻辑的开发,而不用花费大量时间在网络传输、配置导表、日志、监控等基础功能的开发上;
***
在 Minotaur 中不包括任何跨服实现,但支持基于多级路由器快速实现跨服功能。推荐使用 [`NATS.io`](https://nats.io/) 作为跨服消息中间件。
- 目前已实践的弹幕游戏项目以 `NATS.io` 作为消息队列,实现了跨服、埋点日志收集等功能,部署在 `Kubernetes` 集群中;
- 该项目客户端与服务端采用 `WebSocket` 进行通讯,服务端暴露 `HTTP` 接口接收互动数据消息回调,通过负载均衡器进入 `Kubernetes` 集群中的 `Minotaur` 服务,最终通过 `NATS.io` 消息队列转发至对应所在的 `Pod` 中进行处理;
<details>
<summary>关于 Pod 配置参数及非极限压测数据</summary>
> 本次压测 `Pod` 扩容数量为 1但由于压测连接是最开始就建立好的所以该扩容的 `Pod` 并没有接受到压力。
> 理论上来说该 `Pod` 也应该接受 `HTTP` 回调压力,实测过程中,这个扩容的 `Pod` 没有接受到任何压力
**Pod 配置参数**
![pod](.github/images/pod.png)
**压测结果**
![压测数据](.github/images/yc1.png)
![压测数据](.github/images/yc2.png)
**监控数据**
![事件](./.github/images/yc-event.png)
![CPU](./.github/images/yc-cpu.png)
![内存](./.github/images/yc-memory.png)
</details>
***
## 特色内容
```mermaid
mindmap
root((Minotaur))
所有功能均为可选的,引入即用
基于动态分流的异步消息支持
兼容各类实现的帧同步组件
多级消息路由器,轻松搭建跨服服务
开箱即用的配置导表工具
强大的 utils 包
常用的切片、map、随机、时间、数学、文件、类型转换等工具函数
适用于定时、编排、组合、匹配、校验等行为的组件
版本比较、重试、耗时计数等super package
活动、任务、AOI、寻路、战斗、移动、房间等通用游戏组件
针对扑克牌玩法的大量支持
```
## Server 架构预览
![server-gdi.svg](.github/images/server-gdi.svg)
## 安装
注意:依赖于 **[Go](https://go.dev/) 1.20 +**
运行以下 Go 命令来安装软件包:`minotaur`
```sh
$ go get -u github.com/kercylan98/minotaur
```
## 用法
- 在`Minotaur`中大量使用了 **[泛型](https://go.dev/doc/tutorial/generics)** 、 **[观察者(事件)](https://www.runoob.com/design-pattern/observer-pattern.html)** 和 **[选项模式](https://juejin.cn/post/6844903729313873927)**,在使用前建议先进行相应了解;
- 项目文档可访问 **[pkg.go.dev](https://pkg.go.dev/github.com/kercylan98/minotaur)** 进行查阅;
### 本地文档
可使用 `godoc` 搭建本地文档服务器
#### 安装 godoc
```shell
git clone golang.org/x/tools
cd tools/cmd
go install ...
```
#### 使用 `godoc` 启动本地文档服务器
```shell
godoc -http=:9998 -play
```
#### Windows
```shell
.\local-doc.bat
```
#### Linux or MacOS
```shell
chmod 777 ./local-doc.sh
./local-doc.sh
```
#### 文档地址
- **[http://localhost:9998/pkg/github.com/kercylan98/minotaur/](http://localhost:9998/pkg/github.com/kercylan98/minotaur/)**
- **[https://pkg.go.dev/github.com/kercylan98/minotaur](https://pkg.go.dev/github.com/kercylan98/minotaur)**
### 简单回响服务器
创建一个基于`Websocket`创建的单线程回响服务器。
```go
package main
import (
"github.com/kercylan98/minotaur/server"
)
func main() {
srv := server.New(server.NetworkWebsocket)
srv.RegConnectionReceivePacketEvent(func(srv *server.Server, conn *server.Conn, packet []byte) {
conn.Write(packet)
})
if err := srv.Run(":9999"); err != nil {
panic(err)
}
}
```
访问 **[WebSocket 在线测试](http://www.websocket-test.com/)** 进行验证。
> Websocket地址: ws://127.0.0.1:9999
### 分流服务器
分流服务器可以将消息分流到不同的分组上,每个分组中为串行处理,不同分组之间并行处理。
> 关于分流服务器的思考:
> - 当游戏需要以房间的形式进行时,应该确保相同房间的玩家处于同一分流中,不同房间的玩家处于不同分流中,这样可以避免不同房间的玩家之间的消息互相阻塞;
> - 这时候网络 IO 应该根据不同的游戏类型而进行不同的处理,例如回合制可以同步执行,而实时游戏应该采用异步执行;
> - 当游戏大部分时候以单人游戏进行时,应该每个玩家处于自身唯一的分流中,此时非互动的消息造成的网络 IO 采用同步执行即可,也不会阻塞到其他玩家的消息处理;
```go
package main
import "github.com/kercylan98/minotaur/server"
func main() {
srv := server.New(server.NetworkWebsocket)
srv.RegConnectionOpenedEvent(func(srv *server.Server, conn *server.Conn) {
// 通过 user_id 进行分流,不同用户的消息将不会互相阻塞
srv.UseShunt(conn, conn.Gata("user_id").(string))
})
srv.RegConnectionReceivePacketEvent(func(srv *server.Server, conn *server.Conn, packet []byte) {
var roomId = "default"
switch string(packet) {
case "JoinRoom":
// 将用户所处的分流渠道切换到 roomId 渠道,此刻同一分流渠道的消息将会按队列顺序处理
srv.UseShunt(conn, roomId)
case "LeaveRoom":
// 将用户所处分流切换为用户自身的分流渠道
srv.UseShunt(conn, conn.Gata("user_id").(string))
}
})
if err := srv.Run(":9999"); err != nil {
panic(err)
}
}
```
> 该示例中模拟了用户分流渠道在自身渠道和房间渠道切换的过程,通过`UseShunt`对连接分流渠道进行设置,提高并发处理能力。
### 服务器死锁检测
`Minotaur`内置了服务器消息死锁检测功能,可通过`server.WithDeadlockDetect`进行开启。
```go
package main
import (
"github.com/kercylan98/minotaur/server"
"time"
)
func main() {
srv := server.New(server.NetworkWebsocket,
server.WithDeadlockDetect(time.Second*5),
)
srv.RegConnectionReceivePacketEvent(func(srv *server.Server, conn *server.Conn, packet []byte) {
time.Sleep(10 * time.Second)
conn.Write(packet)
})
if err := srv.Run(":9999"); err != nil {
panic(err)
}
}
```
> 在开启死锁检测的时候需要设置一个合理的死锁怀疑时间,该时间内消息没有处理完毕则会触发死锁检测,并打印`WARN`级别的日志输出。
### 计时器
在默认的`server.Server`不会包含计时器功能,可通过`server.WithTicker`进行开启,例如:
```go
package main
import "github.com/kercylan98/minotaur/server"
func main() {
srv := server.New(server.NetworkWebsocket, server.WithTicker(-1, 50, 10, false))
if err := srv.Run(":9999"); err != nil {
panic(err)
}
}
```
也可以通过`timer.GetTicker`获取计时器进行使用,例如:
```go
package main
import (
"fmt"
"github.com/kercylan98/minotaur/utils/timer"
"github.com/kercylan98/minotaur/utils/times"
"sync"
)
func main() {
var ticker = timer.GetTicker(10)
var wait sync.WaitGroup
wait.Add(3)
ticker.Loop("LOOP", timer.Instantly, times.Second, timer.Forever, func() {
fmt.Println("LOOP")
wait.Done()
})
wait.Wait()
}
```
在分布式环境中,如果存在类似于多服务器需要同时间刷新配置时,可使用`Cron`表达式设置定时任务。
### 基于`xlsx`文件的配置导出工具
该导出器的`xlsx`文件配置使用`JSON`语法进行复杂类型配置,具体可参考图例
- **[`planner/pce/exporter`](planner/pce/exporter)** 是实现了基于`xlsx`文件的配置导出工具,可直接编译成可执行文件使用;
- **[`planner/pce/exporter/xlsx_template.xlsx`](planner/pce/exporter/xlsx_template.xlsx)** 是导出工具的模板文件,其中包含了具体的规则说明。
- 模板文件图例:
![exporter-xlsx-template.png](.github/images/exporter-xlsx-template.png)
#### 导出 JSON 文件(可供客户端直接使用,包含索引的配置导出后为键值模式,可直接读取)
```text
Flags:
-e, --exclude string excluded configuration names or display names (comma separated) | 排除的配置名或显示名(英文逗号分隔)
-h, --help help for json
-o, --output string directory path of the output json file | 输出的 json 文件所在目录路径
-p, --prefix string export configuration file name prefix | 导出配置文件名前缀
-t, --type string export server configuration[s] or client configuration[c] | 导出服务端配置[s]还是客户端配置[c]
-f, --xlsx string xlsx file path or directory path | xlsx 文件路径或所在目录路径
```
```shell
expoter.exe json -t s -f xlsx_template.xlsx -o ./output
```
导出结果示例
```json
{
"1": {
"b": {
"Id": 1,
"Count": "b",
"Info": {
"id": 1,
"name": "小明",
"info": {
"lv": 1,
"exp": {
"mux": 10,
"count": 100
}
}
},
"Other": [
{
"id": 1,
"name": "张飞"
},
{
"id": 2,
"name": "刘备"
}
]
}
}
}
```
#### 导出 Golang 文件
```text
Flags:
-e, --exclude string excluded configuration names or display names (comma separated) | 排除的配置名或显示名(英文逗号分隔)
-h, --help help for go
-o, --output string output path | 输出的 go 文件路径
-f, --xlsx string xlsx file path or directory path | xlsx 文件路径或所在目录路径
```
```shell
expoter.exe go -f xlsx_template.xlsx -o ./output
```
使用示例
```go
package main
import (
"fmt"
"config"
)
func main() {
fmt.Println(config.EasyConfig.Id)
}
```
### 持续更新的示例项目
- **[Minotaur-Example](https://github.com/kercylan98/minotaur-example)**
### 贡献者列表
<a href="https://github.com/kercylan98/minotaur/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kercylan98/minotaur" />
</a>
#### 参与贡献请参考 **[CONTRIBUTING.md](CONTRIBUTING.md)** 贡献指南。
### 联系方式
- **[Email: kercylan@gmail.com](mailto:kercylan@gmail.com)**
- **[Telegram: ziv_siren](https://telegram.me/ziv_siren)**
# JetBrains OS licenses
`Minotaur` had been being developed with `GoLand` IDE under the **free JetBrains Open Source license(s)** granted by JetBrains s.r.o., hence I would like to express my thanks here.
<a href="https://www.jetbrains.com/?from=minotaur" target="_blank"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png?_gl=1*1vt713y*_ga*MTEzMjEzODQxNC4xNjc5OTY3ODUw*_ga_9J976DJZ68*MTY4ODU0MDUyMy4yMC4xLjE2ODg1NDA5NDAuMjUuMC4w&_ga=2.261225293.1519421387.1688540524-1132138414.1679967850" width="250" align="middle"/></a>

92
configuration/README.md Normal file
View File

@ -0,0 +1,92 @@
# Configuration
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
configuration 基于配置导表功能实现的配置加载及刷新功能
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 包级函数定义
|函数名称|描述
|:--|:--
|[Init](#Init)|配置初始化
|[Load](#Load)|加载配置
|[Refresh](#Refresh)|刷新配置
|[WithTickerLoad](#WithTickerLoad)|通过定时器加载配置
|[StopTickerLoad](#StopTickerLoad)|停止通过定时器加载配置
|[RegConfigRefreshEvent](#RegConfigRefreshEvent)|当配置刷新时将立即执行被注册的事件处理函数
|[OnConfigRefreshEvent](#OnConfigRefreshEvent)|暂无描述...
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`STRUCT`|[RefreshEventHandle](#struct_RefreshEventHandle)|配置刷新事件处理函数
|`INTERFACE`|[Loader](#struct_Loader)|配置加载器
</details>
***
## 详情信息
#### func Init(loader ...Loader)
<span id="Init"></span>
> 配置初始化
> - 在初始化后会立即加载配置
***
#### func Load()
<span id="Load"></span>
> 加载配置
> - 加载后并不会刷新线上配置,需要执行 Refresh 函数对线上配置进行刷新
***
#### func Refresh()
<span id="Refresh"></span>
> 刷新配置
***
#### func WithTickerLoad(ticker *timer.Ticker, interval time.Duration)
<span id="WithTickerLoad"></span>
> 通过定时器加载配置
> - 通过定时器加载配置后,会自动刷新线上配置
> - 调用该函数后不会立即刷新,而是在 interval 后加载并刷新一次配置,之后每隔 interval 加载并刷新一次配置
***
#### func StopTickerLoad()
<span id="StopTickerLoad"></span>
> 停止通过定时器加载配置
***
#### func RegConfigRefreshEvent(handle RefreshEventHandle)
<span id="RegConfigRefreshEvent"></span>
> 当配置刷新时将立即执行被注册的事件处理函数
***
#### func OnConfigRefreshEvent()
<span id="OnConfigRefreshEvent"></span>
***
<span id="struct_RefreshEventHandle"></span>
### RefreshEventHandle `STRUCT`
配置刷新事件处理函数
```go
type RefreshEventHandle func()
```
<span id="struct_Loader"></span>
### Loader `INTERFACE`
配置加载器
```go
type Loader interface {
Load()
Refresh()
}
```

73
configuration/config.go Normal file
View File

@ -0,0 +1,73 @@
package configuration
import (
"github.com/kercylan98/minotaur/utils/log"
"github.com/kercylan98/minotaur/utils/timer"
"time"
)
const (
tickerLoadRefresh = "_tickerLoadRefresh"
)
var (
cTicker *timer.Ticker
cInterval time.Duration
cLoader []Loader
)
// Init 配置初始化
// - 在初始化后会立即加载配置
func Init(loader ...Loader) {
cLoader = loader
Load()
Refresh()
}
// Load 加载配置
// - 加载后并不会刷新线上配置,需要执行 Refresh 函数对线上配置进行刷新
func Load() {
defer func() {
if err := recover(); err != nil {
log.Error("Config", log.String("Action", "Load"), log.Err(err.(error)))
}
}()
for _, loader := range cLoader {
loader.Load()
}
}
// Refresh 刷新配置
func Refresh() {
defer func() {
if err := recover(); err != nil {
log.Error("Config", log.String("Action", "Refresh"), log.Err(err.(error)))
}
OnConfigRefreshEvent()
}()
for _, loader := range cLoader {
loader.Refresh()
}
}
// WithTickerLoad 通过定时器加载配置
// - 通过定时器加载配置后,会自动刷新线上配置
// - 调用该函数后不会立即刷新,而是在 interval 后加载并刷新一次配置,之后每隔 interval 加载并刷新一次配置
func WithTickerLoad(ticker *timer.Ticker, interval time.Duration) {
if ticker != cTicker && cTicker != nil {
cTicker.StopTimer(tickerLoadRefresh)
}
cTicker = ticker
cInterval = interval
cTicker.Loop(tickerLoadRefresh, cInterval, cInterval, timer.Forever, func() {
Load()
Refresh()
})
}
// StopTickerLoad 停止通过定时器加载配置
func StopTickerLoad() {
if cTicker != nil {
cTicker.StopTimer(tickerLoadRefresh)
}
}

2
configuration/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package configuration 基于配置导表功能实现的配置加载及刷新功能
package configuration

17
configuration/event.go Normal file
View File

@ -0,0 +1,17 @@
package configuration
// RefreshEventHandle 配置刷新事件处理函数
type RefreshEventHandle func()
var configRefreshEventHandles []func()
// RegConfigRefreshEvent 当配置刷新时将立即执行被注册的事件处理函数
func RegConfigRefreshEvent(handle RefreshEventHandle) {
configRefreshEventHandles = append(configRefreshEventHandles, handle)
}
func OnConfigRefreshEvent() {
for _, handle := range configRefreshEventHandles {
handle()
}
}

10
configuration/loader.go Normal file
View File

@ -0,0 +1,10 @@
package configuration
// Loader 配置加载器
type Loader interface {
// Load 加载配置
// - 加载后并不会刷新线上配置,需要执行 Refresh 函数对线上配置进行刷新
Load()
// Refresh 刷新线上配置
Refresh()
}

View File

@ -0,0 +1,15 @@
# Main
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
</details>
***

View File

@ -0,0 +1,19 @@
package main
import (
"github.com/kercylan98/minotaur/server"
"time"
)
func main() {
srv := server.New(server.NetworkWebsocket,
server.WithDeadlockDetect(time.Second*5),
)
srv.RegConnectionReceivePacketEvent(func(srv *server.Server, conn *server.Conn, packet []byte) {
time.Sleep(10 * time.Second)
conn.Write(packet)
})
if err := srv.Run(":9999"); err != nil {
panic(err)
}
}

View File

@ -0,0 +1,15 @@
# Main
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
</details>
***

View File

@ -0,0 +1,18 @@
package main
import (
"github.com/kercylan98/minotaur/server"
)
func main() {
srv := server.New(server.NetworkWebsocket)
srv.RegConnectionOpenedEvent(func(srv *server.Server, conn *server.Conn) {
srv.UseShunt(conn, conn.GetData("room_id").(string))
})
srv.RegConnectionReceivePacketEvent(func(srv *server.Server, conn *server.Conn, packet []byte) {
conn.Write(packet)
})
if err := srv.Run(":9999"); err != nil {
panic(err)
}
}

View File

@ -0,0 +1,15 @@
# Main
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
</details>
***

View File

@ -0,0 +1,13 @@
package main
import "github.com/kercylan98/minotaur/server"
func main() {
srv := server.New(server.NetworkWebsocket)
srv.RegConnectionReceivePacketEvent(func(srv *server.Server, conn *server.Conn, packet []byte) {
conn.Write(packet)
})
if err := srv.Run(":9999"); err != nil {
panic(err)
}
}

View File

@ -0,0 +1,15 @@
# Main
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
</details>
***

View File

@ -0,0 +1,10 @@
package main
import "github.com/kercylan98/minotaur/server"
func main() {
srv := server.New(server.NetworkWebsocket, server.WithTicker(-1, 50, 10, false))
if err := srv.Run(":9999"); err != nil {
panic(err)
}
}

View File

@ -0,0 +1,15 @@
# Main
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
</details>
***

View File

@ -0,0 +1,19 @@
package main
import (
"fmt"
"github.com/kercylan98/minotaur/utils/timer"
"github.com/kercylan98/minotaur/utils/times"
"sync"
)
func main() {
var ticker = timer.GetTicker(10)
var wait sync.WaitGroup
wait.Add(3)
ticker.Loop("LOOP", timer.Instantly, times.Second, timer.Forever, func() {
fmt.Println("LOOP")
wait.Done()
})
wait.Wait()
}

13
game/README.md Normal file
View File

@ -0,0 +1,13 @@
# Game
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
game 目录下包含了各类通用的游戏玩法性内容,其中该目录主要为基础性内容,具体目录将对应不同的游戏功能性内容。
</details>
***

376
game/activity/README.md Normal file
View File

@ -0,0 +1,376 @@
# Activity
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
activity 活动状态管理
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 包级函数定义
|函数名称|描述
|:--|:--
|[SetTicker](#SetTicker)|设置自定义定时器,该方法必须在使用活动系统前调用,且只能调用一次
|[LoadGlobalData](#LoadGlobalData)|加载所有活动全局数据
|[LoadEntityData](#LoadEntityData)|加载所有活动实体数据
|[LoadOrRefreshActivity](#LoadOrRefreshActivity)|加载或刷新活动
|[DefineNoneDataActivity](#DefineNoneDataActivity)|声明无数据的活动类型
|[DefineGlobalDataActivity](#DefineGlobalDataActivity)|声明拥有全局数据的活动类型
|[DefineEntityDataActivity](#DefineEntityDataActivity)|声明拥有实体数据的活动类型
|[DefineGlobalAndEntityDataActivity](#DefineGlobalAndEntityDataActivity)|声明拥有全局数据和实体数据的活动类型
|[RegUpcomingEvent](#RegUpcomingEvent)|注册即将开始的活动事件处理器
|[OnUpcomingEvent](#OnUpcomingEvent)|即将开始的活动事件
|[RegStartedEvent](#RegStartedEvent)|注册活动开始事件处理器
|[OnStartedEvent](#OnStartedEvent)|活动开始事件
|[RegEndedEvent](#RegEndedEvent)|注册活动结束事件处理器
|[OnEndedEvent](#OnEndedEvent)|活动结束事件
|[RegExtendedShowStartedEvent](#RegExtendedShowStartedEvent)|注册活动结束后延长展示开始事件处理器
|[OnExtendedShowStartedEvent](#OnExtendedShowStartedEvent)|活动结束后延长展示开始事件
|[RegExtendedShowEndedEvent](#RegExtendedShowEndedEvent)|注册活动结束后延长展示结束事件处理器
|[OnExtendedShowEndedEvent](#OnExtendedShowEndedEvent)|活动结束后延长展示结束事件
|[RegNewDayEvent](#RegNewDayEvent)|注册新的一天事件处理器
|[OnNewDayEvent](#OnNewDayEvent)|新的一天事件
|[NewOptions](#NewOptions)|创建活动选项
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`STRUCT`|[Activity](#struct_Activity)|活动描述
|`STRUCT`|[Controller](#struct_Controller)|活动控制器
|`INTERFACE`|[BasicActivityController](#struct_BasicActivityController)|暂无描述...
|`INTERFACE`|[NoneDataActivityController](#struct_NoneDataActivityController)|无数据活动控制器
|`INTERFACE`|[GlobalDataActivityController](#struct_GlobalDataActivityController)|全局数据活动控制器
|`INTERFACE`|[EntityDataActivityController](#struct_EntityDataActivityController)|实体数据活动控制器
|`INTERFACE`|[GlobalAndEntityDataActivityController](#struct_GlobalAndEntityDataActivityController)|全局数据和实体数据活动控制器
|`STRUCT`|[DataMeta](#struct_DataMeta)|全局活动数据
|`STRUCT`|[EntityDataMeta](#struct_EntityDataMeta)|活动实体数据
|`STRUCT`|[UpcomingEventHandler](#struct_UpcomingEventHandler)|暂无描述...
|`STRUCT`|[Options](#struct_Options)|活动选项
</details>
***
## 详情信息
#### func SetTicker(size int, options ...timer.Option)
<span id="SetTicker"></span>
> 设置自定义定时器,该方法必须在使用活动系统前调用,且只能调用一次
***
#### func LoadGlobalData(handler func (activityType any))
<span id="LoadGlobalData"></span>
> 加载所有活动全局数据
***
#### func LoadEntityData(handler func (activityType any))
<span id="LoadEntityData"></span>
> 加载所有活动实体数据
***
#### func LoadOrRefreshActivity\[Type generic.Basic, ID generic.Basic\](activityType Type, activityId ID, options ...*Options) error
<span id="LoadOrRefreshActivity"></span>
> 加载或刷新活动
> - 通常在活动配置刷新时候将活动通过该方法注册或刷新
***
#### func DefineNoneDataActivity\[Type generic.Basic, ID generic.Basic\](activityType Type) NoneDataActivityController[Type, ID, *none, none, *none]
<span id="DefineNoneDataActivity"></span>
> 声明无数据的活动类型
***
#### func DefineGlobalDataActivity\[Type generic.Basic, ID generic.Basic, Data any\](activityType Type) GlobalDataActivityController[Type, ID, Data, none, *none]
<span id="DefineGlobalDataActivity"></span>
> 声明拥有全局数据的活动类型
***
#### func DefineEntityDataActivity\[Type generic.Basic, ID generic.Basic, EntityID generic.Basic, EntityData any\](activityType Type) EntityDataActivityController[Type, ID, *none, EntityID, EntityData]
<span id="DefineEntityDataActivity"></span>
> 声明拥有实体数据的活动类型
***
#### func DefineGlobalAndEntityDataActivity\[Type generic.Basic, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any\](activityType Type) GlobalAndEntityDataActivityController[Type, ID, Data, EntityID, EntityData]
<span id="DefineGlobalAndEntityDataActivity"></span>
> 声明拥有全局数据和实体数据的活动类型
***
#### func RegUpcomingEvent\[Type generic.Basic, ID generic.Basic\](activityType Type, handler UpcomingEventHandler[ID], priority ...int)
<span id="RegUpcomingEvent"></span>
> 注册即将开始的活动事件处理器
***
#### func OnUpcomingEvent\[Type generic.Basic, ID generic.Basic\](activity *Activity[Type, ID])
<span id="OnUpcomingEvent"></span>
> 即将开始的活动事件
***
#### func RegStartedEvent\[Type generic.Basic, ID generic.Basic\](activityType Type, handler StartedEventHandler[ID], priority ...int)
<span id="RegStartedEvent"></span>
> 注册活动开始事件处理器
***
#### func OnStartedEvent\[Type generic.Basic, ID generic.Basic\](activity *Activity[Type, ID])
<span id="OnStartedEvent"></span>
> 活动开始事件
***
#### func RegEndedEvent\[Type generic.Basic, ID generic.Basic\](activityType Type, handler EndedEventHandler[ID], priority ...int)
<span id="RegEndedEvent"></span>
> 注册活动结束事件处理器
***
#### func OnEndedEvent\[Type generic.Basic, ID generic.Basic\](activity *Activity[Type, ID])
<span id="OnEndedEvent"></span>
> 活动结束事件
***
#### func RegExtendedShowStartedEvent\[Type generic.Basic, ID generic.Basic\](activityType Type, handler ExtendedShowStartedEventHandler[ID], priority ...int)
<span id="RegExtendedShowStartedEvent"></span>
> 注册活动结束后延长展示开始事件处理器
***
#### func OnExtendedShowStartedEvent\[Type generic.Basic, ID generic.Basic\](activity *Activity[Type, ID])
<span id="OnExtendedShowStartedEvent"></span>
> 活动结束后延长展示开始事件
***
#### func RegExtendedShowEndedEvent\[Type generic.Basic, ID generic.Basic\](activityType Type, handler ExtendedShowEndedEventHandler[ID], priority ...int)
<span id="RegExtendedShowEndedEvent"></span>
> 注册活动结束后延长展示结束事件处理器
***
#### func OnExtendedShowEndedEvent\[Type generic.Basic, ID generic.Basic\](activity *Activity[Type, ID])
<span id="OnExtendedShowEndedEvent"></span>
> 活动结束后延长展示结束事件
***
#### func RegNewDayEvent\[Type generic.Basic, ID generic.Basic\](activityType Type, handler NewDayEventHandler[ID], priority ...int)
<span id="RegNewDayEvent"></span>
> 注册新的一天事件处理器
***
#### func OnNewDayEvent\[Type generic.Basic, ID generic.Basic\](activity *Activity[Type, ID])
<span id="OnNewDayEvent"></span>
> 新的一天事件
***
#### func NewOptions() *Options
<span id="NewOptions"></span>
> 创建活动选项
***
<span id="struct_Activity"></span>
### Activity `STRUCT`
活动描述
```go
type Activity[Type generic.Basic, ID generic.Basic] struct {
id ID
t Type
options *Options
state byte
lazy bool
tickerKey string
retention time.Duration
retentionKey string
mutex sync.RWMutex
getLastNewDayTime func() time.Time
setLastNewDayTime func(time.Time)
clearData func()
initializeData func()
}
```
<span id="struct_Controller"></span>
### Controller `STRUCT`
活动控制器
```go
type Controller[Type generic.Basic, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] struct {
t Type
activities map[ID]*Activity[Type, ID]
globalData map[ID]*DataMeta[Data]
entityData map[ID]map[EntityID]*EntityDataMeta[EntityData]
entityTof reflect.Type
globalInit func(activityId ID, data *DataMeta[Data])
entityInit func(activityId ID, entityId EntityID, data *EntityDataMeta[EntityData])
mutex sync.RWMutex
}
```
<span id="struct_Controller_GetGlobalData"></span>
#### func (*Controller) GetGlobalData(activityId ID) Data
> 获取特定活动全局数据
***
<span id="struct_Controller_GetEntityData"></span>
#### func (*Controller) GetEntityData(activityId ID, entityId EntityID) EntityData
> 获取特定活动实体数据
***
<span id="struct_Controller_IsOpen"></span>
#### func (*Controller) IsOpen(activityId ID) bool
> 活动是否开启
***
<span id="struct_Controller_IsShow"></span>
#### func (*Controller) IsShow(activityId ID) bool
> 活动是否展示
***
<span id="struct_Controller_IsOpenOrShow"></span>
#### func (*Controller) IsOpenOrShow(activityId ID) bool
> 活动是否开启或展示
***
<span id="struct_Controller_Refresh"></span>
#### func (*Controller) Refresh(activityId ID)
> 刷新活动
***
<span id="struct_Controller_InitializeNoneData"></span>
#### func (*Controller) InitializeNoneData(handler func (activityId ID, data *DataMeta[Data])) NoneDataActivityController[Type, ID, Data, EntityID, EntityData]
***
<span id="struct_Controller_InitializeGlobalData"></span>
#### func (*Controller) InitializeGlobalData(handler func (activityId ID, data *DataMeta[Data])) GlobalDataActivityController[Type, ID, Data, EntityID, EntityData]
***
<span id="struct_Controller_InitializeEntityData"></span>
#### func (*Controller) InitializeEntityData(handler func (activityId ID, entityId EntityID, data *EntityDataMeta[EntityData])) EntityDataActivityController[Type, ID, Data, EntityID, EntityData]
***
<span id="struct_Controller_InitializeGlobalAndEntityData"></span>
#### func (*Controller) InitializeGlobalAndEntityData(handler func (activityId ID, data *DataMeta[Data]), entityHandler func (activityId ID, entityId EntityID, data *EntityDataMeta[EntityData])) GlobalAndEntityDataActivityController[Type, ID, Data, EntityID, EntityData]
***
<span id="struct_BasicActivityController"></span>
### BasicActivityController `INTERFACE`
```go
type BasicActivityController[Type generic.Basic, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] interface {
IsOpen(activityId ID) bool
IsShow(activityId ID) bool
IsOpenOrShow(activityId ID) bool
Refresh(activityId ID)
}
```
<span id="struct_NoneDataActivityController"></span>
### NoneDataActivityController `INTERFACE`
无数据活动控制器
```go
type NoneDataActivityController[Type generic.Basic, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] interface {
BasicActivityController[Type, ID, Data, EntityID, EntityData]
InitializeNoneData(handler func(activityId ID, data *DataMeta[Data])) NoneDataActivityController[Type, ID, Data, EntityID, EntityData]
}
```
<span id="struct_GlobalDataActivityController"></span>
### GlobalDataActivityController `INTERFACE`
全局数据活动控制器
```go
type GlobalDataActivityController[Type generic.Basic, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] interface {
BasicActivityController[Type, ID, Data, EntityID, EntityData]
GetGlobalData(activityId ID) Data
InitializeGlobalData(handler func(activityId ID, data *DataMeta[Data])) GlobalDataActivityController[Type, ID, Data, EntityID, EntityData]
}
```
<span id="struct_EntityDataActivityController"></span>
### EntityDataActivityController `INTERFACE`
实体数据活动控制器
```go
type EntityDataActivityController[Type generic.Basic, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] interface {
BasicActivityController[Type, ID, Data, EntityID, EntityData]
GetEntityData(activityId ID, entityId EntityID) EntityData
InitializeEntityData(handler func(activityId ID, entityId EntityID, data *EntityDataMeta[EntityData])) EntityDataActivityController[Type, ID, Data, EntityID, EntityData]
}
```
<span id="struct_GlobalAndEntityDataActivityController"></span>
### GlobalAndEntityDataActivityController `INTERFACE`
全局数据和实体数据活动控制器
```go
type GlobalAndEntityDataActivityController[Type generic.Basic, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] interface {
BasicActivityController[Type, ID, Data, EntityID, EntityData]
GetGlobalData(activityId ID) Data
GetEntityData(activityId ID, entityId EntityID) EntityData
InitializeGlobalAndEntityData(handler func(activityId ID, data *DataMeta[Data]), entityHandler func(activityId ID, entityId EntityID, data *EntityDataMeta[EntityData])) GlobalAndEntityDataActivityController[Type, ID, Data, EntityID, EntityData]
}
```
<span id="struct_DataMeta"></span>
### DataMeta `STRUCT`
全局活动数据
```go
type DataMeta[Data any] struct {
once sync.Once
Data Data
LastNewDay time.Time
}
```
<span id="struct_EntityDataMeta"></span>
### EntityDataMeta `STRUCT`
活动实体数据
```go
type EntityDataMeta[Data any] struct {
once sync.Once
Data Data
LastNewDay time.Time
}
```
<span id="struct_UpcomingEventHandler"></span>
### UpcomingEventHandler `STRUCT`
```go
type UpcomingEventHandler[ID generic.Basic] func(activityId ID)
```
<span id="struct_Options"></span>
### Options `STRUCT`
活动选项
```go
type Options struct {
Tl *times.StateLine[byte]
Loop time.Duration
}
```
<span id="struct_Options_WithUpcomingTime"></span>
#### func (*Options) WithUpcomingTime(t time.Time) *Options
> 设置活动预告时间
***
<span id="struct_Options_WithStartTime"></span>
#### func (*Options) WithStartTime(t time.Time) *Options
> 设置活动开始时间
***
<span id="struct_Options_WithEndTime"></span>
#### func (*Options) WithEndTime(t time.Time) *Options
> 设置活动结束时间
***
<span id="struct_Options_WithExtendedShowTime"></span>
#### func (*Options) WithExtendedShowTime(t time.Time) *Options
> 设置延长展示时间
***
<span id="struct_Options_WithLoop"></span>
#### func (*Options) WithLoop(interval time.Duration) *Options
> 设置活动循环,时间间隔小于等于 0 表示不循环
> - 当活动状态展示结束后,会根据该选项设置的时间间隔重新开始
***

146
game/activity/activity.go Normal file
View File

@ -0,0 +1,146 @@
// Package activity 活动状态管理
package activity
import (
"fmt"
"github.com/kercylan98/minotaur/utils/generic"
"github.com/kercylan98/minotaur/utils/timer"
"reflect"
"sync"
"time"
)
const (
stateClosed = byte(iota) // 已关闭
stateUpcoming // 即将开始
stateStarted // 已开始
stateEnded // 已结束
stateExtendedShowEnded // 延长展示结束
)
var (
stateLine = []byte{stateClosed, stateUpcoming, stateStarted, stateEnded, stateExtendedShowEnded}
ticker *timer.Ticker
tickerOnce sync.Once
)
func init() {
ticker = timer.GetTicker(10)
}
// SetTicker 设置自定义定时器,该方法必须在使用活动系统前调用,且只能调用一次
func SetTicker(size int, options ...timer.Option) {
tickerOnce.Do(func() {
if ticker != nil {
ticker.Release()
}
ticker = timer.GetTicker(size, options...)
})
}
// LoadGlobalData 加载所有活动全局数据
func LoadGlobalData(handler func(activityType, activityId, data any)) {
for _, f := range activityGlobalDataLoader {
f(handler)
}
}
// LoadEntityData 加载所有活动实体数据
func LoadEntityData(handler func(activityType, activityId, entityId, data any)) {
for _, f := range activityEntityDataLoader {
f(handler)
}
}
// LoadOrRefreshActivity 加载或刷新活动
// - 通常在活动配置刷新时候将活动通过该方法注册或刷新
func LoadOrRefreshActivity[Type, ID generic.Basic](activityType Type, activityId ID, options ...*Options) error {
register, exist := activityRegister[activityType]
if !exist {
return fmt.Errorf("activity type %v not registered, activity %v registration failed", activityType, activityId)
}
activity := register(activityId, initOptions(options...)).(*Activity[Type, ID])
if !activity.options.Tl.Check(true, stateLine...) {
return fmt.Errorf("activity %v state timeline is invalid", activityId)
}
stateTrigger := map[byte]func(){
stateUpcoming: func() { OnUpcomingEvent(activity) },
stateStarted: func() { OnStartedEvent(activity) },
stateEnded: func() { OnEndedEvent(activity); OnExtendedShowStartedEvent(activity) },
stateExtendedShowEnded: func() { OnExtendedShowEndedEvent(activity) },
}
for _, state := range stateLine {
if activity.options.Tl.HasState(state) {
activity.options.Tl.AddTriggerToState(state, stateTrigger[state])
continue
}
for next := state; next <= stateLine[len(stateLine)-1]; next++ {
if activity.options.Tl.HasState(next) {
activity.options.Tl.AddTriggerToState(next, stateTrigger[state])
break
}
}
}
activity.refresh()
return nil
}
// Activity 活动描述
type Activity[Type, ID generic.Basic] struct {
id ID // 活动 ID
t Type // 活动类型
options *Options // 活动选项
state byte // 活动状态
lazy bool // 是否懒加载
tickerKey string // 定时器 key
retention time.Duration // 活动数据保留时间
retentionKey string // 保留定时器 key
mutex sync.RWMutex
getLastNewDayTime func() time.Time // 获取上次新的一天的时间
setLastNewDayTime func(time.Time) // 设置上次新的一天的时间
clearData func() // 清理活动数据
initializeData func() // 初始化活动数据
}
func (slf *Activity[Type, ID]) refresh() {
slf.mutex.Lock()
defer slf.mutex.Unlock()
curr := time.Now()
if slf.state = slf.options.Tl.GetStateByTime(curr); slf.state == stateUpcoming || (slf.state == stateStarted && !slf.options.Tl.HasState(stateUpcoming)) {
ticker.StopTimer(slf.retentionKey)
slf.initializeData()
}
for _, f := range slf.options.Tl.GetTriggerByState(slf.state) {
if f != nil {
f()
}
}
next := slf.options.Tl.GetNextTimeByState(slf.state)
if !next.IsZero() && next.After(curr) {
ticker.After(slf.tickerKey, next.Sub(curr)+time.Millisecond*100, slf.refresh)
} else {
ticker.StopTimer(slf.tickerKey)
ticker.StopTimer(fmt.Sprintf("activity:new_day:%d:%v", reflect.ValueOf(slf.t).Kind(), slf.id))
if slf.options.Loop > 0 {
slf.options.Tl.Move(slf.options.Loop * 2)
ticker.After(slf.tickerKey, slf.options.Loop+time.Millisecond*100, slf.refresh)
return
}
if slf.retention > 0 {
ticker.After(slf.tickerKey, slf.retention, func() {
ticker.StopTimer(slf.retentionKey)
slf.clearData()
})
}
}
}

View File

@ -0,0 +1,63 @@
package activity_test
import (
"github.com/kercylan98/minotaur/game/activity"
"github.com/kercylan98/minotaur/utils/times"
"testing"
"time"
)
type ActivityData struct {
players []string
}
type PlayerData struct {
info string
}
func TestRegTypeByGlobalData(t *testing.T) {
controller := activity.DefineGlobalDataActivity[int, int, *ActivityData](1).InitializeGlobalData(func(activityId int, data *activity.DataMeta[*ActivityData]) {
data.Data.players = []string{"1", "2", "3"}
})
activity.RegUpcomingEvent(1, func(activityId int) {
t.Log(controller.GetGlobalData(activityId).players)
t.Log("即将开始")
})
activity.RegStartedEvent(1, func(activityId int) {
t.Log("开始")
})
activity.RegEndedEvent(1, func(activityId int) {
t.Log(controller.GetGlobalData(activityId).players)
t.Log("结束")
})
activity.RegExtendedShowStartedEvent(1, func(activityId int) {
t.Log("延长展示开始")
})
activity.RegExtendedShowEndedEvent(1, func(activityId int) {
t.Log("延长展示结束")
})
activity.RegNewDayEvent(1, func(activityId int) {
t.Log("新的一天")
})
now := time.Now()
if err := activity.LoadOrRefreshActivity(1, 1, activity.NewOptions().
WithUpcomingTime(now.Add(1*time.Second)).
WithStartTime(now.Add(2*times.Second)).
WithEndTime(now.Add(3*times.Second)).
WithExtendedShowTime(now.Add(4*times.Second)).
WithLoop(3*times.Second),
); err != nil {
t.Fatal(err)
}
time.Sleep(times.Week)
}

158
game/activity/controller.go Normal file
View File

@ -0,0 +1,158 @@
package activity
import (
"github.com/kercylan98/minotaur/utils/generic"
"reflect"
"sync"
)
type none byte
// DefineNoneDataActivity 声明无数据的活动类型
func DefineNoneDataActivity[Type, ID generic.Basic](activityType Type) NoneDataActivityController[Type, ID, *none, none, *none] {
return regController(&Controller[Type, ID, *none, none, *none]{
t: activityType,
})
}
// DefineGlobalDataActivity 声明拥有全局数据的活动类型
func DefineGlobalDataActivity[Type, ID generic.Basic, Data any](activityType Type) GlobalDataActivityController[Type, ID, Data, none, *none] {
return regController(&Controller[Type, ID, Data, none, *none]{
t: activityType,
})
}
// DefineEntityDataActivity 声明拥有实体数据的活动类型
func DefineEntityDataActivity[Type, ID, EntityID generic.Basic, EntityData any](activityType Type) EntityDataActivityController[Type, ID, *none, EntityID, EntityData] {
return regController(&Controller[Type, ID, *none, EntityID, EntityData]{
t: activityType,
})
}
// DefineGlobalAndEntityDataActivity 声明拥有全局数据和实体数据的活动类型
func DefineGlobalAndEntityDataActivity[Type, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any](activityType Type) GlobalAndEntityDataActivityController[Type, ID, Data, EntityID, EntityData] {
return regController(&Controller[Type, ID, Data, EntityID, EntityData]{
t: activityType,
})
}
// Controller 活动控制器
type Controller[Type, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] struct {
t Type // 活动类型
activities map[ID]*Activity[Type, ID] // 活动列表
globalData map[ID]*DataMeta[Data] // 全局数据
entityData map[ID]map[EntityID]*EntityDataMeta[EntityData] // 实体数据
entityTof reflect.Type // 实体数据类型
globalInit func(activityId ID, data *DataMeta[Data]) // 全局数据初始化函数
entityInit func(activityId ID, entityId EntityID, data *EntityDataMeta[EntityData]) // 实体数据初始化函数
mutex sync.RWMutex
}
// GetGlobalData 获取特定活动全局数据
func (slf *Controller[Type, ID, Data, EntityID, EntityData]) GetGlobalData(activityId ID) Data {
slf.mutex.RLock()
defer slf.mutex.RUnlock()
global := slf.globalData[activityId]
if slf.globalInit != nil {
global.once.Do(func() {
slf.globalInit(activityId, global)
})
}
return global.Data
}
// GetEntityData 获取特定活动实体数据
func (slf *Controller[Type, ID, Data, EntityID, EntityData]) GetEntityData(activityId ID, entityId EntityID) EntityData {
slf.mutex.RLock()
defer slf.mutex.RUnlock()
entities, exist := slf.entityData[activityId]
if !exist {
entities = make(map[EntityID]*EntityDataMeta[EntityData])
slf.entityData[activityId] = entities
}
entity, exist := entities[entityId]
if !exist {
entity = &EntityDataMeta[EntityData]{
Data: reflect.New(slf.entityTof).Interface().(EntityData),
}
entities[entityId] = entity
}
if slf.entityInit != nil {
entity.once.Do(func() {
slf.entityInit(activityId, entityId, entity)
})
}
return entity.Data
}
// IsOpen 活动是否开启
func (slf *Controller[Type, ID, Data, EntityID, EntityData]) IsOpen(activityId ID) bool {
slf.mutex.RLock()
activity, exist := slf.activities[activityId]
slf.mutex.RUnlock()
if !exist {
return false
}
activity.mutex.RLock()
defer activity.mutex.RUnlock()
return activity.state == stateStarted
}
// IsShow 活动是否展示
func (slf *Controller[Type, ID, Data, EntityID, EntityData]) IsShow(activityId ID) bool {
slf.mutex.RLock()
activity, exist := slf.activities[activityId]
slf.mutex.RUnlock()
if !exist {
return false
}
activity.mutex.RLock()
defer activity.mutex.RUnlock()
return activity.state == stateUpcoming || (activity.state == stateEnded && activity.options.Tl.HasState(stateExtendedShowEnded))
}
// IsOpenOrShow 活动是否开启或展示
func (slf *Controller[Type, ID, Data, EntityID, EntityData]) IsOpenOrShow(activityId ID) bool {
slf.mutex.RLock()
activity, exist := slf.activities[activityId]
slf.mutex.RUnlock()
if !exist {
return false
}
activity.mutex.RLock()
defer activity.mutex.RUnlock()
return activity.state == stateStarted || activity.state == stateUpcoming || (activity.state == stateEnded && activity.options.Tl.HasState(stateExtendedShowEnded))
}
// Refresh 刷新活动
func (slf *Controller[Type, ID, Data, EntityID, EntityData]) Refresh(activityId ID) {
slf.mutex.RLock()
activity, exist := slf.activities[activityId]
slf.mutex.RUnlock()
if !exist {
return
}
activity.refresh()
}
func (slf *Controller[Type, ID, Data, EntityID, EntityData]) InitializeNoneData(handler func(activityId ID, data *DataMeta[Data])) NoneDataActivityController[Type, ID, Data, EntityID, EntityData] {
slf.globalInit = handler
return slf
}
func (slf *Controller[Type, ID, Data, EntityID, EntityData]) InitializeGlobalData(handler func(activityId ID, data *DataMeta[Data])) GlobalDataActivityController[Type, ID, Data, EntityID, EntityData] {
slf.globalInit = handler
return slf
}
func (slf *Controller[Type, ID, Data, EntityID, EntityData]) InitializeEntityData(handler func(activityId ID, entityId EntityID, data *EntityDataMeta[EntityData])) EntityDataActivityController[Type, ID, Data, EntityID, EntityData] {
slf.entityInit = handler
return slf
}
func (slf *Controller[Type, ID, Data, EntityID, EntityData]) InitializeGlobalAndEntityData(handler func(activityId ID, data *DataMeta[Data]), entityHandler func(activityId ID, entityId EntityID, data *EntityDataMeta[EntityData])) GlobalAndEntityDataActivityController[Type, ID, Data, EntityID, EntityData] {
slf.globalInit = handler
slf.entityInit = entityHandler
return slf
}

View File

@ -0,0 +1,56 @@
package activity
import "github.com/kercylan98/minotaur/utils/generic"
type BasicActivityController[Type, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] interface {
// IsOpen 活动是否开启
IsOpen(activityId ID) bool
// IsShow 活动是否展示
IsShow(activityId ID) bool
// IsOpenOrShow 活动是否开启或展示
IsOpenOrShow(activityId ID) bool
// Refresh 刷新活动
Refresh(activityId ID)
}
// NoneDataActivityController 无数据活动控制器
type NoneDataActivityController[Type, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] interface {
BasicActivityController[Type, ID, Data, EntityID, EntityData]
// InitializeNoneData 初始化活动
// - 该函数提供了一个操作活动数据的入口,可以在该函数中对传入的活动数据进行初始化
//
// 对于无数据活动,该函数的意义在于,可以在该函数中对活动进行初始化,比如设置活动的状态等,虽然为无数据活动,但是例如活动本身携带的状态数据也是需要加载的
InitializeNoneData(handler func(activityId ID, data *DataMeta[Data])) NoneDataActivityController[Type, ID, Data, EntityID, EntityData]
}
// GlobalDataActivityController 全局数据活动控制器
type GlobalDataActivityController[Type, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] interface {
BasicActivityController[Type, ID, Data, EntityID, EntityData]
// GetGlobalData 获取全局数据
GetGlobalData(activityId ID) Data
// InitializeGlobalData 初始化活动
// - 该函数提供了一个操作活动数据的入口,可以在该函数中对传入的活动数据进行初始化
InitializeGlobalData(handler func(activityId ID, data *DataMeta[Data])) GlobalDataActivityController[Type, ID, Data, EntityID, EntityData]
}
// EntityDataActivityController 实体数据活动控制器
type EntityDataActivityController[Type, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] interface {
BasicActivityController[Type, ID, Data, EntityID, EntityData]
// GetEntityData 获取实体数据
GetEntityData(activityId ID, entityId EntityID) EntityData
// InitializeEntityData 初始化活动
// - 该函数提供了一个操作活动数据的入口,可以在该函数中对传入的活动数据进行初始化
InitializeEntityData(handler func(activityId ID, entityId EntityID, data *EntityDataMeta[EntityData])) EntityDataActivityController[Type, ID, Data, EntityID, EntityData]
}
// GlobalAndEntityDataActivityController 全局数据和实体数据活动控制器
type GlobalAndEntityDataActivityController[Type, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any] interface {
BasicActivityController[Type, ID, Data, EntityID, EntityData]
// GetGlobalData 获取全局数据
GetGlobalData(activityId ID) Data
// GetEntityData 获取实体数据
GetEntityData(activityId ID, entityId EntityID) EntityData
// InitializeGlobalAndEntityData 初始化活动
// - 该函数提供了一个操作活动数据的入口,可以在该函数中对传入的活动数据进行初始化
InitializeGlobalAndEntityData(handler func(activityId ID, data *DataMeta[Data]), entityHandler func(activityId ID, entityId EntityID, data *EntityDataMeta[EntityData])) GlobalAndEntityDataActivityController[Type, ID, Data, EntityID, EntityData]
}

View File

@ -0,0 +1,107 @@
package activity
import (
"fmt"
"github.com/kercylan98/minotaur/utils/collection"
"github.com/kercylan98/minotaur/utils/generic"
"github.com/kercylan98/minotaur/utils/times"
"reflect"
"time"
)
var (
activityRegister map[any]func(activityId any, options *Options) (activity any) // type -> register (控制活动注册到特定类型控制器的注册机)
activityGlobalDataLoader []func(handler func(activityType, activityId, data any)) // 全局数据加载器
activityEntityDataLoader []func(handler func(activityType, activityId, entityId, data any)) // 实体数据加载器
)
func init() {
activityRegister = make(map[any]func(activityId any, options *Options) (activity any))
}
// regController 注册活动类型控制器
func regController[Type, ID generic.Basic, Data any, EntityID generic.Basic, EntityData any](controller *Controller[Type, ID, Data, EntityID, EntityData]) *Controller[Type, ID, Data, EntityID, EntityData] {
var entityZero EntityData
controller.activities = make(map[ID]*Activity[Type, ID])
controller.globalData = make(map[ID]*DataMeta[Data])
controller.entityTof = reflect.TypeOf(entityZero)
if controller.entityTof.Kind() == reflect.Pointer {
controller.entityTof = controller.entityTof.Elem()
}
// 反射类型
var (
zero Data
tof = reflect.TypeOf(zero)
)
if tof.Kind() == reflect.Pointer {
tof = tof.Elem()
}
// 活动注册机(注册机内不加载活动数据,仅定义基本活动信息)
activityRegister[controller.t] = func(aid any, options *Options) any {
activityId := aid.(ID)
controller.mutex.Lock()
activity, exist := controller.activities[activityId]
if !exist {
activity = &Activity[Type, ID]{
t: controller.t,
id: activityId,
options: options,
tickerKey: fmt.Sprintf("activity:%d:%v", reflect.ValueOf(controller.t).Kind(), activityId),
getLastNewDayTime: func() time.Time {
return controller.globalData[activityId].LastNewDay
},
setLastNewDayTime: func(t time.Time) {
controller.globalData[activityId].LastNewDay = t
},
clearData: func() {
controller.mutex.Lock()
defer controller.mutex.Unlock()
delete(controller.globalData, activityId)
delete(controller.entityData, activityId)
},
initializeData: func() {
controller.mutex.Lock()
defer controller.mutex.Unlock()
controller.globalData[activityId] = &DataMeta[Data]{
Data: reflect.New(tof).Interface().(Data),
}
if controller.entityData == nil {
controller.entityData = make(map[ID]map[EntityID]*EntityDataMeta[EntityData])
}
controller.entityData[activityId] = make(map[EntityID]*EntityDataMeta[EntityData])
},
}
if activity.options == nil {
activity.options = NewOptions()
}
if activity.options.Tl == nil || activity.options.Tl.GetStateCount() == 0 {
activity.options.Tl = times.NewStateLine[byte](stateClosed)
}
controller.activities[activityId] = activity
}
controller.mutex.Unlock()
// 全局数据加载器
activityGlobalDataLoader = append(activityGlobalDataLoader, func(handler func(activityType any, activityId any, data any)) {
controller.mutex.RLock()
data := controller.globalData[activityId]
controller.mutex.RUnlock()
handler(controller.t, activityId, data)
})
// 实体数据加载器
activityEntityDataLoader = append(activityEntityDataLoader, func(handler func(activityType any, activityId any, entityId any, data any)) {
controller.mutex.RLock()
entities := collection.CloneMap(controller.entityData[activityId])
controller.mutex.RUnlock()
for entityId, data := range entities {
handler(controller.t, activityId, entityId, data)
}
})
return activity
}
return controller
}

20
game/activity/data.go Normal file
View File

@ -0,0 +1,20 @@
package activity
import (
"sync"
"time"
)
// DataMeta 全局活动数据
type DataMeta[Data any] struct {
once sync.Once
Data Data `json:"data,omitempty"` // 活动数据
LastNewDay time.Time `json:"last_new_day,omitempty"` // 上次跨天时间
}
// EntityDataMeta 活动实体数据
type EntityDataMeta[Data any] struct {
once sync.Once
Data Data `json:"data,omitempty"` // 活动数据
LastNewDay time.Time `json:"last_new_day,omitempty"` // 上次跨天时间
}

246
game/activity/events.go Normal file
View File

@ -0,0 +1,246 @@
package activity
import (
"fmt"
"github.com/kercylan98/minotaur/utils/collection"
"github.com/kercylan98/minotaur/utils/collection/listings"
"github.com/kercylan98/minotaur/utils/generic"
"github.com/kercylan98/minotaur/utils/log"
"github.com/kercylan98/minotaur/utils/timer"
"github.com/kercylan98/minotaur/utils/times"
"reflect"
"time"
)
type (
UpcomingEventHandler[ID generic.Basic] func(activityId ID) // 即将开始的活动事件处理器
StartedEventHandler[ID generic.Basic] func(activityId ID) // 活动开始事件处理器
EndedEventHandler[ID generic.Basic] func(activityId ID) // 活动结束事件处理器
ExtendedShowStartedEventHandler[ID generic.Basic] func(activityId ID) // 活动结束后延长展示开始事件处理器
ExtendedShowEndedEventHandler[ID generic.Basic] func(activityId ID) // 活动结束后延长展示结束事件处理器
NewDayEventHandler[ID generic.Basic] func(activityId ID) // 新的一天事件处理器
)
var (
upcomingEventHandlers map[any]*listings.PrioritySlice[func(activityId any)] // 即将开始的活动事件处理器
startedEventHandlers map[any]*listings.PrioritySlice[func(activityId any)] // 活动开始事件处理器
endedEventHandlers map[any]*listings.PrioritySlice[func(activityId any)] // 活动结束事件处理器
extShowStartedEventHandlers map[any]*listings.PrioritySlice[func(activityId any)] // 活动结束后延长展示开始事件处理器
extShowEndedEventHandlers map[any]*listings.PrioritySlice[func(activityId any)] // 活动结束后延长展示结束事件处理器
newDayEventHandlers map[any]*listings.PrioritySlice[func(activityId any)] // 新的一天事件处理器
)
func init() {
upcomingEventHandlers = make(map[any]*listings.PrioritySlice[func(activityId any)])
startedEventHandlers = make(map[any]*listings.PrioritySlice[func(activityId any)])
endedEventHandlers = make(map[any]*listings.PrioritySlice[func(activityId any)])
extShowStartedEventHandlers = make(map[any]*listings.PrioritySlice[func(activityId any)])
extShowEndedEventHandlers = make(map[any]*listings.PrioritySlice[func(activityId any)])
newDayEventHandlers = make(map[any]*listings.PrioritySlice[func(activityId any)])
}
// RegUpcomingEvent 注册即将开始的活动事件处理器
func RegUpcomingEvent[Type, ID generic.Basic](activityType Type, handler UpcomingEventHandler[ID], priority ...int) {
handlers, exist := upcomingEventHandlers[activityType]
if !exist {
handlers = listings.NewPrioritySlice[func(activityId any)]()
upcomingEventHandlers[activityType] = handlers
}
handlers.Append(func(activityId any) {
if !reflect.TypeOf(activityId).AssignableTo(reflect.TypeOf(handler).In(0)) {
return
}
handler(activityId.(ID))
}, collection.FindFirstOrDefaultInSlice(priority, 0))
}
// OnUpcomingEvent 即将开始的活动事件
func OnUpcomingEvent[Type, ID generic.Basic](activity *Activity[Type, ID]) {
handlers, exist := upcomingEventHandlers[activity.t]
if !exist {
return
}
handlers.RangeValue(func(index int, value func(activityId any)) bool {
defer func() {
if err := recover(); err != nil {
log.Error("OnUpcomingEvent", log.Any("type", activity.t), log.Any("id", activity.id), log.Any("err", err))
return
}
}()
value(activity.id)
return true
})
}
// RegStartedEvent 注册活动开始事件处理器
func RegStartedEvent[Type, ID generic.Basic](activityType Type, handler StartedEventHandler[ID], priority ...int) {
handlers, exist := startedEventHandlers[activityType]
if !exist {
handlers = listings.NewPrioritySlice[func(activityId any)]()
startedEventHandlers[activityType] = handlers
}
handlers.Append(func(activityId any) {
if !reflect.TypeOf(activityId).AssignableTo(reflect.TypeOf(handler).In(0)) {
return
}
handler(activityId.(ID))
}, collection.FindFirstOrDefaultInSlice(priority, 0))
}
// OnStartedEvent 活动开始事件
func OnStartedEvent[Type, ID generic.Basic](activity *Activity[Type, ID]) {
handlers, exist := startedEventHandlers[activity.t]
if !exist {
return
}
handlers.RangeValue(func(index int, value func(activityId any)) bool {
defer func() {
if err := recover(); err != nil {
log.Error("OnStartedEvent", log.Any("type", activity.t), log.Any("id", activity.id), log.Any("err", err))
return
}
}()
value(activity.id)
return true
})
now := time.Now()
if !times.IsSameDay(now, activity.getLastNewDayTime()) {
OnNewDayEvent(activity)
}
ticker.Loop(fmt.Sprintf("activity:new_day:%d:%v", reflect.ValueOf(activity.t).Kind(), activity.id), times.GetNextDayInterval(now), times.Day, timer.Forever, func() {
OnNewDayEvent(activity)
})
}
// RegEndedEvent 注册活动结束事件处理器
func RegEndedEvent[Type, ID generic.Basic](activityType Type, handler EndedEventHandler[ID], priority ...int) {
handlers, exist := endedEventHandlers[activityType]
if !exist {
handlers = listings.NewPrioritySlice[func(activityId any)]()
endedEventHandlers[activityType] = handlers
}
handlers.Append(func(activityId any) {
if !reflect.TypeOf(activityId).AssignableTo(reflect.TypeOf(handler).In(0)) {
return
}
handler(activityId.(ID))
}, collection.FindFirstOrDefaultInSlice(priority, 0))
}
// OnEndedEvent 活动结束事件
func OnEndedEvent[Type, ID generic.Basic](activity *Activity[Type, ID]) {
handlers, exist := endedEventHandlers[activity.t]
if !exist {
return
}
handlers.RangeValue(func(index int, value func(activityId any)) bool {
defer func() {
if err := recover(); err != nil {
log.Error("OnEndedEvent", log.Any("type", activity.t), log.Any("id", activity.id), log.Any("err", err))
return
}
}()
value(activity.id)
return true
})
}
// RegExtendedShowStartedEvent 注册活动结束后延长展示开始事件处理器
func RegExtendedShowStartedEvent[Type, ID generic.Basic](activityType Type, handler ExtendedShowStartedEventHandler[ID], priority ...int) {
handlers, exist := extShowStartedEventHandlers[activityType]
if !exist {
handlers = listings.NewPrioritySlice[func(activityId any)]()
extShowStartedEventHandlers[activityType] = handlers
}
handlers.Append(func(activityId any) {
if !reflect.TypeOf(activityId).AssignableTo(reflect.TypeOf(handler).In(0)) {
return
}
handler(activityId.(ID))
}, collection.FindFirstOrDefaultInSlice(priority, 0))
}
// OnExtendedShowStartedEvent 活动结束后延长展示开始事件
func OnExtendedShowStartedEvent[Type, ID generic.Basic](activity *Activity[Type, ID]) {
handlers, exist := extShowStartedEventHandlers[activity.t]
if !exist {
return
}
handlers.RangeValue(func(index int, value func(activityId any)) bool {
defer func() {
if err := recover(); err != nil {
log.Error("OnExtendedShowStartedEvent", log.Any("type", activity.t), log.Any("id", activity.id), log.Any("err", err))
return
}
}()
value(activity.id)
return true
})
}
// RegExtendedShowEndedEvent 注册活动结束后延长展示结束事件处理器
func RegExtendedShowEndedEvent[Type, ID generic.Basic](activityType Type, handler ExtendedShowEndedEventHandler[ID], priority ...int) {
handlers, exist := extShowEndedEventHandlers[activityType]
if !exist {
handlers = listings.NewPrioritySlice[func(activityId any)]()
extShowEndedEventHandlers[activityType] = handlers
}
handlers.Append(func(activityId any) {
if !reflect.TypeOf(activityId).AssignableTo(reflect.TypeOf(handler).In(0)) {
return
}
handler(activityId.(ID))
}, collection.FindFirstOrDefaultInSlice(priority, 0))
}
// OnExtendedShowEndedEvent 活动结束后延长展示结束事件
func OnExtendedShowEndedEvent[Type, ID generic.Basic](activity *Activity[Type, ID]) {
handlers, exist := extShowEndedEventHandlers[activity.t]
if !exist {
return
}
handlers.RangeValue(func(index int, value func(activityId any)) bool {
defer func() {
if err := recover(); err != nil {
log.Error("OnExtendedShowEndedEvent", log.Any("type", activity.t), log.Any("id", activity.id), log.Any("err", err))
return
}
}()
value(activity.id)
return true
})
}
// RegNewDayEvent 注册新的一天事件处理器
func RegNewDayEvent[Type, ID generic.Basic](activityType Type, handler NewDayEventHandler[ID], priority ...int) {
handlers, exist := newDayEventHandlers[activityType]
if !exist {
handlers = listings.NewPrioritySlice[func(activityId any)]()
newDayEventHandlers[activityType] = handlers
}
handlers.Append(func(activityId any) {
if !reflect.TypeOf(activityId).AssignableTo(reflect.TypeOf(handler).In(0)) {
return
}
handler(activityId.(ID))
}, collection.FindFirstOrDefaultInSlice(priority, 0))
}
// OnNewDayEvent 新的一天事件
func OnNewDayEvent[Type, ID generic.Basic](activity *Activity[Type, ID]) {
handlers, exist := newDayEventHandlers[activity.t]
if !exist {
return
}
handlers.RangeValue(func(index int, value func(activityId any)) bool {
defer func() {
if err := recover(); err != nil {
log.Error("OnNewDayEvent", log.Any("type", activity.t), log.Any("id", activity.id), log.Any("err", err))
return
}
}()
value(activity.id)
return true
})
}

View File

@ -0,0 +1,15 @@
# Main
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
</details>
***

View File

@ -0,0 +1,15 @@
# Activities
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
</details>
***

View File

@ -0,0 +1,24 @@
package activities
import (
"github.com/kercylan98/minotaur/game/activity"
"github.com/kercylan98/minotaur/game/activity/internal/example/types"
"github.com/kercylan98/minotaur/utils/super"
"time"
)
var (
DemoActivity = activity.DefineEntityDataActivity[int, int, string, *types.DemoActivityData](1).InitializeEntityData(func(activityId int, entityId string, data *activity.EntityDataMeta[*types.DemoActivityData]) {
// 模拟数据库加载
_ = super.UnmarshalJSON([]byte(`{"last_new_day": "2021-01-01 00:00:00", "data": {"login_num": 3}}`), data)
})
)
func init() {
// 模拟配置加载活动
if err := activity.LoadOrRefreshActivity(1, 1, activity.NewOptions().
WithStartTime(time.Now().Add(time.Second*3)),
); err != nil {
panic(err)
}
}

View File

@ -0,0 +1,15 @@
# Demoactivity
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
</details>
***

View File

@ -0,0 +1,15 @@
package demoactivity
import (
"github.com/kercylan98/minotaur/game/activity"
"github.com/kercylan98/minotaur/game/activity/internal/example/activities"
"github.com/kercylan98/minotaur/utils/log"
)
func init() {
activity.RegStartedEvent(1, onActivityStart)
}
func onActivityStart(id int) {
log.Info("activity start", log.Int("id", id), log.Any("entity", activities.DemoActivity.GetEntityData(id, "demo_entity")))
}

View File

@ -0,0 +1,10 @@
package main
import (
_ "github.com/kercylan98/minotaur/game/activity/internal/example/activities/demoactivity"
"time"
)
func main() {
time.Sleep(time.Second * 5)
}

View File

@ -0,0 +1,33 @@
# Types
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`STRUCT`|[DemoActivityData](#struct_DemoActivityData)|暂无描述...
</details>
***
## 详情信息
<span id="struct_DemoActivityData"></span>
### DemoActivityData `STRUCT`
```go
type DemoActivityData struct {
LoginNum int
}
```

View File

@ -0,0 +1,5 @@
package types
type DemoActivityData struct {
LoginNum int `json:"login_num"` // 登录次数
}

72
game/activity/options.go Normal file
View File

@ -0,0 +1,72 @@
package activity
import (
"github.com/kercylan98/minotaur/utils/times"
"time"
)
// NewOptions 创建活动选项
func NewOptions() *Options {
return new(Options)
}
// initOptions 初始化活动选项
func initOptions(opts ...*Options) *Options {
var opt *Options
if len(opts) > 0 {
opt = opts[0]
}
if opt == nil {
opt = NewOptions()
}
return opt
}
// Options 活动选项
type Options struct {
Tl *times.StateLine[byte] // 活动时间线
Loop time.Duration // 活动循环,时间间隔小于等于 0 表示不循环
}
// WithUpcomingTime 设置活动预告时间
func (slf *Options) WithUpcomingTime(t time.Time) *Options {
if slf.Tl == nil {
slf.Tl = times.NewStateLine[byte](stateClosed)
}
slf.Tl.AddState(stateUpcoming, t)
return slf
}
// WithStartTime 设置活动开始时间
func (slf *Options) WithStartTime(t time.Time) *Options {
if slf.Tl == nil {
slf.Tl = times.NewStateLine[byte](stateClosed)
}
slf.Tl.AddState(stateStarted, t)
return slf
}
// WithEndTime 设置活动结束时间
func (slf *Options) WithEndTime(t time.Time) *Options {
if slf.Tl == nil {
slf.Tl = times.NewStateLine[byte](stateClosed)
}
slf.Tl.AddState(stateEnded, t)
return slf
}
// WithExtendedShowTime 设置延长展示时间
func (slf *Options) WithExtendedShowTime(t time.Time) *Options {
if slf.Tl == nil {
slf.Tl = times.NewStateLine[byte](stateClosed)
}
slf.Tl.AddState(stateExtendedShowEnded, t)
return slf
}
// WithLoop 设置活动循环,时间间隔小于等于 0 表示不循环
// - 当活动状态展示结束后,会根据该选项设置的时间间隔重新开始
func (slf *Options) WithLoop(interval time.Duration) *Options {
slf.Loop = interval
return slf
}

2
game/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package game 目录下包含了各类通用的游戏玩法性内容,其中该目录主要为基础性内容,具体目录将对应不同的游戏功能性内容。
package game

229
game/fight/README.md Normal file
View File

@ -0,0 +1,229 @@
# Fight
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 包级函数定义
|函数名称|描述
|:--|:--
|[NewTurnBased](#NewTurnBased)|创建一个新的回合制
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`STRUCT`|[TurnBased](#struct_TurnBased)|回合制
|`INTERFACE`|[TurnBasedControllerInfo](#struct_TurnBasedControllerInfo)|暂无描述...
|`INTERFACE`|[TurnBasedControllerAction](#struct_TurnBasedControllerAction)|暂无描述...
|`STRUCT`|[TurnBasedController](#struct_TurnBasedController)|回合制控制器
|`STRUCT`|[TurnBasedEntitySwitchEventHandler](#struct_TurnBasedEntitySwitchEventHandler)|暂无描述...
</details>
***
## 详情信息
#### func NewTurnBased\[CampID comparable, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]\](calcNextTurnDuration func ( Camp, Entity) time.Duration) *TurnBased[CampID, EntityID, Camp, Entity]
<span id="NewTurnBased"></span>
> 创建一个新的回合制
> - calcNextTurnDuration 将返回下一次行动时间间隔,适用于按照速度计算下一次行动时间间隔的情况。当返回 0 时,将使用默认的行动超时时间
***
<span id="struct_TurnBased"></span>
### TurnBased `STRUCT`
回合制
```go
type TurnBased[CampID comparable, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] struct {
*turnBasedEvents[CampID, EntityID, Camp, Entity]
controller *TurnBasedController[CampID, EntityID, Camp, Entity]
ticker *time.Ticker
actionWaitTicker *time.Ticker
actioning bool
actionMutex sync.RWMutex
entities []Entity
campRel map[EntityID]Camp
calcNextTurnDuration func(Camp, Entity) time.Duration
actionTimeoutHandler func(Camp, Entity) time.Duration
signal chan signal
round int
currCamp Camp
currEntity Entity
currActionTimeout time.Duration
currStart time.Time
closeMutex sync.RWMutex
closed bool
}
```
<span id="struct_TurnBased_Close"></span>
#### func (*TurnBased) Close()
> 关闭回合制
***
<span id="struct_TurnBased_AddCamp"></span>
#### func (*TurnBased) AddCamp(camp Camp, entity Entity, entities ...Entity)
> 添加阵营
***
<span id="struct_TurnBased_SetActionTimeout"></span>
#### func (*TurnBased) SetActionTimeout(actionTimeoutHandler func ( Camp, Entity) time.Duration)
> 设置行动超时时间处理函数
> - 默认情况下行动超时时间函数将始终返回 0
***
<span id="struct_TurnBased_Run"></span>
#### func (*TurnBased) Run()
> 运行
<details>
<summary>查看 / 收起单元测试</summary>
```go
func TestTurnBased_Run(t *testing.T) {
tbi := fight.NewTurnBased[string, string, *Camp, *Entity](func(camp *Camp, entity *Entity) time.Duration {
return time.Duration(float64(time.Second) / entity.speed)
})
tbi.SetActionTimeout(func(camp *Camp, entity *Entity) time.Duration {
return time.Second * 5
})
tbi.RegTurnBasedEntityActionTimeoutEvent(func(controller fight.TurnBasedControllerInfo[string, string, *Camp, *Entity]) {
t.Log("时间", time.Now().Unix(), "回合", controller.GetRound(), "阵营", controller.GetCamp().GetId(), "实体", controller.GetEntity().GetId(), "超时")
})
tbi.RegTurnBasedRoundChangeEvent(func(controller fight.TurnBasedControllerInfo[string, string, *Camp, *Entity]) {
t.Log("时间", time.Now().Unix(), "回合", controller.GetRound(), "回合切换")
})
tbi.RegTurnBasedEntitySwitchEvent(func(controller fight.TurnBasedControllerAction[string, string, *Camp, *Entity]) {
switch controller.GetEntity().GetId() {
case "1":
go func() {
time.Sleep(time.Second * 2)
controller.Finish()
}()
case "2":
controller.Refresh(time.Second)
case "4":
controller.Stop()
}
t.Log("时间", time.Now().Unix(), "回合", controller.GetRound(), "阵营", controller.GetCamp().GetId(), "实体", controller.GetEntity().GetId(), "开始行动")
})
tbi.AddCamp(&Camp{id: "1"}, &Entity{id: "1", speed: 1}, &Entity{id: "2", speed: 1})
tbi.AddCamp(&Camp{id: "2"}, &Entity{id: "3", speed: 1}, &Entity{id: "4", speed: 1})
tbi.Run()
}
```
</details>
***
<span id="struct_TurnBasedControllerInfo"></span>
### TurnBasedControllerInfo `INTERFACE`
```go
type TurnBasedControllerInfo[CampID comparable, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] interface {
GetRound() int
GetCamp() Camp
GetEntity() Entity
GetActionTimeoutDuration() time.Duration
GetActionStartTime() time.Time
GetActionEndTime() time.Time
Stop()
}
```
<span id="struct_TurnBasedControllerAction"></span>
### TurnBasedControllerAction `INTERFACE`
```go
type TurnBasedControllerAction[CampID comparable, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] interface {
TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]
Finish()
Refresh(duration time.Duration) time.Time
}
```
<span id="struct_TurnBasedController"></span>
### TurnBasedController `STRUCT`
回合制控制器
```go
type TurnBasedController[CampID comparable, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] struct {
tb *TurnBased[CampID, EntityID, Camp, Entity]
}
```
<span id="struct_TurnBasedController_GetRound"></span>
#### func (*TurnBasedController) GetRound() int
> 获取当前回合数
***
<span id="struct_TurnBasedController_GetCamp"></span>
#### func (*TurnBasedController) GetCamp() Camp
> 获取当前操作阵营
***
<span id="struct_TurnBasedController_GetEntity"></span>
#### func (*TurnBasedController) GetEntity() Entity
> 获取当前操作实体
***
<span id="struct_TurnBasedController_GetActionTimeoutDuration"></span>
#### func (*TurnBasedController) GetActionTimeoutDuration() time.Duration
> 获取当前行动超时时长
***
<span id="struct_TurnBasedController_GetActionStartTime"></span>
#### func (*TurnBasedController) GetActionStartTime() time.Time
> 获取当前行动开始时间
***
<span id="struct_TurnBasedController_GetActionEndTime"></span>
#### func (*TurnBasedController) GetActionEndTime() time.Time
> 获取当前行动结束时间
***
<span id="struct_TurnBasedController_Finish"></span>
#### func (*TurnBasedController) Finish()
> 结束当前操作,将立即切换到下一个操作实体
***
<span id="struct_TurnBasedController_Stop"></span>
#### func (*TurnBasedController) Stop()
> 在当前回合执行完毕后停止回合进程
***
<span id="struct_TurnBasedController_Refresh"></span>
#### func (*TurnBasedController) Refresh(duration time.Duration) time.Time
> 刷新当前操作实体的行动超时时间
> - 当不在行动阶段时,将返回 time.Time 零值
***
<span id="struct_TurnBasedEntitySwitchEventHandler"></span>
### TurnBasedEntitySwitchEventHandler `STRUCT`
```go
type TurnBasedEntitySwitchEventHandler[CampID comparable, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] func(controller TurnBasedControllerAction[CampID, EntityID, Camp, Entity])
```

197
game/fight/turn_based.go Normal file
View File

@ -0,0 +1,197 @@
package fight
import (
"github.com/kercylan98/minotaur/utils/generic"
"sync"
"time"
)
const (
signalFinish = 1 + iota // 操作结束信号
signalRefresh // 刷新操作超时时间信号
)
type signal struct {
sign byte
data any
}
// NewTurnBased 创建一个新的回合制
// - calcNextTurnDuration 将返回下一次行动时间间隔,适用于按照速度计算下一次行动时间间隔的情况。当返回 0 时,将使用默认的行动超时时间
func NewTurnBased[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]](calcNextTurnDuration func(Camp, Entity) time.Duration) *TurnBased[CampID, EntityID, Camp, Entity] {
tb := &TurnBased[CampID, EntityID, Camp, Entity]{
turnBasedEvents: &turnBasedEvents[CampID, EntityID, Camp, Entity]{},
campRel: make(map[EntityID]Camp),
calcNextTurnDuration: calcNextTurnDuration,
actionTimeoutHandler: func(camp Camp, entity Entity) time.Duration {
return 0
},
}
tb.controller = &TurnBasedController[CampID, EntityID, Camp, Entity]{tb: tb}
return tb
}
// TurnBased 回合制
type TurnBased[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] struct {
*turnBasedEvents[CampID, EntityID, Camp, Entity]
controller *TurnBasedController[CampID, EntityID, Camp, Entity] // 控制器
ticker *time.Ticker // 计时器
actionWaitTicker *time.Ticker // 行动等待计时器
actioning bool // 是否正在行动
actionMutex sync.RWMutex // 行动锁
entities []Entity // 所有阵容实体顺序
campRel map[EntityID]Camp // 实体与阵营的关系
calcNextTurnDuration func(Camp, Entity) time.Duration // 下一次行动时间间隔
actionTimeoutHandler func(Camp, Entity) time.Duration // 行动超时时间
signal chan signal // 信号
round int // 当前回合数
currCamp Camp // 当前操作阵营
currEntity Entity // 当前操作实体
currActionTimeout time.Duration // 当前行动超时时间
currStart time.Time // 当前回合开始时间
closeMutex sync.RWMutex // 关闭锁
closed bool
}
// Close 关闭回合制
func (slf *TurnBased[CampID, EntityID, Camp, Entity]) Close() {
slf.closeMutex.Lock()
defer slf.closeMutex.Unlock()
slf.closed = true
}
// AddCamp 添加阵营
func (slf *TurnBased[CampID, EntityID, Camp, Entity]) AddCamp(camp Camp, entity Entity, entities ...Entity) {
for _, e := range append([]Entity{entity}, entities...) {
slf.entities = append(slf.entities, e)
slf.campRel[e.GetId()] = camp
}
}
// SetActionTimeout 设置行动超时时间处理函数
// - 默认情况下行动超时时间函数将始终返回 0
func (slf *TurnBased[CampID, EntityID, Camp, Entity]) SetActionTimeout(actionTimeoutHandler func(Camp, Entity) time.Duration) {
if actionTimeoutHandler == nil {
panic("actionTimeoutHandler can not be nil")
}
slf.actionTimeoutHandler = actionTimeoutHandler
}
// Run 运行
func (slf *TurnBased[CampID, EntityID, Camp, Entity]) Run() {
slf.round = 1
slf.signal = make(chan signal, 1)
var actionDuration = make(map[EntityID]time.Duration)
var actionSubmit = func() {
slf.actionMutex.Lock()
slf.actioning = false
if slf.actionWaitTicker != nil {
slf.actionWaitTicker.Stop()
}
slf.actionMutex.Unlock()
}
for {
slf.closeMutex.RLock()
if slf.closed {
slf.closeMutex.RUnlock()
break
}
slf.closeMutex.RUnlock()
var minDuration *time.Duration
var delay time.Duration
for _, entity := range slf.entities {
camp := slf.campRel[entity.GetId()]
next := slf.calcNextTurnDuration(camp, entity)
accumulate := next + actionDuration[entity.GetId()]
if minDuration == nil || accumulate < *minDuration {
minDuration = &accumulate
slf.currEntity = entity
slf.currCamp = camp
delay = next
}
}
if *minDuration == 0 {
*minDuration = 1 // 防止永远是第一对象行动
}
actionDuration[slf.currEntity.GetId()] = *minDuration
if len(actionDuration) == len(slf.entities) {
for key := range actionDuration {
delete(actionDuration, key)
}
}
if delay > 0 {
if slf.ticker == nil {
slf.ticker = time.NewTicker(delay)
} else {
slf.ticker.Reset(delay)
}
<-slf.ticker.C
}
// 进入回合操作阶段
slf.currActionTimeout = slf.actionTimeoutHandler(slf.currCamp, slf.currEntity)
slf.currStart = time.Now()
slf.actionMutex.Lock()
slf.actioning = true
slf.actionMutex.Unlock()
slf.OnTurnBasedEntitySwitchEvent(slf.controller)
if slf.actionWaitTicker == nil {
slf.actionWaitTicker = time.NewTicker(slf.currActionTimeout)
} else {
slf.actionWaitTicker.Reset(slf.currActionTimeout)
}
breakListen:
for {
wait:
select {
case <-slf.actionWaitTicker.C:
actionSubmit()
slf.OnTurnBasedEntityActionTimeoutEvent(slf.controller)
break breakListen
case sign := <-slf.signal:
switch sign.sign {
case signalFinish:
actionSubmit()
slf.OnTurnBasedEntityActionFinishEvent(slf.controller)
break breakListen
case signalRefresh:
slf.actionWaitTicker.Reset(sign.data.(time.Duration))
goto wait
}
}
}
slf.OnTurnBasedEntityActionSubmitEvent(slf.controller)
if len(actionDuration) == 0 {
slf.round++
}
slf.closeMutex.Lock()
if slf.closed {
if len(actionDuration) == 0 {
slf.round--
}
if slf.ticker != nil {
slf.ticker.Stop()
slf.ticker = nil
}
if slf.actionWaitTicker != nil {
slf.actionWaitTicker.Stop()
slf.actionWaitTicker = nil
}
if slf.signal != nil {
close(slf.signal)
slf.signal = nil
}
slf.closeMutex.Unlock()
break
} else if len(actionDuration) == 0 {
slf.OnTurnBasedRoundChangeEvent(slf.controller)
}
slf.closeMutex.Unlock()
}
}

View File

@ -0,0 +1,94 @@
package fight
import (
"github.com/kercylan98/minotaur/utils/generic"
"time"
)
type TurnBasedControllerInfo[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] interface {
// GetRound 获取当前回合数
GetRound() int
// GetCamp 获取当前操作阵营
GetCamp() Camp
// GetEntity 获取当前操作实体
GetEntity() Entity
// GetActionTimeoutDuration 获取当前行动超时时长
GetActionTimeoutDuration() time.Duration
// GetActionStartTime 获取当前行动开始时间
GetActionStartTime() time.Time
// GetActionEndTime 获取当前行动结束时间
GetActionEndTime() time.Time
// Stop 在当前回合执行完毕后停止回合进程
Stop()
}
type TurnBasedControllerAction[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] interface {
TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]
// Finish 结束当前操作,将立即切换到下一个操作实体
Finish()
// Refresh 刷新当前操作实体的行动超时时间并返回新的行动超时时间
Refresh(duration time.Duration) time.Time
}
// TurnBasedController 回合制控制器
type TurnBasedController[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] struct {
tb *TurnBased[CampID, EntityID, Camp, Entity]
}
// GetRound 获取当前回合数
func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetRound() int {
return slf.tb.round
}
// GetCamp 获取当前操作阵营
func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetCamp() Camp {
return slf.tb.currCamp
}
// GetEntity 获取当前操作实体
func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetEntity() Entity {
return slf.tb.currEntity
}
// GetActionTimeoutDuration 获取当前行动超时时长
func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetActionTimeoutDuration() time.Duration {
return slf.tb.currActionTimeout
}
// GetActionStartTime 获取当前行动开始时间
func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetActionStartTime() time.Time {
return slf.tb.currStart
}
// GetActionEndTime 获取当前行动结束时间
func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) GetActionEndTime() time.Time {
return slf.tb.currStart.Add(slf.tb.currActionTimeout)
}
// Finish 结束当前操作,将立即切换到下一个操作实体
func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) Finish() {
slf.tb.actionMutex.Lock()
defer slf.tb.actionMutex.Unlock()
if slf.tb.actioning {
slf.tb.actioning = false
slf.tb.signal <- signal{sign: signalFinish}
}
}
// Stop 在当前回合执行完毕后停止回合进程
func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) Stop() {
slf.tb.Close()
}
// Refresh 刷新当前操作实体的行动超时时间
// - 当不在行动阶段时,将返回 time.Time 零值
func (slf *TurnBasedController[CampID, EntityID, Camp, Entity]) Refresh(duration time.Duration) time.Time {
slf.tb.actionMutex.Lock()
defer slf.tb.actionMutex.Unlock()
if slf.tb.actioning {
slf.tb.actioning = false
slf.tb.signal <- signal{sign: signalRefresh, data: duration}
return time.Now().Add(duration)
}
return time.Time{}
}

View File

@ -0,0 +1,86 @@
package fight
import "github.com/kercylan98/minotaur/utils/generic"
type (
TurnBasedEntitySwitchEventHandler[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] func(controller TurnBasedControllerAction[CampID, EntityID, Camp, Entity])
TurnBasedEntityActionTimeoutEventHandler[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] func(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity])
TurnBasedEntityActionFinishEventHandler[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] func(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity])
TurnBasedEntityActionSubmitEventHandler[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] func(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity])
TurnBasedRoundChangeEventHandler[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] func(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity])
)
type turnBasedEvents[CampID, EntityID comparable, Camp generic.IdR[CampID], Entity generic.IdR[EntityID]] struct {
entitySwitchEventHandlers []TurnBasedEntitySwitchEventHandler[CampID, EntityID, Camp, Entity]
actionTimeoutEventHandlers []TurnBasedEntityActionTimeoutEventHandler[CampID, EntityID, Camp, Entity]
actionFinishEventHandlers []TurnBasedEntityActionFinishEventHandler[CampID, EntityID, Camp, Entity]
actionSubmitEventHandlers []TurnBasedEntityActionSubmitEventHandler[CampID, EntityID, Camp, Entity]
roundChangeEventHandlers []TurnBasedRoundChangeEventHandler[CampID, EntityID, Camp, Entity]
}
// RegTurnBasedEntitySwitchEvent 注册回合制实体切换事件处理函数,该处理函数将在切换到实体切换为操作时机时触发
// - 刚函数通常仅用于告知当前操作实体已经完成切换,适合做一些前置校验,但不应该在该函数中执行长时间阻塞操作
// - 操作计时将在该函数执行完毕后开始
//
// 场景:
// - 回合开始,如果该实体被标记为已死亡,则跳过该实体
func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) RegTurnBasedEntitySwitchEvent(handler TurnBasedEntitySwitchEventHandler[CampID, EntityID, Camp, Entity]) {
slf.entitySwitchEventHandlers = append(slf.entitySwitchEventHandlers, handler)
}
// OnTurnBasedEntitySwitchEvent 触发回合制实体切换事件
func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) OnTurnBasedEntitySwitchEvent(controller TurnBasedControllerAction[CampID, EntityID, Camp, Entity]) {
for _, handler := range slf.entitySwitchEventHandlers {
handler(controller)
}
}
// RegTurnBasedEntityActionTimeoutEvent 注册回合制实体行动超时事件处理函数,该处理函数将在实体行动超时时触发
func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) RegTurnBasedEntityActionTimeoutEvent(handler TurnBasedEntityActionTimeoutEventHandler[CampID, EntityID, Camp, Entity]) {
slf.actionTimeoutEventHandlers = append(slf.actionTimeoutEventHandlers, handler)
}
// OnTurnBasedEntityActionTimeoutEvent 触发回合制实体行动超时事件
func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) OnTurnBasedEntityActionTimeoutEvent(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]) {
for _, handler := range slf.actionTimeoutEventHandlers {
handler(controller)
}
}
// RegTurnBasedEntityActionFinishEvent 注册回合制实体行动结束事件处理函数,该处理函数将在实体行动结束时触发
func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) RegTurnBasedEntityActionFinishEvent(handler TurnBasedEntityActionFinishEventHandler[CampID, EntityID, Camp, Entity]) {
slf.actionFinishEventHandlers = append(slf.actionFinishEventHandlers, handler)
}
// OnTurnBasedEntityActionFinishEvent 触发回合制实体行动结束事件
func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) OnTurnBasedEntityActionFinishEvent(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]) {
for _, handler := range slf.actionFinishEventHandlers {
handler(controller)
}
}
// RegTurnBasedEntityActionSubmitEvent 注册回合制实体行动提交事件处理函数,该处理函数将在实体行动提交时触发
// - 该事件将在实体以任意方式结束行动时触发,包括正常结束、超时结束等
// - 该事件会在原本的行动结束事件之后触发
func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) RegTurnBasedEntityActionSubmitEvent(handler TurnBasedEntityActionSubmitEventHandler[CampID, EntityID, Camp, Entity]) {
slf.actionSubmitEventHandlers = append(slf.actionSubmitEventHandlers, handler)
}
// OnTurnBasedEntityActionSubmitEvent 触发回合制实体行动提交事件
func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) OnTurnBasedEntityActionSubmitEvent(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]) {
for _, handler := range slf.actionSubmitEventHandlers {
handler(controller)
}
}
// RegTurnBasedRoundChangeEvent 注册回合制回合变更事件处理函数,该处理函数将在回合变更时触发
func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) RegTurnBasedRoundChangeEvent(handler TurnBasedRoundChangeEventHandler[CampID, EntityID, Camp, Entity]) {
slf.roundChangeEventHandlers = append(slf.roundChangeEventHandlers, handler)
}
// OnTurnBasedRoundChangeEvent 触发回合制回合变更事件
func (slf *turnBasedEvents[CampID, EntityID, Camp, Entity]) OnTurnBasedRoundChangeEvent(controller TurnBasedControllerInfo[CampID, EntityID, Camp, Entity]) {
for _, handler := range slf.roundChangeEventHandlers {
handler(controller)
}
}

View File

@ -0,0 +1,62 @@
package fight_test
import (
"github.com/kercylan98/minotaur/game/fight"
"testing"
"time"
)
type Camp struct {
id string
}
func (slf *Camp) GetId() string {
return slf.id
}
type Entity struct {
id string
speed float64
}
func (slf *Entity) GetId() string {
return slf.id
}
func TestTurnBased_Run(t *testing.T) {
tbi := fight.NewTurnBased[string, string, *Camp, *Entity](func(camp *Camp, entity *Entity) time.Duration {
return time.Duration(float64(time.Second) / entity.speed)
})
tbi.SetActionTimeout(func(camp *Camp, entity *Entity) time.Duration {
return time.Second * 5
})
tbi.RegTurnBasedEntityActionTimeoutEvent(func(controller fight.TurnBasedControllerInfo[string, string, *Camp, *Entity]) {
t.Log("时间", time.Now().Unix(), "回合", controller.GetRound(), "阵营", controller.GetCamp().GetId(), "实体", controller.GetEntity().GetId(), "超时")
})
tbi.RegTurnBasedRoundChangeEvent(func(controller fight.TurnBasedControllerInfo[string, string, *Camp, *Entity]) {
t.Log("时间", time.Now().Unix(), "回合", controller.GetRound(), "回合切换")
})
tbi.RegTurnBasedEntitySwitchEvent(func(controller fight.TurnBasedControllerAction[string, string, *Camp, *Entity]) {
switch controller.GetEntity().GetId() {
case "1":
go func() {
time.Sleep(time.Second * 2)
controller.Finish()
}()
case "2":
controller.Refresh(time.Second)
case "4":
controller.Stop()
}
t.Log("时间", time.Now().Unix(), "回合", controller.GetRound(), "阵营", controller.GetCamp().GetId(), "实体", controller.GetEntity().GetId(), "开始行动")
})
tbi.AddCamp(&Camp{id: "1"}, &Entity{id: "1", speed: 1}, &Entity{id: "2", speed: 1})
tbi.AddCamp(&Camp{id: "2"}, &Entity{id: "3", speed: 1}, &Entity{id: "4", speed: 1})
tbi.Run()
}

434
game/space/README.md Normal file
View File

@ -0,0 +1,434 @@
# Space
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
space 游戏中常见的空间设计,例如房间、地图等
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 包级函数定义
|函数名称|描述
|:--|:--
|[NewRoomManager](#NewRoomManager)|创建房间管理器 RoomManager 的实例
|[NewRoomControllerOptions](#NewRoomControllerOptions)|创建房间控制器选项
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`STRUCT`|[RoomController](#struct_RoomController)|对房间进行操作的控制器,由 RoomManager 接管后返回
|`STRUCT`|[RoomManager](#struct_RoomManager)|房间管理器是用于对房间进行管理的基本单元,通过该实例可以对房间进行增删改查等操作
|`STRUCT`|[RoomAssumeControlEventHandle](#struct_RoomAssumeControlEventHandle)|暂无描述...
|`STRUCT`|[RoomControllerOptions](#struct_RoomControllerOptions)|暂无描述...
</details>
***
## 详情信息
#### func NewRoomManager\[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]\]() *RoomManager[EntityID, RoomID, Entity, Room]
<span id="NewRoomManager"></span>
> 创建房间管理器 RoomManager 的实例
**示例代码:**
```go
func ExampleNewRoomManager() {
var rm = space.NewRoomManager[string, int64, *Player, *Room]()
fmt.Println(rm == nil)
}
```
***
#### func NewRoomControllerOptions\[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]\]() *RoomControllerOptions[EntityID, RoomID, Entity, Room]
<span id="NewRoomControllerOptions"></span>
> 创建房间控制器选项
***
<span id="struct_RoomController"></span>
### RoomController `STRUCT`
对房间进行操作的控制器,由 RoomManager 接管后返回
```go
type RoomController[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] struct {
manager *RoomManager[EntityID, RoomID, Entity, Room]
options *RoomControllerOptions[EntityID, RoomID, Entity, Room]
room Room
entities map[EntityID]Entity
entitiesRWMutex sync.RWMutex
vacancy []int
seat []*EntityID
owner *EntityID
}
```
<span id="struct_RoomController_HasOwner"></span>
#### func (*RoomController) HasOwner() bool
> 判断是否有房主
***
<span id="struct_RoomController_IsOwner"></span>
#### func (*RoomController) IsOwner(entityId EntityID) bool
> 判断是否为房主
***
<span id="struct_RoomController_GetOwner"></span>
#### func (*RoomController) GetOwner() Entity
> 获取房主
***
<span id="struct_RoomController_GetOwnerID"></span>
#### func (*RoomController) GetOwnerID() EntityID
> 获取房主 ID
***
<span id="struct_RoomController_GetOwnerExist"></span>
#### func (*RoomController) GetOwnerExist() ( Entity, bool)
> 获取房间,并返回房主是否存在的状态
***
<span id="struct_RoomController_SetOwner"></span>
#### func (*RoomController) SetOwner(entityId EntityID)
> 设置房主
***
<span id="struct_RoomController_DelOwner"></span>
#### func (*RoomController) DelOwner()
> 删除房主,将房间设置为无主的状态
***
<span id="struct_RoomController_JoinSeat"></span>
#### func (*RoomController) JoinSeat(entityId EntityID, seat ...int) error
> 设置特定对象加入座位,当具体的座位不存在的时候,将会自动分配座位
> - 当目标座位存在玩家或未添加到房间中的时候,将会返回错误
***
<span id="struct_RoomController_LeaveSeat"></span>
#### func (*RoomController) LeaveSeat(entityId EntityID)
> 离开座位
***
<span id="struct_RoomController_GetSeat"></span>
#### func (*RoomController) GetSeat(entityId EntityID) int
> 获取座位
***
<span id="struct_RoomController_GetFirstNotEmptySeat"></span>
#### func (*RoomController) GetFirstNotEmptySeat() int
> 获取第一个非空座位号,如果没有非空座位,将返回 UnknownSeat
***
<span id="struct_RoomController_GetFirstEmptySeatEntity"></span>
#### func (*RoomController) GetFirstEmptySeatEntity() (entity Entity)
> 获取第一个空座位上的实体,如果没有空座位,将返回空实体
***
<span id="struct_RoomController_GetRandomEntity"></span>
#### func (*RoomController) GetRandomEntity() (entity Entity)
> 获取随机实体,如果房间中没有实体,将返回空实体
***
<span id="struct_RoomController_GetNotEmptySeat"></span>
#### func (*RoomController) GetNotEmptySeat() []int
> 获取非空座位
***
<span id="struct_RoomController_GetEmptySeat"></span>
#### func (*RoomController) GetEmptySeat() []int
> 获取空座位
> - 空座位需要在有对象离开座位后才可能出现
***
<span id="struct_RoomController_HasSeat"></span>
#### func (*RoomController) HasSeat(entityId EntityID) bool
> 判断是否有座位
***
<span id="struct_RoomController_GetSeatEntityCount"></span>
#### func (*RoomController) GetSeatEntityCount() int
> 获取座位上的实体数量
***
<span id="struct_RoomController_GetSeatEntities"></span>
#### func (*RoomController) GetSeatEntities() map[EntityID]Entity
> 获取座位上的实体
***
<span id="struct_RoomController_GetSeatEntitiesByOrdered"></span>
#### func (*RoomController) GetSeatEntitiesByOrdered() []Entity
> 有序的获取座位上的实体
***
<span id="struct_RoomController_GetSeatEntitiesByOrderedAndContainsEmpty"></span>
#### func (*RoomController) GetSeatEntitiesByOrderedAndContainsEmpty() []Entity
> 获取有序的座位上的实体,包含空座位
***
<span id="struct_RoomController_GetSeatEntity"></span>
#### func (*RoomController) GetSeatEntity(seat int) (entity Entity)
> 获取座位上的实体
***
<span id="struct_RoomController_ContainEntity"></span>
#### func (*RoomController) ContainEntity(id EntityID) bool
> 房间内是否包含实体
***
<span id="struct_RoomController_GetRoom"></span>
#### func (*RoomController) GetRoom() Room
> 获取原始房间实例,该实例为被接管的房间的原始实例
***
<span id="struct_RoomController_GetEntities"></span>
#### func (*RoomController) GetEntities() map[EntityID]Entity
> 获取所有实体
***
<span id="struct_RoomController_HasEntity"></span>
#### func (*RoomController) HasEntity(id EntityID) bool
> 判断是否有实体
***
<span id="struct_RoomController_GetEntity"></span>
#### func (*RoomController) GetEntity(id EntityID) Entity
> 获取实体
***
<span id="struct_RoomController_GetEntityExist"></span>
#### func (*RoomController) GetEntityExist(id EntityID) ( Entity, bool)
> 获取实体,并返回实体是否存在的状态
***
<span id="struct_RoomController_GetEntityIDs"></span>
#### func (*RoomController) GetEntityIDs() []EntityID
> 获取所有实体ID
***
<span id="struct_RoomController_GetEntityCount"></span>
#### func (*RoomController) GetEntityCount() int
> 获取实体数量
***
<span id="struct_RoomController_ChangePassword"></span>
#### func (*RoomController) ChangePassword(password *string)
> 修改房间密码
> - 当房间密码为 nil 时,将会取消密码
***
<span id="struct_RoomController_AddEntity"></span>
#### func (*RoomController) AddEntity(entity Entity) error
> 添加实体,如果房间存在密码,应使用 AddEntityByPassword 函数进行添加,否则将始终返回 ErrRoomPasswordNotMatch 错误
> - 当房间已满时,将会返回 ErrRoomFull 错误
***
<span id="struct_RoomController_AddEntityByPassword"></span>
#### func (*RoomController) AddEntityByPassword(entity Entity, password string) error
> 通过房间密码添加实体到该房间中
> - 当未设置房间密码时password 参数将会被忽略
> - 当房间密码不匹配时,将会返回 ErrRoomPasswordNotMatch 错误
> - 当房间已满时,将会返回 ErrRoomFull 错误
***
<span id="struct_RoomController_RemoveEntity"></span>
#### func (*RoomController) RemoveEntity(id EntityID)
> 移除实体
> - 当实体被移除时如果实体在座位上,将会自动离开座位
> - 如果实体为房主,将会根据 RoomControllerOptions.WithOwnerInherit 函数的设置进行继承
***
<span id="struct_RoomController_RemoveAllEntities"></span>
#### func (*RoomController) RemoveAllEntities()
> 移除该房间中的所有实体
> - 当实体被移除时如果实体在座位上,将会自动离开座位
> - 如果实体为房主,将会根据 RoomControllerOptions.WithOwnerInherit 函数的设置进行继承
***
<span id="struct_RoomController_Destroy"></span>
#### func (*RoomController) Destroy()
> 销毁房间,房间会从 RoomManager 中移除,同时所有房间的实体、座位等数据都会被清空
> - 该函数与 RoomManager.DestroyRoom 相同RoomManager.DestroyRoom 函数为该函数的快捷方式
***
<span id="struct_RoomController_GetRoomManager"></span>
#### func (*RoomController) GetRoomManager() *RoomManager[EntityID, RoomID, Entity, Room]
> 获取该房间控制器所属的房间管理器
***
<span id="struct_RoomController_GetRoomID"></span>
#### func (*RoomController) GetRoomID() RoomID
> 获取房间 ID
***
<span id="struct_RoomController_Broadcast"></span>
#### func (*RoomController) Broadcast(handler func ( Entity), conditions ...func ( Entity) bool)
> 广播,该函数会将所有房间中满足 conditions 的对象传入 handler 中进行处理
***
<span id="struct_RoomManager"></span>
### RoomManager `STRUCT`
房间管理器是用于对房间进行管理的基本单元,通过该实例可以对房间进行增删改查等操作
- 该实例是线程安全的
```go
type RoomManager[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] struct {
*roomManagerEvents[EntityID, RoomID, Entity, Room]
roomsRWMutex sync.RWMutex
rooms map[RoomID]*RoomController[EntityID, RoomID, Entity, Room]
}
```
<span id="struct_RoomManager_AssumeControl"></span>
#### func (*RoomManager) AssumeControl(room Room, options ...*RoomControllerOptions[EntityID, RoomID, Entity, Room]) *RoomController[EntityID, RoomID, Entity, Room]
> 将房间控制权交由 RoomManager 接管,返回 RoomController 实例
> - 当任何房间需要被 RoomManager 管理时,都应该调用该方法获取到 RoomController 实例后进行操作
> - 房间被接管后需要在释放房间控制权时调用 RoomController.Destroy 方法,否则将会导致 RoomManager 一直持有房间资源
**示例代码:**
```go
func ExampleRoomManager_AssumeControl() {
var rm = space.NewRoomManager[string, int64, *Player, *Room]()
var room = &Room{Id: 1}
var controller = rm.AssumeControl(room)
if err := controller.AddEntity(&Player{Id: "1"}); err != nil {
panic(err)
}
fmt.Println(controller.GetEntityCount())
}
```
***
<span id="struct_RoomManager_DestroyRoom"></span>
#### func (*RoomManager) DestroyRoom(id RoomID)
> 销毁房间,该函数为 RoomController.Destroy 的快捷方式
***
<span id="struct_RoomManager_GetRoom"></span>
#### func (*RoomManager) GetRoom(id RoomID) *RoomController[EntityID, RoomID, Entity, Room]
> 通过房间 ID 获取对应房间的控制器 RoomController当房间不存在时将返回 nil
***
<span id="struct_RoomManager_GetRooms"></span>
#### func (*RoomManager) GetRooms() map[RoomID]*RoomController[EntityID, RoomID, Entity, Room]
> 获取包含所有房间 ID 到对应控制器 RoomController 的映射
> - 返回值的 map 为拷贝对象,可安全的对其进行增删等操作
***
<span id="struct_RoomManager_GetRoomCount"></span>
#### func (*RoomManager) GetRoomCount() int
> 获取房间管理器接管的房间数量
***
<span id="struct_RoomManager_GetRoomIDs"></span>
#### func (*RoomManager) GetRoomIDs() []RoomID
> 获取房间管理器接管的所有房间 ID
***
<span id="struct_RoomManager_HasEntity"></span>
#### func (*RoomManager) HasEntity(entityId EntityID) bool
> 判断特定对象是否在任一房间中,当对象不在任一房间中时将返回 false
***
<span id="struct_RoomManager_GetEntityRooms"></span>
#### func (*RoomManager) GetEntityRooms(entityId EntityID) map[RoomID]*RoomController[EntityID, RoomID, Entity, Room]
> 获取特定对象所在的房间,返回值为房间 ID 到对应控制器 RoomController 的映射
> - 由于一个对象可能在多个房间中,因此返回值为 map 类型
***
<span id="struct_RoomManager_Broadcast"></span>
#### func (*RoomManager) Broadcast(handler func ( Entity), conditions ...func ( Entity) bool)
> 向所有房间对象广播消息,该方法将会遍历所有房间控制器并调用 RoomController.Broadcast 方法
***
<span id="struct_RoomAssumeControlEventHandle"></span>
### RoomAssumeControlEventHandle `STRUCT`
```go
type RoomAssumeControlEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room])
```
<span id="struct_RoomControllerOptions"></span>
### RoomControllerOptions `STRUCT`
```go
type RoomControllerOptions[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] struct {
maxEntityCount *int
password *string
ownerInherit bool
ownerInheritHandler func(controller *RoomController[EntityID, RoomID, Entity, Room]) *EntityID
}
```
<span id="struct_RoomControllerOptions_WithOwnerInherit"></span>
#### func (*RoomControllerOptions) WithOwnerInherit(inherit bool, inheritHandler ...func (controller *RoomController[EntityID, RoomID, Entity, Room]) *EntityID) *RoomControllerOptions[EntityID, RoomID, Entity, Room]
> 设置房间所有者是否继承,默认为 false
> - inherit: 是否继承,当未设置 inheritHandler 且 inherit 为 true 时,将会按照随机或根据座位号顺序继承房间所有者
> - inheritHandler: 继承处理函数,当 inherit 为 true 时,该函数将会被调用,传入当前房间中的所有实体,返回值为新的房间所有者
***
<span id="struct_RoomControllerOptions_WithMaxEntityCount"></span>
#### func (*RoomControllerOptions) WithMaxEntityCount(maxEntityCount int) *RoomControllerOptions[EntityID, RoomID, Entity, Room]
> 设置房间最大实体数量
***
<span id="struct_RoomControllerOptions_WithPassword"></span>
#### func (*RoomControllerOptions) WithPassword(password string) *RoomControllerOptions[EntityID, RoomID, Entity, Room]
> 设置房间密码
***

2
game/space/doc.go Normal file
View File

@ -0,0 +1,2 @@
// Package space 游戏中常见的空间设计,例如房间、地图等
package space

View File

@ -0,0 +1,466 @@
package space
import (
"github.com/kercylan98/minotaur/utils/collection"
"github.com/kercylan98/minotaur/utils/generic"
"sync"
)
const UnknownSeat = -1 // 未知座位号
func newRoomController[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]](manager *RoomManager[EntityID, RoomID, Entity, Room], room Room, options *RoomControllerOptions[EntityID, RoomID, Entity, Room]) *RoomController[EntityID, RoomID, Entity, Room] {
controller := &RoomController[EntityID, RoomID, Entity, Room]{
manager: manager,
options: options,
entities: make(map[EntityID]Entity),
room: room,
}
manager.roomsRWMutex.Lock()
defer manager.roomsRWMutex.Unlock()
manager.rooms[room.GetId()] = controller
return controller
}
// RoomController 对房间进行操作的控制器,由 RoomManager 接管后返回
type RoomController[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] struct {
manager *RoomManager[EntityID, RoomID, Entity, Room]
options *RoomControllerOptions[EntityID, RoomID, Entity, Room]
room Room
entities map[EntityID]Entity
entitiesRWMutex sync.RWMutex
vacancy []int // 空缺的座位
seat []*EntityID // 座位上的玩家
owner *EntityID // 房主
}
// HasOwner 判断是否有房主
func (rc *RoomController[EntityID, RoomID, Entity, Room]) HasOwner() bool {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
return rc.owner != nil
}
// IsOwner 判断是否为房主
func (rc *RoomController[EntityID, RoomID, Entity, Room]) IsOwner(entityId EntityID) bool {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
return rc.owner != nil && *rc.owner == entityId
}
// GetOwner 获取房主
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetOwner() Entity {
return rc.GetEntity(*rc.owner)
}
// GetOwnerID 获取房主 ID
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetOwnerID() EntityID {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
return *rc.owner
}
// GetOwnerExist 获取房间,并返回房主是否存在的状态
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetOwnerExist() (Entity, bool) {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
entity, exist := rc.entities[*rc.owner]
return entity, exist
}
// SetOwner 设置房主
func (rc *RoomController[EntityID, RoomID, Entity, Room]) SetOwner(entityId EntityID) {
rc.entitiesRWMutex.Lock()
defer rc.entitiesRWMutex.Unlock()
old := *rc.owner
rc.owner = &entityId
rc.manager.OnRoomOwnerChangeEvent(rc, &old, &entityId)
}
// DelOwner 删除房主,将房间设置为无主的状态
func (rc *RoomController[EntityID, RoomID, Entity, Room]) DelOwner() {
rc.entitiesRWMutex.Lock()
defer rc.entitiesRWMutex.Unlock()
old := *rc.owner
rc.owner = nil
rc.manager.OnRoomOwnerChangeEvent(rc, &old, nil)
}
// JoinSeat 设置特定对象加入座位,当具体的座位不存在的时候,将会自动分配座位
// - 当目标座位存在玩家或未添加到房间中的时候,将会返回错误
func (rc *RoomController[EntityID, RoomID, Entity, Room]) JoinSeat(entityId EntityID, seat ...int) error {
rc.entitiesRWMutex.Lock()
defer rc.entitiesRWMutex.Unlock()
_, exist := rc.entities[entityId]
if !exist {
return ErrNotInRoom
}
var targetSeat int
if len(seat) > 0 {
targetSeat = seat[0]
if targetSeat < len(rc.seat) && rc.seat[targetSeat] != nil {
return ErrSeatNotEmpty
}
} else {
if len(rc.vacancy) > 0 {
targetSeat = rc.vacancy[0]
rc.vacancy = rc.vacancy[1:]
} else {
targetSeat = len(rc.seat)
}
}
if targetSeat >= len(rc.seat) {
rc.seat = append(rc.seat, make([]*EntityID, targetSeat-len(rc.seat)+1)...)
}
rc.seat[targetSeat] = &entityId
return nil
}
// LeaveSeat 离开座位
func (rc *RoomController[EntityID, RoomID, Entity, Room]) LeaveSeat(entityId EntityID) {
rc.entitiesRWMutex.Lock()
defer rc.entitiesRWMutex.Unlock()
rc.leaveSeat(entityId)
}
// leaveSeat 离开座位(无锁)
func (rc *RoomController[EntityID, RoomID, Entity, Room]) leaveSeat(entityId EntityID) {
for i, seat := range rc.seat {
if seat != nil && *seat == entityId {
rc.seat[i] = nil
rc.vacancy = append(rc.vacancy, i)
break
}
}
}
// GetSeat 获取座位
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeat(entityId EntityID) int {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
for i, seat := range rc.seat {
if seat != nil && *seat == entityId {
return i
}
}
return UnknownSeat
}
// GetFirstNotEmptySeat 获取第一个非空座位号,如果没有非空座位,将返回 UnknownSeat
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetFirstNotEmptySeat() int {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
for i, seat := range rc.seat {
if seat != nil {
return i
}
}
return UnknownSeat
}
// GetFirstEmptySeatEntity 获取第一个空座位上的实体,如果没有空座位,将返回空实体
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetFirstEmptySeatEntity() (entity Entity) {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
for _, seat := range rc.seat {
if seat == nil {
return rc.entities[*seat]
}
}
return entity
}
// GetRandomEntity 获取随机实体,如果房间中没有实体,将返回空实体
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetRandomEntity() (entity Entity) {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
for _, entity = range rc.entities {
return entity
}
return entity
}
// GetNotEmptySeat 获取非空座位
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetNotEmptySeat() []int {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
var seats []int
for i, player := range rc.seat {
if player != nil {
seats = append(seats, i)
}
}
return seats
}
// GetEmptySeat 获取空座位
// - 空座位需要在有对象离开座位后才可能出现
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEmptySeat() []int {
return collection.CloneSlice(rc.vacancy)
}
// HasSeat 判断是否有座位
func (rc *RoomController[EntityID, RoomID, Entity, Room]) HasSeat(entityId EntityID) bool {
return rc.GetSeat(entityId) != UnknownSeat
}
// GetSeatEntityCount 获取座位上的实体数量
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntityCount() int {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
var count int
for _, seat := range rc.seat {
if seat != nil {
count++
}
}
return count
}
// GetSeatEntities 获取座位上的实体
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntities() map[EntityID]Entity {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
var entities = make(map[EntityID]Entity)
for _, entityId := range rc.seat {
if entityId != nil {
entities[*entityId] = rc.entities[*entityId]
}
}
return entities
}
// GetSeatEntitiesByOrdered 有序的获取座位上的实体
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntitiesByOrdered() []Entity {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
var entities = make([]Entity, 0, len(rc.seat))
for _, entityId := range rc.seat {
if entityId != nil {
entities = append(entities, rc.entities[*entityId])
}
}
return entities
}
// GetSeatEntitiesByOrderedAndContainsEmpty 获取有序的座位上的实体,包含空座位
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntitiesByOrderedAndContainsEmpty() []Entity {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
var entities = make([]Entity, len(rc.seat))
for i, entityId := range rc.seat {
if entityId != nil {
entities[i] = rc.entities[*entityId]
}
}
return entities
}
// GetSeatEntity 获取座位上的实体
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntity(seat int) (entity Entity) {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
if seat < len(rc.seat) {
eid := rc.seat[seat]
if eid != nil {
return rc.entities[*eid]
}
}
return entity
}
// ContainEntity 房间内是否包含实体
func (rc *RoomController[EntityID, RoomID, Entity, Room]) ContainEntity(id EntityID) bool {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
_, exist := rc.entities[id]
return exist
}
// GetRoom 获取原始房间实例,该实例为被接管的房间的原始实例
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetRoom() Room {
return rc.room
}
// GetEntities 获取所有实体
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEntities() map[EntityID]Entity {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
return collection.CloneMap(rc.entities)
}
// HasEntity 判断是否有实体
func (rc *RoomController[EntityID, RoomID, Entity, Room]) HasEntity(id EntityID) bool {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
_, exist := rc.entities[id]
return exist
}
// GetEntity 获取实体
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEntity(id EntityID) Entity {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
return rc.entities[id]
}
// GetEntityExist 获取实体,并返回实体是否存在的状态
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEntityExist(id EntityID) (Entity, bool) {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
entity, exist := rc.entities[id]
return entity, exist
}
// GetEntityIDs 获取所有实体ID
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEntityIDs() []EntityID {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
return collection.ConvertMapKeysToSlice(rc.entities)
}
// GetEntityCount 获取实体数量
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEntityCount() int {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
return len(rc.entities)
}
// ChangePassword 修改房间密码
// - 当房间密码为 nil 时,将会取消密码
func (rc *RoomController[EntityID, RoomID, Entity, Room]) ChangePassword(password *string) {
old := rc.options.password
rc.options.password = password
rc.manager.OnRoomChangePasswordEvent(rc, old, rc.options.password)
}
// AddEntity 添加实体,如果房间存在密码,应使用 AddEntityByPassword 函数进行添加,否则将始终返回 ErrRoomPasswordNotMatch 错误
// - 当房间已满时,将会返回 ErrRoomFull 错误
func (rc *RoomController[EntityID, RoomID, Entity, Room]) AddEntity(entity Entity) error {
if rc.options.password != nil {
return ErrRoomPasswordNotMatch
}
rc.entitiesRWMutex.Lock()
defer rc.entitiesRWMutex.Unlock()
if rc.options.maxEntityCount != nil && len(rc.entities) > *rc.options.maxEntityCount {
return ErrRoomFull
}
rc.entities[entity.GetId()] = entity
rc.manager.OnRoomAddEntityEvent(rc, entity)
return nil
}
// AddEntityByPassword 通过房间密码添加实体到该房间中
// - 当未设置房间密码时password 参数将会被忽略
// - 当房间密码不匹配时,将会返回 ErrRoomPasswordNotMatch 错误
// - 当房间已满时,将会返回 ErrRoomFull 错误
func (rc *RoomController[EntityID, RoomID, Entity, Room]) AddEntityByPassword(entity Entity, password string) error {
if rc.options.password == nil || *rc.options.password != password {
return ErrRoomPasswordNotMatch
}
rc.entitiesRWMutex.Lock()
defer rc.entitiesRWMutex.Unlock()
if rc.options.maxEntityCount != nil && len(rc.entities) > *rc.options.maxEntityCount {
return ErrRoomFull
}
rc.entities[entity.GetId()] = entity
rc.manager.OnRoomAddEntityEvent(rc, entity)
return nil
}
// RemoveEntity 移除实体
// - 当实体被移除时如果实体在座位上,将会自动离开座位
// - 如果实体为房主,将会根据 RoomControllerOptions.WithOwnerInherit 函数的设置进行继承
func (rc *RoomController[EntityID, RoomID, Entity, Room]) RemoveEntity(id EntityID) {
rc.entitiesRWMutex.RLock()
defer rc.entitiesRWMutex.RUnlock()
rc.removeEntity(id)
}
// removeEntity 移除实体(无锁)
func (rc *RoomController[EntityID, RoomID, Entity, Room]) removeEntity(id EntityID) {
rc.leaveSeat(id)
entity, exist := rc.entities[id]
if !exist {
return
}
delete(rc.entities, id)
if !rc.options.ownerInherit {
if rc.owner != nil && *rc.owner == id {
rc.owner = nil
}
} else {
if rc.owner != nil && *rc.owner == id {
rc.owner = rc.options.ownerInheritHandler(rc)
defer rc.manager.OnRoomOwnerChangeEvent(rc, &id, rc.owner)
}
}
rc.manager.OnRoomRemoveEntityEvent(rc, entity)
}
// RemoveAllEntities 移除该房间中的所有实体
// - 当实体被移除时如果实体在座位上,将会自动离开座位
// - 如果实体为房主,将会根据 RoomControllerOptions.WithOwnerInherit 函数的设置进行继承
func (rc *RoomController[EntityID, RoomID, Entity, Room]) RemoveAllEntities() {
rc.entitiesRWMutex.Lock()
defer rc.entitiesRWMutex.Unlock()
for id := range rc.entities {
rc.removeEntity(id)
delete(rc.entities, id)
}
}
// Destroy 销毁房间,房间会从 RoomManager 中移除,同时所有房间的实体、座位等数据都会被清空
// - 该函数与 RoomManager.DestroyRoom 相同RoomManager.DestroyRoom 函数为该函数的快捷方式
func (rc *RoomController[EntityID, RoomID, Entity, Room]) Destroy() {
rc.manager.roomsRWMutex.Lock()
defer rc.manager.roomsRWMutex.Unlock()
delete(rc.manager.rooms, rc.room.GetId())
rc.manager.OnRoomDestroyEvent(rc)
rc.entitiesRWMutex.Lock()
defer rc.entitiesRWMutex.Unlock()
for eid := range rc.entities {
rc.removeEntity(eid)
delete(rc.entities, eid)
}
rc.entities = make(map[EntityID]Entity)
rc.seat = rc.seat[:]
rc.vacancy = rc.vacancy[:]
}
// GetRoomManager 获取该房间控制器所属的房间管理器
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetRoomManager() *RoomManager[EntityID, RoomID, Entity, Room] {
return rc.manager
}
// GetRoomID 获取房间 ID
func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetRoomID() RoomID {
return rc.room.GetId()
}
// Broadcast 广播,该函数会将所有房间中满足 conditions 的对象传入 handler 中进行处理
func (rc *RoomController[EntityID, RoomID, Entity, Room]) Broadcast(handler func(Entity), conditions ...func(Entity) bool) {
rc.entitiesRWMutex.RLock()
entities := collection.CloneMap(rc.entities)
rc.entitiesRWMutex.RUnlock()
for _, entity := range entities {
for _, condition := range conditions {
if !condition(entity) {
continue
}
}
handler(entity)
}
}

16
game/space/room_errors.go Normal file
View File

@ -0,0 +1,16 @@
package space
import "errors"
var (
// ErrRoomFull 房间已满
ErrRoomFull = errors.New("room is full")
// ErrSeatNotEmpty 座位上已经有实体
ErrSeatNotEmpty = errors.New("seat is not empty")
// ErrNotInRoom 实体不在房间中
ErrNotInRoom = errors.New("not in room")
// ErrRoomPasswordNotMatch 房间密码不匹配
ErrRoomPasswordNotMatch = errors.New("room password not match")
// ErrPermissionDenied 权限不足
ErrPermissionDenied = errors.New("permission denied")
)

110
game/space/room_manager.go Normal file
View File

@ -0,0 +1,110 @@
package space
import (
"github.com/kercylan98/minotaur/utils/collection"
"github.com/kercylan98/minotaur/utils/generic"
"sync"
)
// NewRoomManager 创建房间管理器 RoomManager 的实例
func NewRoomManager[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]]() *RoomManager[EntityID, RoomID, Entity, Room] {
return &RoomManager[EntityID, RoomID, Entity, Room]{
roomManagerEvents: new(roomManagerEvents[EntityID, RoomID, Entity, Room]),
rooms: make(map[RoomID]*RoomController[EntityID, RoomID, Entity, Room]),
}
}
// RoomManager 房间管理器是用于对房间进行管理的基本单元,通过该实例可以对房间进行增删改查等操作
// - 该实例是线程安全的
type RoomManager[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] struct {
*roomManagerEvents[EntityID, RoomID, Entity, Room]
roomsRWMutex sync.RWMutex
rooms map[RoomID]*RoomController[EntityID, RoomID, Entity, Room]
}
// AssumeControl 将房间控制权交由 RoomManager 接管,返回 RoomController 实例
// - 当任何房间需要被 RoomManager 管理时,都应该调用该方法获取到 RoomController 实例后进行操作
// - 房间被接管后需要在释放房间控制权时调用 RoomController.Destroy 方法,否则将会导致 RoomManager 一直持有房间资源
func (rm *RoomManager[EntityID, RoomID, Entity, Room]) AssumeControl(room Room, options ...*RoomControllerOptions[EntityID, RoomID, Entity, Room]) *RoomController[EntityID, RoomID, Entity, Room] {
controller := newRoomController(rm, room, mergeRoomControllerOptions(options...))
rm.OnRoomAssumeControlEvent(controller)
return controller
}
// DestroyRoom 销毁房间,该函数为 RoomController.Destroy 的快捷方式
func (rm *RoomManager[EntityID, RoomID, Entity, Room]) DestroyRoom(id RoomID) {
rm.roomsRWMutex.Lock()
room, exist := rm.rooms[id]
rm.roomsRWMutex.Unlock()
if !exist {
return
}
room.Destroy()
}
// GetRoom 通过房间 ID 获取对应房间的控制器 RoomController当房间不存在时将返回 nil
func (rm *RoomManager[EntityID, RoomID, Entity, Room]) GetRoom(id RoomID) *RoomController[EntityID, RoomID, Entity, Room] {
rm.roomsRWMutex.RLock()
defer rm.roomsRWMutex.RUnlock()
return rm.rooms[id]
}
// GetRooms 获取包含所有房间 ID 到对应控制器 RoomController 的映射
// - 返回值的 map 为拷贝对象,可安全的对其进行增删等操作
func (rm *RoomManager[EntityID, RoomID, Entity, Room]) GetRooms() map[RoomID]*RoomController[EntityID, RoomID, Entity, Room] {
rm.roomsRWMutex.RLock()
defer rm.roomsRWMutex.RUnlock()
return collection.CloneMap(rm.rooms)
}
// GetRoomCount 获取房间管理器接管的房间数量
func (rm *RoomManager[EntityID, RoomID, Entity, Room]) GetRoomCount() int {
rm.roomsRWMutex.RLock()
defer rm.roomsRWMutex.RUnlock()
return len(rm.rooms)
}
// GetRoomIDs 获取房间管理器接管的所有房间 ID
func (rm *RoomManager[EntityID, RoomID, Entity, Room]) GetRoomIDs() []RoomID {
rm.roomsRWMutex.RLock()
defer rm.roomsRWMutex.RUnlock()
return collection.ConvertMapKeysToSlice(rm.rooms)
}
// HasEntity 判断特定对象是否在任一房间中,当对象不在任一房间中时将返回 false
func (rm *RoomManager[EntityID, RoomID, Entity, Room]) HasEntity(entityId EntityID) bool {
rm.roomsRWMutex.RLock()
rooms := collection.CloneMap(rm.rooms)
rm.roomsRWMutex.RUnlock()
for _, room := range rooms {
if room.HasEntity(entityId) {
return true
}
}
return false
}
// GetEntityRooms 获取特定对象所在的房间,返回值为房间 ID 到对应控制器 RoomController 的映射
// - 由于一个对象可能在多个房间中,因此返回值为 map 类型
func (rm *RoomManager[EntityID, RoomID, Entity, Room]) GetEntityRooms(entityId EntityID) map[RoomID]*RoomController[EntityID, RoomID, Entity, Room] {
rm.roomsRWMutex.RLock()
rooms := collection.CloneMap(rm.rooms)
rm.roomsRWMutex.RUnlock()
var result = make(map[RoomID]*RoomController[EntityID, RoomID, Entity, Room])
for id, room := range rooms {
if room.HasEntity(entityId) {
result[id] = room
}
}
return result
}
// Broadcast 向所有房间对象广播消息,该方法将会遍历所有房间控制器并调用 RoomController.Broadcast 方法
func (rm *RoomManager[EntityID, RoomID, Entity, Room]) Broadcast(handler func(Entity), conditions ...func(Entity) bool) {
rm.roomsRWMutex.RLock()
rooms := collection.CloneMap(rm.rooms)
rm.roomsRWMutex.RUnlock()
for _, room := range rooms {
room.Broadcast(handler, conditions...)
}
}

View File

@ -0,0 +1,93 @@
package space
import "github.com/kercylan98/minotaur/utils/generic"
type (
RoomAssumeControlEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room])
RoomDestroyEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room])
RoomAddEntityEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room], entity Entity)
RoomRemoveEntityEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room], entity Entity)
RoomChangePasswordEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room], oldPassword, password *string)
RoomOwnerChangeEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room], oldOwner, owner *EntityID)
)
type roomManagerEvents[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] struct {
roomAssumeControlEventHandles []RoomAssumeControlEventHandle[EntityID, RoomID, Entity, Room]
roomDestroyEventHandles []RoomDestroyEventHandle[EntityID, RoomID, Entity, Room]
roomAddEntityEventHandles []RoomAddEntityEventHandle[EntityID, RoomID, Entity, Room]
roomRemoveEntityEventHandles []RoomRemoveEntityEventHandle[EntityID, RoomID, Entity, Room]
roomChangePasswordEventHandles []RoomChangePasswordEventHandle[EntityID, RoomID, Entity, Room]
roomOwnerChangeEventHandles []RoomOwnerChangeEventHandle[EntityID, RoomID, Entity, Room]
}
// RegRoomOwnerChangeEvent 注册房间所有者变更事件,当触发事件时,房间所有者已经被修改
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) RegRoomOwnerChangeEvent(handle RoomOwnerChangeEventHandle[EntityID, RoomID, Entity, Room]) {
rme.roomOwnerChangeEventHandles = append(rme.roomOwnerChangeEventHandles, handle)
}
// OnRoomOwnerChangeEvent 房间所有者变更事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) OnRoomOwnerChangeEvent(controller *RoomController[EntityID, RoomID, Entity, Room], oldOwner, owner *EntityID) {
for _, handle := range rme.roomOwnerChangeEventHandles {
handle(controller, oldOwner, owner)
}
}
// RegRoomAssumeControlEvent 注册房间接管事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) RegRoomAssumeControlEvent(handle RoomAssumeControlEventHandle[EntityID, RoomID, Entity, Room]) {
rme.roomAssumeControlEventHandles = append(rme.roomAssumeControlEventHandles, handle)
}
// OnRoomAssumeControlEvent 房间接管事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) OnRoomAssumeControlEvent(controller *RoomController[EntityID, RoomID, Entity, Room]) {
for _, handle := range rme.roomAssumeControlEventHandles {
handle(controller)
}
}
// RegRoomDestroyEvent 注册房间销毁事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) RegRoomDestroyEvent(handle RoomDestroyEventHandle[EntityID, RoomID, Entity, Room]) {
rme.roomDestroyEventHandles = append(rme.roomDestroyEventHandles, handle)
}
// OnRoomDestroyEvent 房间销毁事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) OnRoomDestroyEvent(controller *RoomController[EntityID, RoomID, Entity, Room]) {
for _, handle := range rme.roomDestroyEventHandles {
handle(controller)
}
}
// RegRoomAddEntityEvent 注册房间添加对象事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) RegRoomAddEntityEvent(handle RoomAddEntityEventHandle[EntityID, RoomID, Entity, Room]) {
rme.roomAddEntityEventHandles = append(rme.roomAddEntityEventHandles, handle)
}
// OnRoomAddEntityEvent 房间添加对象事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) OnRoomAddEntityEvent(controller *RoomController[EntityID, RoomID, Entity, Room], entity Entity) {
for _, handle := range rme.roomAddEntityEventHandles {
handle(controller, entity)
}
}
// RegRoomRemoveEntityEvent 注册房间移除对象事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) RegRoomRemoveEntityEvent(handle RoomRemoveEntityEventHandle[EntityID, RoomID, Entity, Room]) {
rme.roomRemoveEntityEventHandles = append(rme.roomRemoveEntityEventHandles, handle)
}
// OnRoomRemoveEntityEvent 房间移除对象事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) OnRoomRemoveEntityEvent(controller *RoomController[EntityID, RoomID, Entity, Room], entity Entity) {
for _, handle := range rme.roomRemoveEntityEventHandles {
handle(controller, entity)
}
}
// RegRoomChangePasswordEvent 注册房间修改密码事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) RegRoomChangePasswordEvent(handle RoomChangePasswordEventHandle[EntityID, RoomID, Entity, Room]) {
rme.roomChangePasswordEventHandles = append(rme.roomChangePasswordEventHandles, handle)
}
// OnRoomChangePasswordEvent 房间修改密码事件
func (rme *roomManagerEvents[EntityID, RoomID, Entity, Room]) OnRoomChangePasswordEvent(controller *RoomController[EntityID, RoomID, Entity, Room], oldPassword, password *string) {
for _, handle := range rme.roomChangePasswordEventHandles {
handle(controller, oldPassword, password)
}
}

View File

@ -0,0 +1,46 @@
package space_test
import (
"fmt"
"github.com/kercylan98/minotaur/game/space"
)
type Room struct {
Id int64
}
func (r *Room) GetId() int64 {
return r.Id
}
type Player struct {
Id string
}
func (p *Player) GetId() string {
return p.Id
}
func ExampleNewRoomManager() {
var rm = space.NewRoomManager[string, int64, *Player, *Room]()
fmt.Println(rm == nil)
// Output:
// false
}
func ExampleRoomManager_AssumeControl() {
var rm = space.NewRoomManager[string, int64, *Player, *Room]()
var room = &Room{Id: 1}
var controller = rm.AssumeControl(room)
if err := controller.AddEntity(&Player{Id: "1"}); err != nil {
// 房间密码不匹配或者房间已满
panic(err)
}
fmt.Println(controller.GetEntityCount())
// Output:
// 1
}

View File

@ -0,0 +1,65 @@
package space
import "github.com/kercylan98/minotaur/utils/generic"
// NewRoomControllerOptions 创建房间控制器选项
func NewRoomControllerOptions[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]]() *RoomControllerOptions[EntityID, RoomID, Entity, Room] {
return &RoomControllerOptions[EntityID, RoomID, Entity, Room]{}
}
// mergeRoomControllerOptions 合并房间控制器选项
func mergeRoomControllerOptions[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]](options ...*RoomControllerOptions[EntityID, RoomID, Entity, Room]) *RoomControllerOptions[EntityID, RoomID, Entity, Room] {
result := NewRoomControllerOptions[EntityID, RoomID, Entity, Room]()
for _, option := range options {
if option.maxEntityCount != nil {
result.maxEntityCount = option.maxEntityCount
}
}
return result
}
type RoomControllerOptions[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] struct {
maxEntityCount *int // 房间最大实体数量
password *string // 房间密码
ownerInherit bool // 房间所有者是否继承
ownerInheritHandler func(controller *RoomController[EntityID, RoomID, Entity, Room]) *EntityID // 房间所有者继承处理函数
}
// WithOwnerInherit 设置房间所有者是否继承,默认为 false
// - inherit: 是否继承,当未设置 inheritHandler 且 inherit 为 true 时,将会按照随机或根据座位号顺序继承房间所有者
// - inheritHandler: 继承处理函数,当 inherit 为 true 时,该函数将会被调用,传入当前房间中的所有实体,返回值为新的房间所有者
func (rco *RoomControllerOptions[EntityID, RoomID, Entity, Room]) WithOwnerInherit(inherit bool, inheritHandler ...func(controller *RoomController[EntityID, RoomID, Entity, Room]) *EntityID) *RoomControllerOptions[EntityID, RoomID, Entity, Room] {
rco.ownerInherit = inherit
if len(inheritHandler) > 0 {
rco.ownerInheritHandler = inheritHandler[0]
} else if inherit {
rco.ownerInheritHandler = func(controller *RoomController[EntityID, RoomID, Entity, Room]) *EntityID {
if e := controller.GetFirstEmptySeatEntity(); !generic.IsNil(e) {
var id = e.GetId()
return &id
}
if e := controller.GetRandomEntity(); !generic.IsNil(e) {
var id = e.GetId()
return &id
}
return nil
}
}
return rco
}
// WithMaxEntityCount 设置房间最大实体数量
func (rco *RoomControllerOptions[EntityID, RoomID, Entity, Room]) WithMaxEntityCount(maxEntityCount int) *RoomControllerOptions[EntityID, RoomID, Entity, Room] {
if maxEntityCount > 0 {
rco.maxEntityCount = &maxEntityCount
}
return rco
}
// WithPassword 设置房间密码
func (rco *RoomControllerOptions[EntityID, RoomID, Entity, Room]) WithPassword(password string) *RoomControllerOptions[EntityID, RoomID, Entity, Room] {
if password != "" {
rco.password = &password
}
return rco
}

387
game/task/README.md Normal file
View File

@ -0,0 +1,387 @@
# Task
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 包级函数定义
|函数名称|描述
|:--|:--
|[Cond](#Cond)|创建任务条件
|[RegisterRefreshTaskCounterEvent](#RegisterRefreshTaskCounterEvent)|注册特定任务类型的刷新任务计数器事件处理函数
|[OnRefreshTaskCounterEvent](#OnRefreshTaskCounterEvent)|触发特定任务类型的刷新任务计数器事件
|[RegisterRefreshTaskConditionEvent](#RegisterRefreshTaskConditionEvent)|注册特定任务类型的刷新任务条件事件处理函数
|[OnRefreshTaskConditionEvent](#OnRefreshTaskConditionEvent)|触发特定任务类型的刷新任务条件事件
|[WithType](#WithType)|设置任务类型
|[WithCondition](#WithCondition)|设置任务完成条件,当满足条件时,任务状态为完成
|[WithCounter](#WithCounter)|设置任务计数器,当计数器达到要求时,任务状态为完成
|[WithOverflowCounter](#WithOverflowCounter)|设置可溢出的任务计数器,当计数器达到要求时,任务状态为完成
|[WithDeadline](#WithDeadline)|设置任务截止时间,超过截至时间并且任务未完成时,任务状态为失败
|[WithLimitedDuration](#WithLimitedDuration)|设置任务限时,超过限时时间并且任务未完成时,任务状态为失败
|[NewTask](#NewTask)|生成任务
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`STRUCT`|[Condition](#struct_Condition)|任务条件
|`STRUCT`|[RefreshTaskCounterEventHandler](#struct_RefreshTaskCounterEventHandler)|暂无描述...
|`STRUCT`|[Option](#struct_Option)|任务选项
|`STRUCT`|[Status](#struct_Status)|暂无描述...
|`STRUCT`|[Task](#struct_Task)|是对任务信息进行描述和处理的结构体
</details>
***
## 详情信息
#### func Cond(k any, v any) Condition
<span id="Cond"></span>
> 创建任务条件
<details>
<summary>查看 / 收起单元测试</summary>
```go
func TestCond(t *testing.T) {
task := NewTask(WithType("T"), WithCounter(5), WithCondition(Cond("N", 5).Cond("M", 10)))
task.AssignConditionValueAndRefresh("N", 5)
task.AssignConditionValueAndRefresh("M", 10)
RegisterRefreshTaskCounterEvent[*Player](task.Type, func(taskType string, trigger *Player, count int64) {
fmt.Println("Player", count)
for _, t := range trigger.tasks[taskType] {
fmt.Println(t.CurrCount, t.IncrementCounter(count).Status)
}
})
RegisterRefreshTaskConditionEvent[*Player](task.Type, func(taskType string, trigger *Player, condition Condition) {
fmt.Println("Player", condition)
for _, t := range trigger.tasks[taskType] {
fmt.Println(t.CurrCount, t.AssignConditionValueAndRefresh("N", 5).Status)
}
})
RegisterRefreshTaskCounterEvent[*Monster](task.Type, func(taskType string, trigger *Monster, count int64) {
fmt.Println("Monster", count)
})
player := &Player{tasks: map[string][]*Task{task.Type: {task}}}
OnRefreshTaskCounterEvent(task.Type, player, 1)
OnRefreshTaskCounterEvent(task.Type, player, 2)
OnRefreshTaskCounterEvent(task.Type, player, 3)
OnRefreshTaskCounterEvent(task.Type, new(Monster), 3)
}
```
</details>
***
#### func RegisterRefreshTaskCounterEvent\[Trigger any\](taskType string, handler RefreshTaskCounterEventHandler[Trigger])
<span id="RegisterRefreshTaskCounterEvent"></span>
> 注册特定任务类型的刷新任务计数器事件处理函数
***
#### func OnRefreshTaskCounterEvent(taskType string, trigger any, count int64)
<span id="OnRefreshTaskCounterEvent"></span>
> 触发特定任务类型的刷新任务计数器事件
***
#### func RegisterRefreshTaskConditionEvent\[Trigger any\](taskType string, handler RefreshTaskConditionEventHandler[Trigger])
<span id="RegisterRefreshTaskConditionEvent"></span>
> 注册特定任务类型的刷新任务条件事件处理函数
***
#### func OnRefreshTaskConditionEvent(taskType string, trigger any, condition Condition)
<span id="OnRefreshTaskConditionEvent"></span>
> 触发特定任务类型的刷新任务条件事件
***
#### func WithType(taskType string) Option
<span id="WithType"></span>
> 设置任务类型
***
#### func WithCondition(condition Condition) Option
<span id="WithCondition"></span>
> 设置任务完成条件,当满足条件时,任务状态为完成
> - 任务条件值需要变更时可通过 Task.AssignConditionValueAndRefresh 方法变更
> - 当多次设置该选项时,后面的设置会覆盖之前的设置
***
#### func WithCounter(counter int64, initCount ...int64) Option
<span id="WithCounter"></span>
> 设置任务计数器,当计数器达到要求时,任务状态为完成
> - 一些场景下,任务计数器可能会溢出,此时可通过 WithOverflowCounter 设置可溢出的任务计数器
> - 当多次设置该选项时,后面的设置会覆盖之前的设置
> - 如果需要初始化计数器的值,可通过 initCount 参数设置
***
#### func WithOverflowCounter(counter int64, initCount ...int64) Option
<span id="WithOverflowCounter"></span>
> 设置可溢出的任务计数器,当计数器达到要求时,任务状态为完成
> - 当多次设置该选项时,后面的设置会覆盖之前的设置
> - 如果需要初始化计数器的值,可通过 initCount 参数设置
***
#### func WithDeadline(deadline time.Time) Option
<span id="WithDeadline"></span>
> 设置任务截止时间,超过截至时间并且任务未完成时,任务状态为失败
***
#### func WithLimitedDuration(start time.Time, duration time.Duration) Option
<span id="WithLimitedDuration"></span>
> 设置任务限时,超过限时时间并且任务未完成时,任务状态为失败
***
#### func NewTask(options ...Option) *Task
<span id="NewTask"></span>
> 生成任务
***
<span id="struct_Condition"></span>
### Condition `STRUCT`
任务条件
```go
type Condition map[any]any
```
<span id="struct_Condition_Cond"></span>
#### func (Condition) Cond(k any, v any) Condition
> 创建任务条件
***
<span id="struct_Condition_GetString"></span>
#### func (Condition) GetString(key any) string
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetInt"></span>
#### func (Condition) GetInt(key any) int
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetInt8"></span>
#### func (Condition) GetInt8(key any) int8
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetInt16"></span>
#### func (Condition) GetInt16(key any) int16
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetInt32"></span>
#### func (Condition) GetInt32(key any) int32
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetInt64"></span>
#### func (Condition) GetInt64(key any) int64
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetUint"></span>
#### func (Condition) GetUint(key any) uint
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetUint8"></span>
#### func (Condition) GetUint8(key any) uint8
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetUint16"></span>
#### func (Condition) GetUint16(key any) uint16
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetUint32"></span>
#### func (Condition) GetUint32(key any) uint32
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetUint64"></span>
#### func (Condition) GetUint64(key any) uint64
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetFloat32"></span>
#### func (Condition) GetFloat32(key any) float32
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetFloat64"></span>
#### func (Condition) GetFloat64(key any) float64
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetBool"></span>
#### func (Condition) GetBool(key any) bool
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetTime"></span>
#### func (Condition) GetTime(key any) time.Time
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetDuration"></span>
#### func (Condition) GetDuration(key any) time.Duration
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetByte"></span>
#### func (Condition) GetByte(key any) byte
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetBytes"></span>
#### func (Condition) GetBytes(key any) []byte
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetRune"></span>
#### func (Condition) GetRune(key any) rune
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetRunes"></span>
#### func (Condition) GetRunes(key any) []rune
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_Condition_GetAny"></span>
#### func (Condition) GetAny(key any) any
> 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
***
<span id="struct_RefreshTaskCounterEventHandler"></span>
### RefreshTaskCounterEventHandler `STRUCT`
```go
type RefreshTaskCounterEventHandler[Trigger any] func(taskType string, trigger Trigger, count int64)
```
<span id="struct_Option"></span>
### Option `STRUCT`
任务选项
```go
type Option func(task *Task)
```
<span id="struct_Status"></span>
### Status `STRUCT`
```go
type Status byte
```
<span id="struct_Status_String"></span>
#### func (Status) String() string
***
<span id="struct_Task"></span>
### Task `STRUCT`
是对任务信息进行描述和处理的结构体
```go
type Task struct {
Type string
Status Status
Cond Condition
CondValue map[any]any
Counter int64
CurrCount int64
CurrOverflow bool
Deadline time.Time
StartTime time.Time
LimitedDuration time.Duration
}
```
<span id="struct_Task_IsComplete"></span>
#### func (*Task) IsComplete() bool
> 判断任务是否已完成
***
<span id="struct_Task_IsFailed"></span>
#### func (*Task) IsFailed() bool
> 判断任务是否已失败
***
<span id="struct_Task_IsReward"></span>
#### func (*Task) IsReward() bool
> 判断任务是否已领取奖励
***
<span id="struct_Task_ReceiveReward"></span>
#### func (*Task) ReceiveReward() bool
> 领取任务奖励,当任务状态为已完成时,才能领取奖励,此时返回 true并且任务状态变更为已领取奖励
***
<span id="struct_Task_IncrementCounter"></span>
#### func (*Task) IncrementCounter(incr int64) *Task
> 增加计数器的值,当 incr 为负数时,计数器的值不会发生变化
> - 如果需要溢出计数器,可通过 WithOverflowCounter 设置可溢出的任务计数器
***
<span id="struct_Task_DecrementCounter"></span>
#### func (*Task) DecrementCounter(decr int64) *Task
> 减少计数器的值,当 decr 为负数时,计数器的值不会发生变化
***
<span id="struct_Task_AssignConditionValueAndRefresh"></span>
#### func (*Task) AssignConditionValueAndRefresh(key any, value any) *Task
> 分配条件值并刷新任务状态
***
<span id="struct_Task_AssignConditionValueAndRefreshByCondition"></span>
#### func (*Task) AssignConditionValueAndRefreshByCondition(condition Condition) *Task
> 分配条件值并刷新任务状态
***
<span id="struct_Task_ResetStatus"></span>
#### func (*Task) ResetStatus() *Task
> 重置任务状态
> - 该函数会将任务状态重置为已接受状态后,再刷新任务状态
> - 当任务条件变更,例如任务计数要求为 10已经完成的情况下将任务计数要求变更为 5 或 20此时任务状态由于是已完成或已领取状态不会自动刷新需要调用该函数刷新任务状态
***

142
game/task/condition.go Normal file
View File

@ -0,0 +1,142 @@
package task
import "time"
// Cond 创建任务条件
func Cond(k, v any) Condition {
return map[any]any{k: v}
}
// Condition 任务条件
type Condition map[any]any
// Cond 创建任务条件
func (slf Condition) Cond(k, v any) Condition {
slf[k] = v
return slf
}
// GetString 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetString(key any) string {
v, _ := slf[key].(string)
return v
}
// GetInt 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetInt(key any) int {
v, _ := slf[key].(int)
return v
}
// GetInt8 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetInt8(key any) int8 {
v, _ := slf[key].(int8)
return v
}
// GetInt16 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetInt16(key any) int16 {
v, _ := slf[key].(int16)
return v
}
// GetInt32 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetInt32(key any) int32 {
v, _ := slf[key].(int32)
return v
}
// GetInt64 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetInt64(key any) int64 {
v, _ := slf[key].(int64)
return v
}
// GetUint 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetUint(key any) uint {
v, _ := slf[key].(uint)
return v
}
// GetUint8 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetUint8(key any) uint8 {
v, _ := slf[key].(uint8)
return v
}
// GetUint16 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetUint16(key any) uint16 {
v, _ := slf[key].(uint16)
return v
}
// GetUint32 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetUint32(key any) uint32 {
v, _ := slf[key].(uint32)
return v
}
// GetUint64 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetUint64(key any) uint64 {
v, _ := slf[key].(uint64)
return v
}
// GetFloat32 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetFloat32(key any) float32 {
v, _ := slf[key].(float32)
return v
}
// GetFloat64 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetFloat64(key any) float64 {
v, _ := slf[key].(float64)
return v
}
// GetBool 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetBool(key any) bool {
v, _ := slf[key].(bool)
return v
}
// GetTime 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetTime(key any) time.Time {
v, _ := slf[key].(time.Time)
return v
}
// GetDuration 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetDuration(key any) time.Duration {
v, _ := slf[key].(time.Duration)
return v
}
// GetByte 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetByte(key any) byte {
v, _ := slf[key].(byte)
return v
}
// GetBytes 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetBytes(key any) []byte {
v, _ := slf[key].([]byte)
return v
}
// GetRune 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetRune(key any) rune {
v, _ := slf[key].(rune)
return v
}
// GetRunes 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetRunes(key any) []rune {
v, _ := slf[key].([]rune)
return v
}
// GetAny 获取特定类型的任务条件值,该值必须与预期类型一致,否则返回零值
func (slf Condition) GetAny(key any) any {
return slf[key]
}

77
game/task/events.go Normal file
View File

@ -0,0 +1,77 @@
package task
import (
"reflect"
)
type (
RefreshTaskCounterEventHandler[Trigger any] func(taskType string, trigger Trigger, count int64) // 刷新任务计数器事件处理函数
RefreshTaskConditionEventHandler[Trigger any] func(taskType string, trigger Trigger, condition Condition) // 刷新任务条件事件处理函数
)
var (
refreshTaskCounterEventHandlers = make(map[string][]struct {
t reflect.Type
h func(taskType string, trigger any, count int64)
})
refreshTaskConditionEventHandlers = make(map[string][]struct {
t reflect.Type
h func(taskType string, trigger any, condition Condition)
})
)
// RegisterRefreshTaskCounterEvent 注册特定任务类型的刷新任务计数器事件处理函数
func RegisterRefreshTaskCounterEvent[Trigger any](taskType string, handler RefreshTaskCounterEventHandler[Trigger]) {
if refreshTaskCounterEventHandlers == nil {
refreshTaskCounterEventHandlers = make(map[string][]struct {
t reflect.Type
h func(taskType string, trigger any, count int64)
})
}
refreshTaskCounterEventHandlers[taskType] = append(refreshTaskCounterEventHandlers[taskType], struct {
t reflect.Type
h func(taskType string, trigger any, count int64)
}{reflect.TypeOf(handler).In(1), func(taskType string, trigger any, count int64) {
handler(taskType, trigger.(Trigger), count)
}})
}
// OnRefreshTaskCounterEvent 触发特定任务类型的刷新任务计数器事件
func OnRefreshTaskCounterEvent(taskType string, trigger any, count int64) {
if handlers, exist := refreshTaskCounterEventHandlers[taskType]; exist {
for _, handler := range handlers {
if !reflect.TypeOf(trigger).AssignableTo(handler.t) {
continue
}
handler.h(taskType, trigger, count)
}
}
}
// RegisterRefreshTaskConditionEvent 注册特定任务类型的刷新任务条件事件处理函数
func RegisterRefreshTaskConditionEvent[Trigger any](taskType string, handler RefreshTaskConditionEventHandler[Trigger]) {
if refreshTaskConditionEventHandlers == nil {
refreshTaskConditionEventHandlers = make(map[string][]struct {
t reflect.Type
h func(taskType string, trigger any, condition Condition)
})
}
refreshTaskConditionEventHandlers[taskType] = append(refreshTaskConditionEventHandlers[taskType], struct {
t reflect.Type
h func(taskType string, trigger any, condition Condition)
}{reflect.TypeOf(handler).In(1), func(taskType string, trigger any, condition Condition) {
handler(taskType, trigger.(Trigger), condition)
}})
}
// OnRefreshTaskConditionEvent 触发特定任务类型的刷新任务条件事件
func OnRefreshTaskConditionEvent(taskType string, trigger any, condition Condition) {
if handlers, exist := refreshTaskConditionEventHandlers[taskType]; exist {
for _, handler := range handlers {
if !reflect.TypeOf(trigger).AssignableTo(handler.t) {
continue
}
handler.h(taskType, trigger, condition)
}
}
}

82
game/task/options.go Normal file
View File

@ -0,0 +1,82 @@
package task
import (
"time"
)
// Option 任务选项
type Option func(task *Task)
// WithType 设置任务类型
func WithType(taskType string) Option {
return func(task *Task) {
task.Type = taskType
}
}
// WithCondition 设置任务完成条件,当满足条件时,任务状态为完成
// - 任务条件值需要变更时可通过 Task.AssignConditionValueAndRefresh 方法变更
// - 当多次设置该选项时,后面的设置会覆盖之前的设置
func WithCondition(condition Condition) Option {
return func(task *Task) {
if condition == nil {
return
}
if task.Cond == nil {
task.Cond = condition
return
}
for k, v := range condition {
task.Cond[k] = v
}
}
}
// WithCounter 设置任务计数器,当计数器达到要求时,任务状态为完成
// - 一些场景下,任务计数器可能会溢出,此时可通过 WithOverflowCounter 设置可溢出的任务计数器
// - 当多次设置该选项时,后面的设置会覆盖之前的设置
// - 如果需要初始化计数器的值,可通过 initCount 参数设置
func WithCounter(counter int64, initCount ...int64) Option {
return func(task *Task) {
task.Counter = counter
if len(initCount) > 0 {
task.CurrCount = initCount[0]
if task.CurrCount < 0 {
task.CurrCount = 0
} else if task.CurrCount > task.Counter {
task.CurrCount = task.Counter
}
}
}
}
// WithOverflowCounter 设置可溢出的任务计数器,当计数器达到要求时,任务状态为完成
// - 当多次设置该选项时,后面的设置会覆盖之前的设置
// - 如果需要初始化计数器的值,可通过 initCount 参数设置
func WithOverflowCounter(counter int64, initCount ...int64) Option {
return func(task *Task) {
task.Counter = counter
task.CurrOverflow = true
if len(initCount) > 0 {
task.CurrCount = initCount[0]
if task.CurrCount < 0 {
task.CurrCount = 0
}
}
}
}
// WithDeadline 设置任务截止时间,超过截至时间并且任务未完成时,任务状态为失败
func WithDeadline(deadline time.Time) Option {
return func(task *Task) {
task.Deadline = deadline
}
}
// WithLimitedDuration 设置任务限时,超过限时时间并且任务未完成时,任务状态为失败
func WithLimitedDuration(start time.Time, duration time.Duration) Option {
return func(task *Task) {
task.StartTime = start
task.LimitedDuration = duration
}
}

23
game/task/status.go Normal file
View File

@ -0,0 +1,23 @@
package task
const (
StatusAccept Status = iota + 1 // 已接受
StatusFailed // 已失败
StatusComplete // 已完成
StatusReward // 已领取奖励
)
var (
statusFormat = map[Status]string{
StatusAccept: "Accept",
StatusComplete: "Complete",
StatusReward: "Reward",
StatusFailed: "Failed",
}
)
type Status byte
func (slf Status) String() string {
return statusFormat[slf]
}

152
game/task/task.go Normal file
View File

@ -0,0 +1,152 @@
package task
import (
"time"
)
// NewTask 生成任务
func NewTask(options ...Option) *Task {
task := new(Task)
for _, option := range options {
option(task)
}
return task.refreshTaskStatus()
}
// Task 是对任务信息进行描述和处理的结构体
type Task struct {
Type string `json:"type,omitempty"` // 任务类型
Status Status `json:"status,omitempty"` // 任务状态
Cond Condition `json:"cond,omitempty"` // 任务条件
CondValue map[any]any `json:"cond_value,omitempty"` // 任务条件值
Counter int64 `json:"counter,omitempty"` // 任务要求计数器
CurrCount int64 `json:"curr_count,omitempty"` // 任务当前计数
CurrOverflow bool `json:"curr_overflow,omitempty"` // 任务当前计数是否允许溢出
Deadline time.Time `json:"deadline,omitempty"` // 任务截止时间
StartTime time.Time `json:"start_time,omitempty"` // 任务开始时间
LimitedDuration time.Duration `json:"limited_duration,omitempty"` // 任务限时
}
// IsComplete 判断任务是否已完成
func (slf *Task) IsComplete() bool {
return slf.Status == StatusComplete
}
// IsFailed 判断任务是否已失败
func (slf *Task) IsFailed() bool {
return slf.Status == StatusFailed
}
// IsReward 判断任务是否已领取奖励
func (slf *Task) IsReward() bool {
return slf.Status == StatusReward
}
// ReceiveReward 领取任务奖励,当任务状态为已完成时,才能领取奖励,此时返回 true并且任务状态变更为已领取奖励
func (slf *Task) ReceiveReward() bool {
if slf.Status != StatusComplete {
return false
}
slf.Status = StatusReward
return true
}
// IncrementCounter 增加计数器的值,当 incr 为负数时,计数器的值不会发生变化
// - 如果需要溢出计数器,可通过 WithOverflowCounter 设置可溢出的任务计数器
func (slf *Task) IncrementCounter(incr int64) *Task {
if incr < 0 {
return slf
}
slf.CurrCount += incr
if !slf.CurrOverflow && slf.CurrCount > slf.Counter {
slf.CurrCount = slf.Counter
}
return slf.refreshTaskStatus()
}
// DecrementCounter 减少计数器的值,当 decr 为负数时,计数器的值不会发生变化
func (slf *Task) DecrementCounter(decr int64) *Task {
if decr < 0 {
return slf
}
slf.CurrCount -= decr
if slf.CurrCount < 0 {
slf.CurrCount = 0
}
return slf.refreshTaskStatus()
}
// AssignConditionValueAndRefresh 分配条件值并刷新任务状态
func (slf *Task) AssignConditionValueAndRefresh(key, value any) *Task {
if slf.Cond == nil {
return slf
}
if _, exist := slf.Cond[key]; !exist {
return slf
}
if slf.CondValue == nil {
slf.CondValue = make(map[any]any)
}
slf.CondValue[key] = value
return slf.refreshTaskStatus()
}
// AssignConditionValueAndRefreshByCondition 分配条件值并刷新任务状态
func (slf *Task) AssignConditionValueAndRefreshByCondition(condition Condition) *Task {
if slf.Cond == nil {
return slf
}
if slf.CondValue == nil {
slf.CondValue = make(map[any]any)
}
for k, v := range condition {
if _, exist := slf.Cond[k]; !exist {
continue
}
slf.CondValue[k] = v
}
return slf.refreshTaskStatus()
}
// ResetStatus 重置任务状态
// - 该函数会将任务状态重置为已接受状态后,再刷新任务状态
// - 当任务条件变更,例如任务计数要求为 10已经完成的情况下将任务计数要求变更为 5 或 20此时任务状态由于是已完成或已领取状态不会自动刷新需要调用该函数刷新任务状态
func (slf *Task) ResetStatus() *Task {
slf.Status = StatusAccept
return slf.refreshTaskStatus()
}
// refreshTaskStatus 刷新任务状态
func (slf *Task) refreshTaskStatus() *Task {
curr := time.Now()
if (!slf.StartTime.IsZero() && curr.Before(slf.StartTime)) || (!slf.Deadline.IsZero() && curr.After(slf.Deadline)) || slf.Status >= StatusComplete {
return slf
}
slf.Status = StatusComplete
if slf.Counter > 0 && slf.CurrCount < slf.Counter {
slf.Status = StatusAccept
return slf
}
if slf.Cond != nil {
for k, v := range slf.Cond {
if v != slf.CondValue[k] {
slf.Status = StatusAccept
return slf
}
}
}
if !slf.Deadline.IsZero() && slf.Status == StatusAccept {
if slf.Deadline.After(curr) {
slf.Status = StatusFailed
return slf
}
}
if slf.LimitedDuration > 0 && slf.Status == StatusAccept {
if curr.Sub(slf.StartTime) > slf.LimitedDuration {
slf.Status = StatusFailed
return slf
}
}
return slf
}

48
game/task/task_test.go Normal file
View File

@ -0,0 +1,48 @@
package task
import (
"fmt"
"testing"
)
type Player struct {
tasks map[string][]*Task
}
type Monster struct {
}
func TestCond(t *testing.T) {
task := NewTask(WithType("T"), WithCounter(5), WithCondition(Cond("N", 5).Cond("M", 10)))
task.AssignConditionValueAndRefresh("N", 5)
task.AssignConditionValueAndRefresh("M", 10)
RegisterRefreshTaskCounterEvent[*Player](task.Type, func(taskType string, trigger *Player, count int64) {
fmt.Println("Player", count)
for _, t := range trigger.tasks[taskType] {
fmt.Println(t.CurrCount, t.IncrementCounter(count).Status)
}
})
RegisterRefreshTaskConditionEvent[*Player](task.Type, func(taskType string, trigger *Player, condition Condition) {
fmt.Println("Player", condition)
for _, t := range trigger.tasks[taskType] {
fmt.Println(t.CurrCount, t.AssignConditionValueAndRefresh("N", 5).Status)
}
})
RegisterRefreshTaskCounterEvent[*Monster](task.Type, func(taskType string, trigger *Monster, count int64) {
fmt.Println("Monster", count)
})
player := &Player{
tasks: map[string][]*Task{
task.Type: {task},
},
}
OnRefreshTaskCounterEvent(task.Type, player, 1)
OnRefreshTaskCounterEvent(task.Type, player, 2)
OnRefreshTaskCounterEvent(task.Type, player, 3)
OnRefreshTaskCounterEvent(task.Type, new(Monster), 3)
}

81
go.mod Normal file
View File

@ -0,0 +1,81 @@
module github.com/kercylan98/minotaur
go 1.22.0
require (
github.com/RussellLuo/timingwheel v0.0.0-20220218152713-54845bda3108
github.com/alphadose/haxmap v1.3.1
github.com/gin-contrib/pprof v1.4.0
github.com/gin-gonic/gin v1.9.1
github.com/go-resty/resty/v2 v2.11.0
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75
github.com/gorilla/websocket v1.5.1
github.com/json-iterator/go v1.1.12
github.com/panjf2000/ants/v2 v2.9.0
github.com/panjf2000/gnet v1.6.7
github.com/panjf2000/gnet/v2 v2.3.6
github.com/pkg/errors v0.9.1
github.com/smartystreets/goconvey v1.8.1
github.com/sony/sonyflake v1.2.0
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
github.com/tealeg/xlsx v1.0.5
github.com/tidwall/gjson v1.17.0
github.com/xtaci/kcp-go/v5 v5.6.7
go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.18.0
google.golang.org/grpc v1.60.1
)
require (
github.com/bytedance/gopkg v0.0.0-20240315062850-21fc7a1671a8 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.3.2 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/reedsolomon v1.12.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/smarty/assertions v1.15.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/templexxx/cpu v0.1.0 // indirect
github.com/templexxx/xorsimd v0.4.2 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

353
go.sum Normal file
View File

@ -0,0 +1,353 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/RussellLuo/timingwheel v0.0.0-20220218152713-54845bda3108 h1:iPugyBI7oFtbDZXC4dnY093M1kZx6k/95sen92gafbY=
github.com/RussellLuo/timingwheel v0.0.0-20220218152713-54845bda3108/go.mod h1:WAMLHwunr1hi3u7OjGV6/VWG9QbdMhGpEKjROiSFd10=
github.com/alphadose/haxmap v1.3.1 h1:KmZh75duO1tC8pt3LmUwoTYiZ9sh4K52FX8p7/yrlqU=
github.com/alphadose/haxmap v1.3.1/go.mod h1:rjHw1IAqbxm0S3U5tD16GoKsiAd8FWx5BJ2IYqXwgmM=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bytedance/gopkg v0.0.0-20240315062850-21fc7a1671a8 h1:8LX2T6XzOOPvVMS8RH0sY4+QFmO5XyFUnrmwVbtD13k=
github.com/bytedance/gopkg v0.0.0-20240315062850-21fc7a1671a8/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/pprof v1.4.0 h1:XxiBSf5jWZ5i16lNOPbMTVdgHBdhfGRD5PZ1LWazzvg=
github.com/gin-contrib/pprof v1.4.0/go.mod h1:RrehPJasUVBPK6yTUwOl8/NP6i0vbUgmxtis+Z5KE90=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8=
github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY=
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/panjf2000/ants/v2 v2.4.7/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A=
github.com/panjf2000/ants/v2 v2.9.0 h1:SztCLkVxBRigbg+vt0S5QvF5vxAbxbKt09/YfAJ0tEo=
github.com/panjf2000/ants/v2 v2.9.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I=
github.com/panjf2000/gnet v1.6.7 h1:zv1k6kw80sG5ZQrLpbbFDheNCm50zm3z2e3ck5GwMOM=
github.com/panjf2000/gnet v1.6.7/go.mod h1:KcOU7QsCaCBjeD5kyshBIamG3d9kAQtlob4Y0v0E+sc=
github.com/panjf2000/gnet/v2 v2.3.6 h1:BUHjMPJaNO8N5rQZmhKce9/Iu2ryeMjhKPEOi+ecisQ=
github.com/panjf2000/gnet/v2 v2.3.6/go.mod h1:R+X5M5YBpOGMVP/92OJ02P35SbmoHjiL7GnaBhht6GE=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/sony/sonyflake v1.2.0 h1:Pfr3A+ejSg+0SPqpoAmQgEtNDAhc2G1SUYk205qVMLQ=
github.com/sony/sonyflake v1.2.0/go.mod h1:LORtCywH/cq10ZbyfhKrHYgAUGH7mOBa76enV9txy/Y=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tealeg/xlsx v1.0.5 h1:+f8oFmvY8Gw1iUXzPk+kz+4GpbDZPK1FhPiQRd+ypgE=
github.com/tealeg/xlsx v1.0.5/go.mod h1:btRS8dz54TDnvKNosuAqxrM1QgN1udgk9O34bDCnORM=
github.com/templexxx/cpu v0.1.0 h1:wVM+WIJP2nYaxVxqgHPD4wGA2aJ9rvrQRV8CvFzNb40=
github.com/templexxx/cpu v0.1.0/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUoVI=
github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/xtaci/kcp-go/v5 v5.6.7 h1:7+rnxNFIsjEwTXQk4cSZpXM4pO0hqtpwE1UFFoJBffA=
github.com/xtaci/kcp-go/v5 v5.6.7/go.mod h1:oE9j2NVqAkuKO5o8ByKGch3vgVX3BNf8zqP8JiGq0bM=
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211204120058-94396e421777/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

3
local-doc.bat Normal file
View File

@ -0,0 +1,3 @@
@echo off
echo Open: http://localhost:9998/pkg/github.com/kercylan98/minotaur/
godoc -http=:9998 -play

3
local-doc.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/bash
echo Open: http://localhost:9998/pkg/github.com/kercylan98/minotaur/
godoc -http=:9998 -play

73
modular/README.md Normal file
View File

@ -0,0 +1,73 @@
# Modular
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 包级函数定义
|函数名称|描述
|:--|:--
|[Run](#Run)|运行模块化应用程序
|[RegisterServices](#RegisterServices)|注册服务
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`INTERFACE`|[Block](#struct_Block)|标识模块化服务为阻塞进程的服务,当实现了 Service 且实现了 Block 接口时,模块化应用程序会在 Service.OnMount 阶段完成后执行 OnBlock 函数
|`INTERFACE`|[Service](#struct_Service)|模块化服务接口,所有的服务均需要实现该接口,在服务的生命周期内发生任何错误均应通过 panic 阻止服务继续运行
</details>
***
## 详情信息
#### func Run()
<span id="Run"></span>
> 运行模块化应用程序
***
#### func RegisterServices(s ...Service)
<span id="RegisterServices"></span>
> 注册服务
***
<span id="struct_Block"></span>
### Block `INTERFACE`
标识模块化服务为阻塞进程的服务,当实现了 Service 且实现了 Block 接口时,模块化应用程序会在 Service.OnMount 阶段完成后执行 OnBlock 函数
该接口适用于 Http 服务、WebSocket 服务等需要阻塞进程的服务。需要注意的是, OnBlock 的执行不能保证按照 Service 的注册顺序执行
```go
type Block interface {
Service
OnBlock()
}
```
<span id="struct_Service"></span>
### Service `INTERFACE`
模块化服务接口,所有的服务均需要实现该接口,在服务的生命周期内发生任何错误均应通过 panic 阻止服务继续运行
- 生命周期示例: OnInit -> OnPreload -> OnMount
在 Golang 中,包与包之间互相引用会导致循环依赖,因此在模块化应用程序中,所有的服务均不应该直接引用其他服务。
服务应该在 OnInit 阶段将不依赖其他服务的内容初始化完成,并且如果服务需要暴露给其他服务调用,那么也应该在 OnInit 阶段完成对外暴露。
- 暴露方式可参考 modular/example
在 OnPreload 阶段,服务应该完成对其依赖服务的依赖注入,最终在 OnMount 阶段完成对服务功能的定义、路由的声明等。
```go
type Service interface {
OnInit()
OnPreload()
OnMount()
}
```

10
modular/block.go Normal file
View File

@ -0,0 +1,10 @@
package modular
// Block 标识模块化服务为阻塞进程的服务,当实现了 Service 且实现了 Block 接口时,模块化应用程序会在 Service.OnMount 阶段完成后执行 OnBlock 函数
//
// 该接口适用于 Http 服务、WebSocket 服务等需要阻塞进程的服务。需要注意的是, OnBlock 的执行不能保证按照 Service 的注册顺序执行
type Block interface {
Service
// OnBlock 阻塞进程
OnBlock()
}

40
modular/dimension.go Normal file
View File

@ -0,0 +1,40 @@
package modular
// Dimension 维度接口
// - 维度与服务的区别在于,维度是对非全局性的服务进行抽象,例如:依赖特定游戏房间的局内玩家管理服务
type Dimension[Owner any] interface {
// OnInit 服务初始化阶段,该阶段不应该依赖其他任何服务
OnInit(owner Owner) error
// OnPreload 预加载阶段,在进入该阶段时,所有服务已经初始化完成,可在该阶段注入其他服务的依赖
OnPreload() error
// OnMount 挂载阶段,该阶段所有服务本身及依赖的服务都已经初始化完成,可在该阶段进行服务功能的定义
OnMount() error
}
// RunDimensions 运行维度
func RunDimensions[Owner any](owner Owner, dimensions ...Dimension[Owner]) error {
// OnInit
for _, dimension := range dimensions {
if err := dimension.OnInit(owner); err != nil {
return err
}
}
// OnPreload
for _, dimension := range dimensions {
if err := dimension.OnPreload(); err != nil {
return err
}
}
// OnMount
for _, dimension := range dimensions {
if err := dimension.OnMount(); err != nil {
return err
}
}
return nil
}

15
modular/example/README.md Normal file
View File

@ -0,0 +1,15 @@
# Main
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
</details>
***

View File

@ -0,0 +1,4 @@
package core
type Events struct {
}

View File

@ -0,0 +1,9 @@
package core
import "github.com/kercylan98/minotaur/modular/example/internal/dimension/dimensions/exposes"
type Room struct {
RoomId int64
*Events
exposes.VisitorsExpose
}

View File

@ -0,0 +1,20 @@
package dimension
import (
"github.com/kercylan98/minotaur/modular"
"github.com/kercylan98/minotaur/modular/example/internal/dimension/core"
"github.com/kercylan98/minotaur/modular/example/internal/dimension/dimensions/dimensions/visitors"
)
func New(roomId int64) error {
visitorsDimension := new(visitors.Dimension)
return modular.RunDimensions(&core.Room{
RoomId: roomId,
Events: &core.Events{},
VisitorsExpose: visitorsDimension,
},
visitorsDimension,
)
}

View File

@ -0,0 +1,64 @@
package visitors
import (
"fmt"
"github.com/kercylan98/minotaur/modular/example/internal/dimension/core"
"github.com/kercylan98/minotaur/modular/example/internal/dimension/dimensions/exposes"
"github.com/kercylan98/minotaur/modular/example/internal/dimension/dimensions/models"
"github.com/kercylan98/minotaur/utils/collection"
)
type Dimension struct {
*core.Room // 房间 Id
visitors map[string]*models.VisitorsMember // 所有访客
visitorIds []string // 所有访客 OpenId
}
func (d *Dimension) OnInit(owner *core.Room) error {
exposes.Visitors = d
d.Room = owner
d.visitors = make(map[string]*models.VisitorsMember)
fmt.Println("visitors dimension initialized")
return nil
}
func (d *Dimension) OnPreload() error {
fmt.Println("visitors dimension preloaded")
return nil
}
func (d *Dimension) OnMount() error {
fmt.Println("visitors dimension mounted")
return nil
}
func (d *Dimension) Count() int {
return len(d.visitors)
}
func (d *Dimension) OpenIds() []string {
return d.visitorIds
}
func (d *Dimension) Has(openId string) bool {
return collection.KeyInMap(d.visitors, openId)
}
func (d *Dimension) Del(openId string) {
member := d.Get(openId)
if member == nil {
return
}
delete(d.visitors, openId)
collection.DropSliceByIndices(&d.visitorIds, member.OpenIdIdx)
}
func (d *Dimension) Get(openId string) *models.VisitorsMember {
return d.visitors[openId]
}
func (d *Dimension) Add(member *models.VisitorsMember) {
member.OpenIdIdx = len(d.visitorIds)
d.visitorIds = append(d.visitorIds, member.OpenId)
d.visitors[member.OpenId] = member
}

View File

@ -0,0 +1,27 @@
package exposes
import (
"github.com/kercylan98/minotaur/modular/example/internal/dimension/dimensions/models"
)
var Visitors VisitorsExpose
type VisitorsExpose interface {
// Count 访客数量
Count() int
// OpenIds 访客 OpenId 列表
OpenIds() []string
// Has 是否存在指定 OpenId 的访客
Has(openId string) bool
// Del 删除指定 OpenId 的访客
Del(openId string)
// Get 获取指定 OpenId 的访客
Get(openId string) *models.VisitorsMember
// Add 添加访客
Add(member *models.VisitorsMember)
}

View File

@ -0,0 +1,7 @@
package models
// VisitorsMember 访客成员
type VisitorsMember struct {
OpenId string // 访客成员 OpenId
OpenIdIdx int // 访客成员 OpenId 索引
}

View File

@ -0,0 +1,42 @@
# Expose
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`INTERFACE`|[Attack](#struct_Attack)|暂无描述...
|`INTERFACE`|[Login](#struct_Login)|暂无描述...
</details>
***
## 详情信息
<span id="struct_Attack"></span>
### Attack `INTERFACE`
```go
type Attack interface {
Name() string
}
```
<span id="struct_Login"></span>
### Login `INTERFACE`
```go
type Login interface {
Name() string
}
```

View File

@ -0,0 +1,7 @@
package expose
var AttackExpose Attack
type Attack interface {
Name() string
}

View File

@ -0,0 +1,7 @@
package expose
var LoginExpose Login
type Login interface {
Name() string
}

View File

@ -0,0 +1,54 @@
# Attack
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`STRUCT`|[Service](#struct_Service)|暂无描述...
</details>
***
## 详情信息
<span id="struct_Service"></span>
### Service `STRUCT`
```go
type Service struct {
Login expose.Login
name string
}
```
<span id="struct_Service_OnInit"></span>
#### func (*Service) OnInit()
***
<span id="struct_Service_OnPreload"></span>
#### func (*Service) OnPreload()
***
<span id="struct_Service_OnMount"></span>
#### func (*Service) OnMount()
***
<span id="struct_Service_Name"></span>
#### func (*Service) Name() string
***

View File

@ -0,0 +1,28 @@
package attack
import (
"fmt"
"github.com/kercylan98/minotaur/modular/example/internal/service/expose"
)
type Service struct {
Login expose.Login
name string
}
func (a *Service) OnInit() {
expose.AttackExpose = a
a.name = "attack"
}
func (a *Service) OnPreload() {
a.Login = expose.LoginExpose
}
func (a *Service) OnMount() {
fmt.Println("attack service mounted, call", a.Login.Name(), "service")
}
func (a *Service) Name() string {
return a.name
}

View File

@ -0,0 +1,54 @@
# Login
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`STRUCT`|[Service](#struct_Service)|暂无描述...
</details>
***
## 详情信息
<span id="struct_Service"></span>
### Service `STRUCT`
```go
type Service struct {
Attack expose.Attack
name string
}
```
<span id="struct_Service_OnInit"></span>
#### func (*Service) OnInit()
***
<span id="struct_Service_OnPreload"></span>
#### func (*Service) OnPreload()
***
<span id="struct_Service_OnMount"></span>
#### func (*Service) OnMount()
***
<span id="struct_Service_Name"></span>
#### func (*Service) Name() string
***

View File

@ -0,0 +1,28 @@
package login
import (
"fmt"
"github.com/kercylan98/minotaur/modular/example/internal/service/expose"
)
type Service struct {
Attack expose.Attack
name string
}
func (l *Service) OnInit() {
expose.LoginExpose = l
l.name = "login"
}
func (l *Service) OnPreload() {
l.Attack = expose.AttackExpose
}
func (l *Service) OnMount() {
fmt.Println("attack service mounted, call", l.Attack.Name(), "service")
}
func (l *Service) Name() string {
return l.name
}

View File

@ -0,0 +1,53 @@
# Server
[![Go doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/kercylan98/minotaur)
![](https://img.shields.io/badge/Email-kercylan@gmail.com-green.svg?style=flat)
暂无介绍...
## 目录导航
列出了该 `package` 下所有的函数及类型定义,可通过目录导航进行快捷跳转 ❤️
<details>
<summary>展开 / 折叠目录导航</summary>
> 类型定义
|类型|名称|描述
|:--|:--|:--
|`STRUCT`|[Service](#struct_Service)|暂无描述...
</details>
***
## 详情信息
<span id="struct_Service"></span>
### Service `STRUCT`
```go
type Service struct {
srv *server.Server
}
```
<span id="struct_Service_OnInit"></span>
#### func (*Service) OnInit()
***
<span id="struct_Service_OnPreload"></span>
#### func (*Service) OnPreload()
***
<span id="struct_Service_OnMount"></span>
#### func (*Service) OnMount()
***
<span id="struct_Service_OnBlock"></span>
#### func (*Service) OnBlock()
***

View File

@ -0,0 +1,28 @@
package server
import (
"github.com/kercylan98/minotaur/server"
"time"
)
type Service struct {
srv *server.Server
}
func (s *Service) OnInit() {
s.srv = server.New(server.NetworkNone, server.WithLimitLife(time.Second*3))
}
func (s *Service) OnPreload() {
}
func (s *Service) OnMount() {
}
func (s *Service) OnBlock() {
if err := s.srv.RunNone(); err != nil {
panic(err)
}
}

24
modular/example/main.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"github.com/kercylan98/minotaur/modular"
"github.com/kercylan98/minotaur/modular/example/internal/dimension"
"github.com/kercylan98/minotaur/modular/example/internal/service/services/attack"
"github.com/kercylan98/minotaur/modular/example/internal/service/services/login"
"github.com/kercylan98/minotaur/modular/example/internal/service/services/server"
)
func main() {
modular.RegisterServices(
new(attack.Service),
new(server.Service),
new(login.Service),
)
err := dimension.New(1) // generate a room
if err != nil {
panic(err)
}
modular.Run()
}

Some files were not shown because too many files have changed in this diff Show More