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,333 @@
|
||||
package dnfrobot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfrobotdomain "longheng.io/server/internal/modules/dnf/robot"
|
||||
)
|
||||
|
||||
const (
|
||||
// AutomaticFleetTarget is deliberately below robot.MaxProfiles so operators
|
||||
// can still keep manual profiles beside the zero-touch fleet.
|
||||
AutomaticFleetTarget = 500
|
||||
// AutomaticFleetOnlineTarget keeps all 500 persistent identities available
|
||||
// while bounding native game sessions. The recovered robot supervisor used
|
||||
// a finite actor/pending pool; treating every profile as an independently
|
||||
// reconnecting actor can starve real character-selection transactions.
|
||||
AutomaticFleetOnlineTarget = 64
|
||||
// automaticFleetPendingLimit mirrors the recovered supervisor's bounded
|
||||
// actor backlog: at most eight batches may be waiting to log in or
|
||||
// reconnect. With the current two-session native handshake limit this keeps
|
||||
// repository pressure bounded at sixteen pending automatic runtimes.
|
||||
automaticFleetPendingLimit = 16
|
||||
automaticFleetRetry = 30 * time.Second
|
||||
automaticFleetGatewayRetry = 2 * time.Second
|
||||
automaticFleetGatewaySamples = 3
|
||||
automaticFleetGatewayGap = 250 * time.Millisecond
|
||||
automaticFleetPreferredChannelID = 19
|
||||
automaticFleetConfigVersion = 7
|
||||
automaticFleetProfilePrefix = "auto-generated-"
|
||||
)
|
||||
|
||||
const (
|
||||
automationActionMove = "move"
|
||||
automationActionChat = "chat"
|
||||
automationActionStore = "store"
|
||||
automationActionMegaphone = "megaphone"
|
||||
)
|
||||
|
||||
// AutomaticFleetProvider creates or reuses one native, repository-backed
|
||||
// character. Implementations must be idempotent for the same index.
|
||||
type AutomaticFleetProvider interface {
|
||||
EnsureAutomaticRobotProfile(ctx context.Context, index int) (dnfrobotdomain.Profile, error)
|
||||
}
|
||||
|
||||
// DefaultAutomaticFleetPolicy is the zero-operation policy installed only
|
||||
// when no prior automation policy exists. The intervals are intentionally
|
||||
// broad; per-profile personalities further stagger every action.
|
||||
func DefaultAutomaticFleetPolicy() dnfrobotdomain.AutomationPolicy {
|
||||
return dnfrobotdomain.AutomationPolicy{
|
||||
Enabled: true, TargetOnline: AutomaticFleetOnlineTarget,
|
||||
ScaleIntervalSeconds: 5, ScaleUpBatch: 2, ScaleDownBatch: 2,
|
||||
MoveIntervalMinSeconds: 12, MoveIntervalMaxSeconds: 45, MoveRadius: 120,
|
||||
ChatIntervalMinSeconds: 180, ChatIntervalMaxSeconds: 600, ChatMode: 3,
|
||||
ChatMessages: []string{
|
||||
"今天也要努力冒险", "有人一起刷图吗", "整理一下背包", "城镇里真热闹",
|
||||
"看看我的小店吧", "休息一会再出发", "祝大家都出好装备", "准备下一次冒险",
|
||||
},
|
||||
MegaphoneIntervalMinSeconds: 900, MegaphoneIntervalMaxSeconds: 1800,
|
||||
MegaphoneProfileModulo: 50,
|
||||
StoreIntervalMinSeconds: 90, StoreIntervalMaxSeconds: 300,
|
||||
StoreDurationSeconds: 120, StoreConcurrent: 2, StoreMaximumRows: 8,
|
||||
StoreUnitPrice: 10000, StoreTitle: "冒险家商店",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) runAutomaticFleet(ctx context.Context) {
|
||||
defer m.wg.Done()
|
||||
provider, ok := m.endpoint.(AutomaticFleetProvider)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if stability, available := m.endpoint.(AutomaticFleetGatewayStability); available {
|
||||
for {
|
||||
if err := stability.WaitRobotGameEndpointStable(ctx, automaticFleetPreferredChannelID, automaticFleetGatewaySamples, automaticFleetGatewayGap); err == nil {
|
||||
break
|
||||
} else if ctx.Err() != nil {
|
||||
return
|
||||
} else {
|
||||
m.logger.Warn("dnf automatic robot gateway is not stable", "channel", automaticFleetPreferredChannelID, "error", err)
|
||||
}
|
||||
timer := time.NewTimer(automaticFleetGatewayRetry)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
ensured := make(map[int]struct{}, AutomaticFleetTarget)
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
operation := m.BeginOperation("automatic_fleet", fmt.Sprintf("target=%d", AutomaticFleetTarget))
|
||||
succeeded, failed := 0, 0
|
||||
for index := 1; index <= AutomaticFleetTarget; index++ {
|
||||
if _, ok := ensured[index]; ok {
|
||||
succeeded++
|
||||
continue
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
m.CompleteOperation(operation.ID, fmt.Sprintf("ensured=%d pending=%d", succeeded, AutomaticFleetTarget-succeeded), ctx.Err())
|
||||
return
|
||||
}
|
||||
profile, err := provider.EnsureAutomaticRobotProfile(ctx, index)
|
||||
if err == nil {
|
||||
// Fleet profiles are scheduler-owned. Direct autostart would fan out
|
||||
// hundreds of character-selection transactions at process startup.
|
||||
profile.AutoStart = false
|
||||
err = m.installAutomaticProfile(ctx, profile)
|
||||
}
|
||||
if err != nil {
|
||||
failed++
|
||||
m.logger.Warn("dnf automatic robot ensure deferred", "index", index, "error", err)
|
||||
// Repository pressure is shared by every following profile. Stop at
|
||||
// the first failure instead of immediately starting hundreds of
|
||||
// transactions that are expected to fail for the same reason.
|
||||
break
|
||||
}
|
||||
ensured[index] = struct{}{}
|
||||
succeeded++
|
||||
provisionDelay := m.markAutomaticFleetProfileEnsured()
|
||||
if provisionDelay > 0 && index < AutomaticFleetTarget {
|
||||
pace := time.NewTimer(provisionDelay)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
pace.Stop()
|
||||
m.CompleteOperation(operation.ID, fmt.Sprintf("ensured=%d pending=%d", succeeded, AutomaticFleetTarget-succeeded), ctx.Err())
|
||||
return
|
||||
case <-pace.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
summary := fmt.Sprintf("ensured=%d pending=%d", succeeded, AutomaticFleetTarget-succeeded)
|
||||
if failed == 0 {
|
||||
m.CompleteOperation(operation.ID, summary, nil)
|
||||
return
|
||||
}
|
||||
m.CompleteOperation(operation.ID, summary, fmt.Errorf("%d automatic robot profiles are pending", failed))
|
||||
timer.Reset(automaticFleetRetry)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) markAutomaticFleetProfileEnsured() time.Duration {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if !m.automaticFleetReady {
|
||||
m.automaticFleetReady = true
|
||||
m.nextScale = time.Time{}
|
||||
}
|
||||
return m.fleetProvisionDelay
|
||||
}
|
||||
|
||||
func (m *Manager) installAutomaticProfile(ctx context.Context, profile dnfrobotdomain.Profile) error {
|
||||
m.mu.RLock()
|
||||
current, found := findProfile(m.config, profile.ID)
|
||||
if found && current == profile {
|
||||
m.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
for _, candidate := range m.config.Profiles {
|
||||
if candidate.CharacterID == profile.CharacterID && profileKey(candidate.ID) != profileKey(profile.ID) {
|
||||
m.mu.RUnlock()
|
||||
return fmt.Errorf("automatic character %d is already assigned to profile %q", profile.CharacterID, candidate.ID)
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
_, err := m.Upsert(ctx, profile)
|
||||
return err
|
||||
}
|
||||
|
||||
func automationBehaviorSeed(profile dnfrobotdomain.Profile) uint64 {
|
||||
hash := fnv.New64a()
|
||||
_, _ = hash.Write([]byte(strings.ToLower(strings.TrimSpace(profile.ID))))
|
||||
_, _ = hash.Write([]byte{0})
|
||||
_, _ = hash.Write([]byte(profile.AccountID))
|
||||
_, _ = hash.Write([]byte{byte(profile.CharacterID), byte(profile.CharacterID >> 8)})
|
||||
return hash.Sum64()
|
||||
}
|
||||
|
||||
func automationPersonality(seed uint64) string {
|
||||
switch seed % 4 {
|
||||
case 0:
|
||||
return "merchant"
|
||||
case 1:
|
||||
return "wanderer"
|
||||
case 2:
|
||||
return "social"
|
||||
default:
|
||||
return "balanced"
|
||||
}
|
||||
}
|
||||
|
||||
func automationBehaviorDelay(minSeconds, maxSeconds, cursor int, seed uint64, personality, action string) time.Duration {
|
||||
if minSeconds <= 0 {
|
||||
return 0
|
||||
}
|
||||
numerator := 100
|
||||
switch personality {
|
||||
case "merchant":
|
||||
switch action {
|
||||
case automationActionStore:
|
||||
numerator = 70
|
||||
case automationActionMove:
|
||||
numerator = 140
|
||||
case automationActionChat:
|
||||
numerator = 120
|
||||
}
|
||||
case "wanderer":
|
||||
if action == automationActionMove {
|
||||
numerator = 65
|
||||
} else if action == automationActionStore {
|
||||
numerator = 150
|
||||
}
|
||||
case "social":
|
||||
if action == automationActionChat {
|
||||
numerator = 60
|
||||
} else if action == automationActionStore {
|
||||
numerator = 130
|
||||
}
|
||||
}
|
||||
minSeconds = maxInt(1, minSeconds*numerator/100)
|
||||
maxSeconds = maxInt(minSeconds, maxSeconds*numerator/100)
|
||||
if seed == 0 {
|
||||
return automationDelay(minSeconds, maxSeconds, cursor)
|
||||
}
|
||||
width := maxSeconds - minSeconds + 1
|
||||
offset := int(automationMix(seed, uint64(cursor), stringSeed(action)) % uint64(width))
|
||||
return time.Duration(minSeconds+offset) * time.Second
|
||||
}
|
||||
|
||||
func automationBehaviorPosition(originX, originY, radius uint16, cursor int, seed uint64) (uint16, uint16) {
|
||||
if seed == 0 {
|
||||
return automationPosition(originX, originY, radius, cursor)
|
||||
}
|
||||
if radius == 0 {
|
||||
return originX, originY
|
||||
}
|
||||
mixed := automationMix(seed, uint64(cursor), 0x6d6f7665)
|
||||
distance := int(radius)/2 + int(mixed%uint64(maxInt(1, int(radius)/2+1)))
|
||||
directions := [8][2]int{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}}
|
||||
direction := directions[(mixed>>8)&7]
|
||||
x := clampAutomationCoordinate(int(originX) + direction[0]*distance)
|
||||
y := clampAutomationCoordinate(int(originY) + direction[1]*distance)
|
||||
return uint16(x), uint16(y)
|
||||
}
|
||||
|
||||
func automationBehaviorPositionForProfile(profile dnfrobotdomain.Profile, radius uint16, cursor int, seed uint64) (uint16, uint16) {
|
||||
x, y := automationBehaviorPosition(profile.X, profile.Y, radius, cursor, seed)
|
||||
if !profile.MoveBoundsSet {
|
||||
return x, y
|
||||
}
|
||||
return clampAutomationRange(x, profile.MoveMinX, profile.MoveMaxX), clampAutomationRange(y, profile.MoveMinY, profile.MoveMaxY)
|
||||
}
|
||||
|
||||
func clampAutomationRange(value, minimum, maximum uint16) uint16 {
|
||||
if value < minimum {
|
||||
return minimum
|
||||
}
|
||||
if value > maximum {
|
||||
return maximum
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func automationMessageIndex(cursor int, seed uint64, count int) int {
|
||||
if count <= 0 {
|
||||
return 0
|
||||
}
|
||||
if seed == 0 {
|
||||
return cursor % count
|
||||
}
|
||||
return int(automationMix(seed, uint64(cursor), 0x63686174) % uint64(count))
|
||||
}
|
||||
|
||||
func automationStorePrice(base uint32, seed uint64, cursor int) uint32 {
|
||||
if base == 0 || seed == 0 {
|
||||
return base
|
||||
}
|
||||
percent := uint64(85) + automationMix(seed, uint64(cursor), 0x7072696365)%31
|
||||
price := uint64(base) * percent / 100
|
||||
if price == 0 {
|
||||
return 1
|
||||
}
|
||||
if price > uint64(^uint32(0)) {
|
||||
return ^uint32(0)
|
||||
}
|
||||
return uint32(price)
|
||||
}
|
||||
|
||||
func automationMix(seed, cursor, salt uint64) uint64 {
|
||||
value := seed ^ salt ^ (cursor+1)*0x9e3779b97f4a7c15
|
||||
value ^= value >> 30
|
||||
value *= 0xbf58476d1ce4e5b9
|
||||
value ^= value >> 27
|
||||
value *= 0x94d049bb133111eb
|
||||
return value ^ (value >> 31)
|
||||
}
|
||||
|
||||
func stringSeed(value string) uint64 {
|
||||
hash := fnv.New64a()
|
||||
_, _ = hash.Write([]byte(value))
|
||||
return hash.Sum64()
|
||||
}
|
||||
|
||||
func clampAutomationCoordinate(value int) int {
|
||||
if value < 0 {
|
||||
return 0
|
||||
}
|
||||
if value > 65535 {
|
||||
return 65535
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func maxInt(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in new issue
Block a user