DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,4 @@
|
||||
// Package charstat 从 DNF PVF 角色文件加载职业基础属性和成长属性。
|
||||
//
|
||||
// 本包只提供只读静态表和计算结果,不读取玩家数据库,也不写入运行时状态。
|
||||
package charstat
|
||||
@@ -0,0 +1,323 @@
|
||||
package charstat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
)
|
||||
|
||||
var (
|
||||
tagLineRE = regexp.MustCompile(`(?is)\[\s*%s\s*\]\s*([-\d.]+)`)
|
||||
growtypeTagRE = regexp.MustCompile(`(?i)\[\s*growtype\s+([1-6])\s*\]`)
|
||||
awakeningTagRE = regexp.MustCompile(`(?i)\[\s*awakening\s+([1-2])\s*\]`)
|
||||
initialValueTag = regexp.MustCompile(`(?i)\[\s*initial\s+value\s*\]`)
|
||||
)
|
||||
|
||||
// Load 从 PVF 文本来源加载 character.lst 和每个职业 chr 文件。
|
||||
func Load(ctx context.Context, source Source, options Options) (*Table, error) {
|
||||
if source == nil {
|
||||
return nil, ErrSourceRequired
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
listPath := cleanPath(options.ListPath)
|
||||
if listPath == "" {
|
||||
listPath = DefaultList
|
||||
}
|
||||
listText, err := source.ReadText(listPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
doc, err := dnfpvf.Parse(listPath, listText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries := dnfpvf.ParseList(doc)
|
||||
if len(entries) == 0 {
|
||||
return nil, fmt.Errorf("%w: %s", ErrListEmpty, listPath)
|
||||
}
|
||||
table := &Table{jobs: make(map[byte]jobTables)}
|
||||
for _, entry := range entries {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry.ID < 0 || entry.ID > 0xff || strings.TrimSpace(entry.Path) == "" {
|
||||
continue
|
||||
}
|
||||
charPath := resolvePath(path.Dir(listPath), entry.Path)
|
||||
text, actualPath, err := readTextAny(source, charPath, entry.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read character stat job %d: %w", entry.ID, err)
|
||||
}
|
||||
tables, err := parseJobTables(actualPath, text)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse character stat job %d %s: %w", entry.ID, actualPath, err)
|
||||
}
|
||||
table.jobs[byte(entry.ID)] = tables
|
||||
}
|
||||
if len(table.jobs) == 0 {
|
||||
return nil, fmt.Errorf("%w: %s", ErrListEmpty, listPath)
|
||||
}
|
||||
return table, nil
|
||||
}
|
||||
|
||||
// Compute 计算 job/growType/level 对应的职业基础属性。
|
||||
func (t *Table) Compute(job byte, growType byte, level int) (Vector, error) {
|
||||
if t == nil {
|
||||
return Vector{}, ErrSourceRequired
|
||||
}
|
||||
tables, ok := t.jobs[job]
|
||||
if !ok {
|
||||
return Vector{}, fmt.Errorf("%w: %d", ErrJobMissing, job)
|
||||
}
|
||||
first, second := DecodeGrowType(growType)
|
||||
if first < 0 || first > 5 || second < 0 || second > 2 {
|
||||
return Vector{}, fmt.Errorf("%w: job=%d grow=%d", ErrGrowthMissing, job, growType)
|
||||
}
|
||||
if level < 1 {
|
||||
level = 1
|
||||
}
|
||||
out := tables.base
|
||||
for current := 1; current < level; current++ {
|
||||
switch {
|
||||
case current <= 14:
|
||||
growth, ok := tables.growth(job, 0, 0)
|
||||
if !ok {
|
||||
return Vector{}, fmt.Errorf("%w: job=%d growtype=1", ErrGrowthMissing, job)
|
||||
}
|
||||
out.add(growth)
|
||||
case current <= 49:
|
||||
growth, ok := tables.growth(job, first, 0)
|
||||
if !ok {
|
||||
return Vector{}, fmt.Errorf("%w: job=%d growtype=%d", ErrGrowthMissing, job, first+1)
|
||||
}
|
||||
out.add(growth)
|
||||
default:
|
||||
growth, ok := tables.growth(job, first, second)
|
||||
if !ok {
|
||||
return Vector{}, fmt.Errorf("%w: job=%d growtype=%d awakening=%d", ErrGrowthMissing, job, first+1, second)
|
||||
}
|
||||
out.add(growth)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (t jobTables) growth(_ byte, first int, second int) (Vector, bool) {
|
||||
growIndex := first + 1
|
||||
if growIndex < 1 || growIndex >= len(t.growtype) {
|
||||
return Vector{}, false
|
||||
}
|
||||
if second == 0 {
|
||||
return t.growtype[growIndex], t.hasGrow[growIndex]
|
||||
}
|
||||
if second < 0 || second >= len(t.awakening[growIndex]) {
|
||||
return Vector{}, false
|
||||
}
|
||||
return t.awakening[growIndex][second], t.hasAwake[growIndex][second]
|
||||
}
|
||||
|
||||
func parseJobTables(charPath string, text string) (jobTables, error) {
|
||||
initial := initialValueTag.FindStringIndex(text)
|
||||
if initial == nil {
|
||||
return jobTables{}, fmt.Errorf("missing [initial value]")
|
||||
}
|
||||
growMatches := growtypeTagRE.FindAllStringSubmatchIndex(text, -1)
|
||||
firstGrow := len(text)
|
||||
for _, match := range growMatches {
|
||||
if match[0] > initial[0] && match[0] < firstGrow {
|
||||
firstGrow = match[0]
|
||||
}
|
||||
}
|
||||
tables := jobTables{
|
||||
path: charPath,
|
||||
base: parseVector(text[initial[0]:firstGrow]),
|
||||
}
|
||||
growStarts := make([]struct {
|
||||
no int
|
||||
start int
|
||||
end int
|
||||
}, 0, len(growMatches))
|
||||
for _, match := range growMatches {
|
||||
no, err := strconv.Atoi(text[match[2]:match[3]])
|
||||
if err != nil || no < 1 || no > 6 {
|
||||
continue
|
||||
}
|
||||
growStarts = append(growStarts, struct {
|
||||
no int
|
||||
start int
|
||||
end int
|
||||
}{no: no, start: match[0]})
|
||||
}
|
||||
sort.Slice(growStarts, func(i, j int) bool { return growStarts[i].start < growStarts[j].start })
|
||||
for idx := range growStarts {
|
||||
end := len(text)
|
||||
if idx+1 < len(growStarts) {
|
||||
end = growStarts[idx+1].start
|
||||
}
|
||||
growStarts[idx].end = end
|
||||
block := text[growStarts[idx].start:end]
|
||||
parseGrowthBlock(block, growStarts[idx].no, &tables)
|
||||
}
|
||||
if !tables.hasGrow[1] {
|
||||
return jobTables{}, fmt.Errorf("missing [growtype 1]")
|
||||
}
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func parseGrowthBlock(block string, growNo int, tables *jobTables) {
|
||||
awakeMatches := awakeningTagRE.FindAllStringSubmatchIndex(block, -1)
|
||||
growEnd := len(block)
|
||||
if len(awakeMatches) > 0 {
|
||||
growEnd = awakeMatches[0][0]
|
||||
}
|
||||
tables.growtype[growNo] = parseVector(block[:growEnd])
|
||||
tables.hasGrow[growNo] = true
|
||||
for idx, match := range awakeMatches {
|
||||
no, err := strconv.Atoi(block[match[2]:match[3]])
|
||||
if err != nil || no < 1 || no > 2 {
|
||||
continue
|
||||
}
|
||||
end := len(block)
|
||||
if idx+1 < len(awakeMatches) {
|
||||
end = awakeMatches[idx+1][0]
|
||||
}
|
||||
tables.awakening[growNo][no] = parseVector(block[match[0]:end])
|
||||
tables.hasAwake[growNo][no] = true
|
||||
}
|
||||
}
|
||||
|
||||
func parseVector(section string) Vector {
|
||||
physicalAttack := statRaw(section, "physical attack")
|
||||
physicalDefense := statRaw(section, "physical defense")
|
||||
magicalAttack := statRaw(section, "magical attack")
|
||||
magicalDefense := statRaw(section, "magical defense")
|
||||
strength := statRaw(section, "strength")
|
||||
intelligence := statRaw(section, "intelligence")
|
||||
vitality := statRaw(section, "vitality")
|
||||
spirit := statRaw(section, "spirit")
|
||||
// 当前 23.4.15.0 PVF 的 etc/avatarabilitystringtable.etc 把
|
||||
// PHYSICAL_ATTACK/PHYSICAL_DEFENSE/MAGICAL_ATTACK/MAGICAL_DEFENSE 显示为
|
||||
// 力量/体力/智力/精神;部分 .chr 只写旧标签,所以这里按客户端能力表补齐四维。
|
||||
if strength == 0 {
|
||||
strength = physicalAttack
|
||||
}
|
||||
if vitality == 0 {
|
||||
vitality = physicalDefense
|
||||
}
|
||||
if intelligence == 0 {
|
||||
intelligence = magicalAttack
|
||||
}
|
||||
if spirit == 0 {
|
||||
spirit = magicalDefense
|
||||
}
|
||||
return Vector{
|
||||
HPMax: statScaled10(section, "HP MAX"),
|
||||
MPMax: statScaled10(section, "MP MAX"),
|
||||
Strength: strength,
|
||||
Intelligence: intelligence,
|
||||
Vitality: vitality,
|
||||
Spirit: spirit,
|
||||
PhysicalAttack: physicalAttack,
|
||||
PhysicalDefense: physicalDefense,
|
||||
MagicalAttack: magicalAttack,
|
||||
MagicalDefense: magicalDefense,
|
||||
IndependentAttack: statRaw(section, "independent attack"),
|
||||
FireResistance: statRaw(section, "fire resistance"),
|
||||
WaterResistance: statRaw(section, "water resistance"),
|
||||
DarkResistance: statRaw(section, "dark resistance"),
|
||||
LightResistance: statRaw(section, "light resistance"),
|
||||
InventoryLimit: statScaled10(section, "inventory limit"),
|
||||
HPRegenSpeed: statScaled10(section, "HP regen speed"),
|
||||
MPRegenSpeed: statScaled10(section, "MP regen speed"),
|
||||
MoveSpeed: statScaled10(section, "move speed"),
|
||||
AttackSpeed: statScaled10(section, "attack speed"),
|
||||
CastSpeed: statScaled10(section, "cast speed"),
|
||||
HitRecovery: statScaled10(section, "hit recovery"),
|
||||
JumpPower: statScaled10(section, "jump power"),
|
||||
Weight: statScaled10(section, "weight"),
|
||||
}
|
||||
}
|
||||
|
||||
func statRaw(section string, key string) int64 {
|
||||
return statNumber(section, key, 1)
|
||||
}
|
||||
|
||||
func statScaled10(section string, key string) int64 {
|
||||
return statNumber(section, key, 10)
|
||||
}
|
||||
|
||||
func statNumber(section string, key string, scale float32) int64 {
|
||||
pattern := fmt.Sprintf(tagLineRE.String(), regexp.QuoteMeta(key))
|
||||
matches := regexp.MustCompile(pattern).FindStringSubmatch(section)
|
||||
if len(matches) < 2 {
|
||||
return 0
|
||||
}
|
||||
value, err := strconv.ParseFloat(matches[1], 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return int64(float32(value) * scale)
|
||||
}
|
||||
|
||||
func readTextAny(source Source, candidates ...string) (string, string, error) {
|
||||
seen := make(map[string]struct{}, len(candidates))
|
||||
var lastErr error
|
||||
for _, candidate := range candidates {
|
||||
clean := cleanPath(candidate)
|
||||
if clean == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(clean)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
text, err := source.ReadText(clean)
|
||||
if err == nil {
|
||||
return text, clean, nil
|
||||
}
|
||||
lastErr = err
|
||||
}
|
||||
if lastErr != nil {
|
||||
return "", "", lastErr
|
||||
}
|
||||
return "", "", fmt.Errorf("pvf path candidates are empty")
|
||||
}
|
||||
|
||||
func resolvePath(base string, ref string) string {
|
||||
base = cleanPath(base)
|
||||
ref = cleanPath(ref)
|
||||
if base == "" || ref == "" {
|
||||
return ref
|
||||
}
|
||||
lowerRef := strings.ToLower(ref)
|
||||
lowerBase := strings.ToLower(base)
|
||||
if lowerRef == lowerBase || strings.HasPrefix(lowerRef, lowerBase+"/") {
|
||||
return ref
|
||||
}
|
||||
return cleanPath(path.Join(base, ref))
|
||||
}
|
||||
|
||||
func cleanPath(value string) string {
|
||||
value = strings.TrimSpace(strings.ReplaceAll(value, "\\", "/"))
|
||||
for strings.HasPrefix(value, "./") || strings.HasPrefix(value, "/") {
|
||||
value = strings.TrimPrefix(value, "./")
|
||||
value = strings.TrimPrefix(value, "/")
|
||||
}
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
clean := strings.TrimSuffix(path.Clean(value), "/")
|
||||
if clean == "." {
|
||||
return ""
|
||||
}
|
||||
return clean
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package charstat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type memSource map[string]string
|
||||
|
||||
func (s memSource) ReadText(relativePath string) (string, error) {
|
||||
if text, ok := s[cleanPath(relativePath)]; ok {
|
||||
return text, nil
|
||||
}
|
||||
return "", errMissing(relativePath)
|
||||
}
|
||||
|
||||
type errMissing string
|
||||
|
||||
func (e errMissing) Error() string { return "missing pvf text: " + string(e) }
|
||||
|
||||
func TestLoadComputesJobGrowLevelFromPVF(t *testing.T) {
|
||||
source := memSource{
|
||||
"character/character.lst": "11 `female_swordman.chr`\n",
|
||||
"character/female_swordman.chr": `
|
||||
[initial value]
|
||||
[HP MAX] 100
|
||||
[MP MAX] 50
|
||||
[strength] 8
|
||||
[intelligence] 9
|
||||
[vitality] 7
|
||||
[spirit] 6
|
||||
[physical attack] 3
|
||||
[magical attack] 4
|
||||
[inventory limit] 48000
|
||||
[move speed] 850
|
||||
[growtype 1]
|
||||
[HP MAX] 10
|
||||
[MP MAX] 20
|
||||
[strength] 1
|
||||
[growtype 2]
|
||||
[HP MAX] 100
|
||||
[MP MAX] 200
|
||||
[strength] 10
|
||||
[awakening 1]
|
||||
[HP MAX] 1000
|
||||
[MP MAX] 2000
|
||||
[strength] 100
|
||||
`,
|
||||
}
|
||||
|
||||
table, err := Load(context.Background(), source, Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
got, err := table.Compute(11, 0x11, 51)
|
||||
if err != nil {
|
||||
t.Fatalf("Compute() error = %v", err)
|
||||
}
|
||||
|
||||
if got.HPMax != 1000+14*100+35*1000+10000 {
|
||||
t.Fatalf("HPMax = %d", got.HPMax)
|
||||
}
|
||||
if got.MPMax != 500+14*200+35*2000+20000 {
|
||||
t.Fatalf("MPMax = %d", got.MPMax)
|
||||
}
|
||||
if got.Strength != 8+14*1+35*10+100 {
|
||||
t.Fatalf("Strength = %d", got.Strength)
|
||||
}
|
||||
if got.InventoryLimit != 480000 {
|
||||
t.Fatalf("InventoryLimit = %d", got.InventoryLimit)
|
||||
}
|
||||
if got.MoveSpeed != 8500 {
|
||||
t.Fatalf("MoveSpeed = %d", got.MoveSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMapsLegacyAttackDefenseTagsToPrimaryStats(t *testing.T) {
|
||||
source := memSource{
|
||||
"character/character.lst": "11 `female_swordman.chr`\n",
|
||||
"character/female_swordman.chr": `
|
||||
[initial value]
|
||||
[HP MAX] 170
|
||||
[MP MAX] 150
|
||||
[physical attack] 7
|
||||
[physical defense] 6
|
||||
[magical attack] 5
|
||||
[magical defense] 4
|
||||
[growtype 1]
|
||||
[HP MAX] 1
|
||||
`,
|
||||
}
|
||||
table, err := Load(context.Background(), source, Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
got, err := table.Compute(11, 0, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Compute() error = %v", err)
|
||||
}
|
||||
if got.Strength != 7 || got.Vitality != 6 || got.Intelligence != 5 || got.Spirit != 4 {
|
||||
t.Fatalf("primary stats = str:%d vit:%d int:%d spr:%d", got.Strength, got.Vitality, got.Intelligence, got.Spirit)
|
||||
}
|
||||
if got.PhysicalAttack != 7 || got.PhysicalDefense != 6 || got.MagicalAttack != 5 || got.MagicalDefense != 4 {
|
||||
t.Fatalf("combat stats = pa:%d pd:%d ma:%d md:%d", got.PhysicalAttack, got.PhysicalDefense, got.MagicalAttack, got.MagicalDefense)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeReturnsErrorWhenGrowthMissing(t *testing.T) {
|
||||
source := memSource{
|
||||
"character/character.lst": "1 `fighter.chr`\n",
|
||||
"character/fighter.chr": "[initial value]\n[HP MAX] 1\n[growtype 1]\n[HP MAX] 1\n",
|
||||
}
|
||||
table, err := Load(context.Background(), source, Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
if _, err := table.Compute(1, 0x01, 16); err == nil {
|
||||
t.Fatalf("Compute() expected missing growth error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package charstat
|
||||
|
||||
import "errors"
|
||||
|
||||
const DefaultList = "character/character.lst"
|
||||
|
||||
var (
|
||||
ErrSourceRequired = errors.New("dnf character stat source is required")
|
||||
ErrListEmpty = errors.New("dnf character stat list is empty")
|
||||
ErrJobMissing = errors.New("dnf character stat job is missing")
|
||||
ErrGrowthMissing = errors.New("dnf character stat growth is missing")
|
||||
)
|
||||
|
||||
// Source 表示已经加载到内存的 PVF 文本来源。
|
||||
type Source interface {
|
||||
ReadText(relativePath string) (string, error)
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
ListPath string
|
||||
}
|
||||
|
||||
// Vector 是客户端 USERINFO 属性块使用的 10 倍整数属性。
|
||||
type Vector struct {
|
||||
HPMax int64
|
||||
MPMax int64
|
||||
Strength int64
|
||||
Intelligence int64
|
||||
Vitality int64
|
||||
Spirit int64
|
||||
PhysicalAttack int64
|
||||
PhysicalDefense int64
|
||||
MagicalAttack int64
|
||||
MagicalDefense int64
|
||||
IndependentAttack int64
|
||||
FireResistance int64
|
||||
WaterResistance int64
|
||||
DarkResistance int64
|
||||
LightResistance int64
|
||||
InventoryLimit int64
|
||||
HPRegenSpeed int64
|
||||
MPRegenSpeed int64
|
||||
MoveSpeed int64
|
||||
AttackSpeed int64
|
||||
CastSpeed int64
|
||||
HitRecovery int64
|
||||
JumpPower int64
|
||||
Weight int64
|
||||
}
|
||||
|
||||
func (v *Vector) add(other Vector) {
|
||||
v.HPMax += other.HPMax
|
||||
v.MPMax += other.MPMax
|
||||
v.Strength += other.Strength
|
||||
v.Intelligence += other.Intelligence
|
||||
v.Vitality += other.Vitality
|
||||
v.Spirit += other.Spirit
|
||||
v.PhysicalAttack += other.PhysicalAttack
|
||||
v.PhysicalDefense += other.PhysicalDefense
|
||||
v.MagicalAttack += other.MagicalAttack
|
||||
v.MagicalDefense += other.MagicalDefense
|
||||
v.IndependentAttack += other.IndependentAttack
|
||||
v.FireResistance += other.FireResistance
|
||||
v.WaterResistance += other.WaterResistance
|
||||
v.DarkResistance += other.DarkResistance
|
||||
v.LightResistance += other.LightResistance
|
||||
v.InventoryLimit += other.InventoryLimit
|
||||
v.HPRegenSpeed += other.HPRegenSpeed
|
||||
v.MPRegenSpeed += other.MPRegenSpeed
|
||||
v.MoveSpeed += other.MoveSpeed
|
||||
v.AttackSpeed += other.AttackSpeed
|
||||
v.CastSpeed += other.CastSpeed
|
||||
v.HitRecovery += other.HitRecovery
|
||||
v.JumpPower += other.JumpPower
|
||||
v.Weight += other.Weight
|
||||
}
|
||||
|
||||
// Add merges another stat vector into v. Callers outside this package use it
|
||||
// to aggregate PVF character growth with item-derived PVF stats.
|
||||
func (v *Vector) Add(other Vector) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
v.add(other)
|
||||
}
|
||||
|
||||
type jobTables struct {
|
||||
path string
|
||||
base Vector
|
||||
growtype [7]Vector
|
||||
hasGrow [7]bool
|
||||
awakening [7][3]Vector
|
||||
hasAwake [7][3]bool
|
||||
}
|
||||
|
||||
// Table 是按 job 编号索引的角色成长表。
|
||||
type Table struct {
|
||||
jobs map[byte]jobTables
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
Jobs int `json:"jobs"`
|
||||
}
|
||||
|
||||
func (t *Table) Snapshot() Snapshot {
|
||||
if t == nil {
|
||||
return Snapshot{}
|
||||
}
|
||||
return Snapshot{Jobs: len(t.jobs)}
|
||||
}
|
||||
|
||||
// DecodeGrowType 拆出客户端角色记录里的 grow_type 低 4 位转职和高 4 位觉醒。
|
||||
func DecodeGrowType(growType byte) (first int, second int) {
|
||||
return int(growType & 0x0f), int((growType >> 4) & 0x0f)
|
||||
}
|
||||
Reference in new issue
Block a user