Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

179 lines
5.1 KiB
Go

package dnfrobot
import (
"context"
"errors"
"fmt"
"strings"
)
const (
BatchActionStart = "start"
BatchActionStop = "stop"
BatchActionMove = "move"
BatchActionChat = "chat"
BatchActionStoreOpen = "store_open"
BatchActionStoreClose = "store_close"
)
type BatchRequest struct {
Action string `json:"action"`
IDs []string `json:"ids,omitempty"`
Village uint8 `json:"village,omitempty"`
Area uint8 `json:"area,omitempty"`
X uint16 `json:"x,omitempty"`
Y uint16 `json:"y,omitempty"`
MoveType uint8 `json:"move_type,omitempty"`
ChatMode uint8 `json:"chat_mode,omitempty"`
Message string `json:"message,omitempty"`
StoreTitle string `json:"store_title,omitempty"`
StoreRows int `json:"store_rows,omitempty"`
StorePrice uint32 `json:"store_price,omitempty"`
}
type BatchActionResult struct {
ID string `json:"id"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Snapshot RuntimeSnapshot `json:"snapshot"`
}
type BatchResult struct {
Action string `json:"action"`
Requested int `json:"requested"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Results []BatchActionResult `json:"results"`
}
type ManagerStatus struct {
Configured int `json:"configured"`
Running int `json:"running"`
Connecting int `json:"connecting"`
TownReady int `json:"town_ready"`
StoreReady int `json:"store_ready"`
Pending int `json:"pending"`
Errors int `json:"errors"`
Reconnects int `json:"reconnects"`
TargetOnline int `json:"target_online"`
SchedulerManaged int `json:"scheduler_managed"`
}
func (m *Manager) Status() ManagerStatus {
status := ManagerStatus{TargetOnline: m.Automation().TargetOnline}
for _, snapshot := range m.List() {
status.Configured++
status.Reconnects += snapshot.Reconnects
if snapshot.Running {
status.Running++
}
if snapshot.SchedulerManaged {
status.SchedulerManaged++
}
switch snapshot.State {
case StateConnecting, StateBinding, StateSelecting:
status.Connecting++
case StateTownReady:
status.TownReady++
case StateStoreReady:
status.StoreReady++
case StateStoreOpening, StateStoreClosing:
status.Pending++
case StateError:
status.Errors++
}
if snapshot.LastError != "" && snapshot.State != StateError {
status.Errors++
}
}
return status
}
func (m *Manager) Batch(ctx context.Context, request BatchRequest) (BatchResult, error) {
if m == nil {
return BatchResult{}, ErrManagerStopped
}
action := strings.ToLower(strings.TrimSpace(request.Action))
switch action {
case BatchActionStart, BatchActionStop, BatchActionMove, BatchActionChat, BatchActionStoreOpen, BatchActionStoreClose:
default:
return BatchResult{}, errors.New("invalid dnf robot batch action")
}
ids := m.batchProfileIDs(request.IDs)
if len(ids) == 0 {
return BatchResult{}, ErrProfileNotFound
}
operation := m.BeginOperation("batch", action)
result := BatchResult{Action: action, Requested: len(ids), Results: make([]BatchActionResult, 0, len(ids))}
for _, id := range ids {
var (
snapshot RuntimeSnapshot
err error
)
switch action {
case BatchActionStart:
snapshot, err = m.StartProfile(id)
case BatchActionStop:
snapshot, err = m.StopProfile(id)
case BatchActionMove:
err = m.Move(id, request.Village, request.Area, request.X, request.Y, request.MoveType)
snapshot = m.snapshotByID(id)
case BatchActionChat:
err = m.Chat(id, request.ChatMode, request.Message)
snapshot = m.snapshotByID(id)
case BatchActionStoreOpen:
err = m.OpenPrivateStore(ctx, id, request.StoreTitle, request.StoreRows, request.StorePrice)
snapshot = m.snapshotByID(id)
case BatchActionStoreClose:
err = m.ClosePrivateStore(id)
snapshot = m.snapshotByID(id)
}
row := BatchActionResult{ID: id, OK: err == nil, Snapshot: snapshot}
if err != nil {
row.Error = err.Error()
result.Failed++
} else {
result.Succeeded++
}
result.Results = append(result.Results, row)
}
m.CompleteOperation(operation.ID, fmt.Sprintf("requested=%d succeeded=%d failed=%d", result.Requested, result.Succeeded, result.Failed), nil)
return result, nil
}
func (m *Manager) batchProfileIDs(requested []string) []string {
if len(requested) == 0 {
m.mu.RLock()
ids := make([]string, 0, len(m.config.Profiles))
for _, profile := range m.config.Profiles {
ids = append(ids, profile.ID)
}
m.mu.RUnlock()
return ids
}
seen := make(map[string]struct{}, len(requested))
ids := make([]string, 0, len(requested))
for _, id := range requested {
id = strings.TrimSpace(id)
key := profileKey(id)
if key == "" {
continue
}
if _, exists := seen[key]; exists {
continue
}
seen[key] = struct{}{}
ids = append(ids, id)
}
return ids
}
func (m *Manager) snapshotByID(id string) RuntimeSnapshot {
for _, snapshot := range m.List() {
if profileKey(snapshot.Profile.ID) == profileKey(id) {
return snapshot
}
}
return RuntimeSnapshot{}
}