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,431 @@
|
||||
package guild
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWarClosed = errors.New("guild war is closed")
|
||||
ErrWarNoOpponent = errors.New("no eligible rival guild")
|
||||
ErrWarActive = errors.New("guild war already active")
|
||||
ErrWarNotActive = errors.New("guild war is not active")
|
||||
ErrWarLimit = errors.New("guild war daily participation limit")
|
||||
ErrWarGuardian = errors.New("guild war requires an active guardian")
|
||||
)
|
||||
|
||||
type WarRules struct {
|
||||
Days [7]bool
|
||||
PauseStart, PauseEnd int
|
||||
PlaySeconds, DailyMax, EnemyHP, ClearDamage uint32
|
||||
FriendlyHP uint32
|
||||
EnemyDungeon uint32
|
||||
Dungeons map[uint32]bool
|
||||
RequiredClear []uint32
|
||||
ChallengeCosts []uint32
|
||||
}
|
||||
|
||||
func LoadWarRules(source dnfpvf.Source) (*WarRules, error) {
|
||||
doc, err := readWarDocument(source, "etc/agitwar.etc")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := &WarRules{Dungeons: make(map[uint32]bool)}
|
||||
values := func(name string) ([]uint32, error) {
|
||||
t, ok := doc.Section(name)
|
||||
if !ok {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
return warUnsigned(t)
|
||||
}
|
||||
days, err := values("agitwar day")
|
||||
if err != nil || len(days) == 0 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
for _, day := range days {
|
||||
if day > 6 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
r.Days[day] = true
|
||||
}
|
||||
pause, ok := doc.Section("war limit time")
|
||||
if !ok || len(pause) != 2 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
for i, t := range pause {
|
||||
v := strings.Split(t.Value, ":")
|
||||
if len(v) != 2 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
h, e := strconv.Atoi(v[0])
|
||||
m, e2 := strconv.Atoi(v[1])
|
||||
if e != nil || e2 != nil || h < 0 || h > 23 || m < 0 || m > 59 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
if i == 0 {
|
||||
r.PauseStart = h*60 + m
|
||||
} else {
|
||||
r.PauseEnd = h*60 + m
|
||||
}
|
||||
}
|
||||
for _, s := range []struct {
|
||||
name string
|
||||
dst *uint32
|
||||
}{{"play time", &r.PlaySeconds}, {"daily max challenge", &r.DailyMax}, {"enemy hp", &r.EnemyHP}, {"clear damage", &r.ClearDamage}} {
|
||||
v, e := values(s.name)
|
||||
if e != nil || len(v) != 1 || v[0] == 0 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
*s.dst = v[0]
|
||||
}
|
||||
if r.PlaySeconds > 86400 || r.DailyMax > 100 || r.ClearDamage > r.EnemyHP {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
if v, e := values("hp"); e == nil && len(v) > 0 {
|
||||
r.FriendlyHP = v[0]
|
||||
} else {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
r.ChallengeCosts, err = values("challenge chance")
|
||||
if err != nil || len(r.ChallengeCosts) == 0 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
r.RequiredClear, err = values("required clear dungeon")
|
||||
if err != nil || len(r.RequiredClear) == 0 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
scope := ""
|
||||
for _, sec := range doc.Sections {
|
||||
switch sec.Name {
|
||||
case "enemy agit", "friend agit", "cannon", "support dungeon", "underpass dungeon":
|
||||
scope = sec.Name
|
||||
case "/enemy agit", "/friend agit", "/cannon", "/support dungeon", "/underpass dungeon":
|
||||
scope = ""
|
||||
case "index":
|
||||
if scope != "" {
|
||||
v, e := warUnsigned(doc.Tokens[sec.Start:sec.End])
|
||||
if e != nil || len(v) != 1 || v[0] == 0 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
r.Dungeons[v[0]] = true
|
||||
if scope == "enemy agit" {
|
||||
r.EnemyDungeon = v[0]
|
||||
}
|
||||
}
|
||||
case "enemy barricade dungeon", "friend barricade dungeon":
|
||||
v, e := warUnsigned(doc.Tokens[sec.Start:sec.End])
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
for _, id := range v {
|
||||
if id == 0 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
r.Dungeons[id] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.EnemyDungeon == 0 {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
for _, id := range r.RequiredClear {
|
||||
if !r.Dungeons[id] {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *WarRules) Open(now time.Time) bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
local := now.In(time.FixedZone("CST", 8*3600))
|
||||
day, _ := WarDay(now)
|
||||
minute := local.Hour()*60 + local.Minute()
|
||||
paused := minute >= r.PauseStart && minute < r.PauseEnd
|
||||
if r.PauseStart > r.PauseEnd {
|
||||
paused = minute >= r.PauseStart || minute < r.PauseEnd
|
||||
}
|
||||
return r.Days[int(day.Weekday())] && !paused
|
||||
}
|
||||
|
||||
// D13FD0 computes the next Monday at 06:00 for the native season boundary.
|
||||
func WarSeason(now time.Time) (time.Time, time.Time) {
|
||||
day, _ := WarDay(now)
|
||||
start := day.AddDate(0, 0, -(int(day.Weekday())+6)%7)
|
||||
return start, start.AddDate(0, 0, 7)
|
||||
}
|
||||
|
||||
type WarBattle struct {
|
||||
Key string `json:"key"`
|
||||
Opponent uint32 `json:"opponent"`
|
||||
OpponentGuardian Guardian `json:"opponent_guardian"`
|
||||
OwnGuardian Guardian `json:"own_guardian"`
|
||||
StartedAt, EndsAt int64
|
||||
EnemyHP uint32 `json:"enemy_hp"`
|
||||
Clears map[uint32]uint32 `json:"clears,omitempty"`
|
||||
Members map[string]bool `json:"members,omitempty"`
|
||||
Receipts map[string]bool `json:"receipts,omitempty"`
|
||||
}
|
||||
type WarState struct {
|
||||
DayStart, SeasonStart int64
|
||||
NextBattle uint64 `json:"next_battle"`
|
||||
Rivals []uint32 `json:"rivals,omitempty"`
|
||||
Results map[uint32]uint32 `json:"results,omitempty"`
|
||||
Participants map[string]uint32 `json:"participants,omitempty"`
|
||||
AttackWins, AttackLosses, DefenceWins, DefenceLosses uint32
|
||||
Battle *WarBattle `json:"battle,omitempty"`
|
||||
LastBattle *WarBattle `json:"last_battle,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Registry) AdvanceWars(now time.Time) {
|
||||
for _, g := range r.Guilds {
|
||||
if g != nil && g.War.Battle != nil && now.Unix() >= g.War.Battle.EndsAt {
|
||||
r.finishWar(g, false)
|
||||
}
|
||||
}
|
||||
day, _ := WarDay(now)
|
||||
season, _ := WarSeason(now)
|
||||
for _, g := range r.Guilds {
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
w := &g.War
|
||||
if w.DayStart != day.Unix() {
|
||||
w.DayStart = day.Unix()
|
||||
w.Rivals = nil
|
||||
w.Results = nil
|
||||
w.Participants = nil
|
||||
}
|
||||
if w.SeasonStart != season.Unix() {
|
||||
w.SeasonStart = season.Unix()
|
||||
w.AttackWins = 0
|
||||
w.AttackLosses = 0
|
||||
w.DefenceWins = 0
|
||||
w.DefenceLosses = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Registry) AddWarRivals(characterID string, initial bool, rules *WarRules, now time.Time) (*Guild, error) {
|
||||
g, m, ok := r.GuildForCharacter(characterID)
|
||||
if !ok {
|
||||
return nil, ErrNotMember
|
||||
}
|
||||
if !rules.Open(now) {
|
||||
return nil, ErrWarClosed
|
||||
}
|
||||
if !initial && g.Permissions(m.Grade)&(1<<16) == 0 {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
r.AdvanceWars(now)
|
||||
if initial && len(g.War.Rivals) > 0 {
|
||||
return g, nil
|
||||
}
|
||||
if len(g.War.Rivals) >= 40 {
|
||||
return nil, ErrWarLimit
|
||||
}
|
||||
existing := map[uint32]bool{g.ID: true}
|
||||
for _, id := range g.War.Rivals {
|
||||
existing[id] = true
|
||||
}
|
||||
candidates := []uint32{}
|
||||
for id, other := range r.Guilds {
|
||||
_, hasGuardian := other.WarGuardian()
|
||||
if other != nil && !existing[id] && hasGuardian && len(other.Members) > 0 {
|
||||
candidates = append(candidates, id)
|
||||
}
|
||||
}
|
||||
sort.Slice(candidates, func(i, j int) bool { return candidates[i] < candidates[j] })
|
||||
if len(candidates) == 0 {
|
||||
return nil, ErrWarNoOpponent
|
||||
}
|
||||
costIndex := len(g.War.Rivals) / 8
|
||||
if costIndex >= len(rules.ChallengeCosts) {
|
||||
return nil, ErrWarLimit
|
||||
}
|
||||
cost := rules.ChallengeCosts[costIndex]
|
||||
if initial {
|
||||
cost = 0
|
||||
}
|
||||
if g.GuardianGP < cost {
|
||||
return nil, ErrInsufficientGuardian
|
||||
}
|
||||
g.GuardianGP -= cost
|
||||
g.War.Rivals = append(g.War.Rivals, candidates[:min(8, min(len(candidates), 40-len(g.War.Rivals)))]...)
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (r *Registry) StartWar(characterID string, target uint32, rules *WarRules, now time.Time) (*Guild, error) {
|
||||
g, m, ok := r.GuildForCharacter(characterID)
|
||||
if !ok {
|
||||
return nil, ErrNotMember
|
||||
}
|
||||
if g.Permissions(m.Grade)&(1<<16) == 0 {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
if !rules.Open(now) {
|
||||
return nil, ErrWarClosed
|
||||
}
|
||||
r.AdvanceWars(now)
|
||||
if b := g.War.Battle; b != nil {
|
||||
if b.Opponent == target {
|
||||
return g, nil
|
||||
}
|
||||
return nil, ErrWarActive
|
||||
}
|
||||
ownGuardian, ownReady := g.WarGuardian()
|
||||
if !ownReady {
|
||||
return nil, ErrWarGuardian
|
||||
}
|
||||
candidate := false
|
||||
for _, id := range g.War.Rivals {
|
||||
candidate = candidate || id == target
|
||||
}
|
||||
enemy := r.Guilds[target]
|
||||
enemyGuardian, enemyReady := enemy.WarGuardian()
|
||||
if !candidate || enemy == nil || target == g.ID || !enemyReady {
|
||||
return nil, ErrWarNoOpponent
|
||||
}
|
||||
if g.War.Results[target] != 0 {
|
||||
return nil, ErrWarLimit
|
||||
}
|
||||
if g.War.Participants[characterID] >= rules.DailyMax {
|
||||
return nil, ErrWarLimit
|
||||
}
|
||||
g.War.NextBattle++
|
||||
end := now.Add(time.Duration(rules.PlaySeconds) * time.Second)
|
||||
_, dayEnd := WarDay(now)
|
||||
pause := dayEnd.Add(time.Duration(rules.PauseStart-360) * time.Minute)
|
||||
if pause.After(now) && pause.Before(end) {
|
||||
end = pause
|
||||
}
|
||||
g.War.Battle = &WarBattle{Key: fmt.Sprintf("%d:%d:%d", g.ID, g.War.NextBattle, now.UnixNano()), Opponent: target, StartedAt: now.Unix(), EndsAt: end.Unix(), EnemyHP: rules.EnemyHP, Clears: make(map[uint32]uint32), Members: make(map[string]bool), Receipts: make(map[string]bool)}
|
||||
g.War.Battle.OpponentGuardian, g.War.Battle.OwnGuardian = enemyGuardian, ownGuardian
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (r *Registry) finishWar(g *Guild, won bool) {
|
||||
b := g.War.Battle
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
if g.War.Results == nil {
|
||||
g.War.Results = make(map[uint32]uint32)
|
||||
}
|
||||
enemy := r.Guilds[b.Opponent]
|
||||
if won {
|
||||
g.War.AttackWins++
|
||||
g.War.Results[b.Opponent] = 1
|
||||
if enemy != nil {
|
||||
enemy.War.DefenceLosses++
|
||||
}
|
||||
} else {
|
||||
g.War.AttackLosses++
|
||||
g.War.Results[b.Opponent] = 2
|
||||
if enemy != nil {
|
||||
enemy.War.DefenceWins++
|
||||
}
|
||||
}
|
||||
g.War.LastBattle = b
|
||||
g.War.Battle = nil
|
||||
}
|
||||
|
||||
func (r *Registry) CancelWar(characterID string, now time.Time) (*Guild, error) {
|
||||
g, m, ok := r.GuildForCharacter(characterID)
|
||||
if !ok {
|
||||
return nil, ErrNotMember
|
||||
}
|
||||
if g.Permissions(m.Grade)&(1<<16) == 0 {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
r.AdvanceWars(now)
|
||||
r.finishWar(g, false)
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (r Registry) WarAdmission(characterID string, dungeon uint32, rules *WarRules, now time.Time) (*Guild, error) {
|
||||
g, _, ok := r.GuildForCharacter(characterID)
|
||||
if !ok {
|
||||
return nil, ErrNotMember
|
||||
}
|
||||
b := g.War.Battle
|
||||
if !rules.Open(now) || b == nil || now.Unix() >= b.EndsAt || !rules.Dungeons[dungeon] {
|
||||
return nil, ErrWarNotActive
|
||||
}
|
||||
if !b.Members[characterID] && g.War.Participants[characterID] >= rules.DailyMax {
|
||||
return nil, ErrWarLimit
|
||||
}
|
||||
if dungeon == rules.EnemyDungeon {
|
||||
unlocked := false
|
||||
for _, id := range rules.RequiredClear {
|
||||
unlocked = unlocked || b.Clears[id] > 0
|
||||
}
|
||||
if !unlocked {
|
||||
return nil, ErrInvalidContent
|
||||
}
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (r *Registry) JoinWar(characterID, key string, dungeon uint32, rules *WarRules, now time.Time) (bool, error) {
|
||||
g, err := r.WarAdmission(characterID, dungeon, rules, now)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
b := g.War.Battle
|
||||
if b.Key != key {
|
||||
return false, ErrWarNotActive
|
||||
}
|
||||
if b.Members[characterID] {
|
||||
return false, nil
|
||||
}
|
||||
if b.Members == nil {
|
||||
b.Members = make(map[string]bool)
|
||||
}
|
||||
if g.War.Participants == nil {
|
||||
g.War.Participants = make(map[string]uint32)
|
||||
}
|
||||
b.Members[characterID] = true
|
||||
g.War.Participants[characterID]++
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Registry) ClearWar(characterID, key, receipt string, dungeon uint32, rules *WarRules, now time.Time) error {
|
||||
g, _, ok := r.GuildForCharacter(characterID)
|
||||
if !ok {
|
||||
return ErrNotMember
|
||||
}
|
||||
b := g.War.Battle
|
||||
if last := g.War.LastBattle; last != nil && last.Key == key && last.Members[characterID] && last.Receipts[receipt] {
|
||||
return nil
|
||||
}
|
||||
if b == nil || b.Key != key || !b.Members[characterID] || now.Unix() >= b.EndsAt || !rules.Dungeons[dungeon] || receipt == "" {
|
||||
return ErrWarNotActive
|
||||
}
|
||||
if b.Receipts[receipt] {
|
||||
return nil
|
||||
}
|
||||
if b.Receipts == nil {
|
||||
b.Receipts = make(map[string]bool)
|
||||
}
|
||||
if b.Clears == nil {
|
||||
b.Clears = make(map[uint32]uint32)
|
||||
}
|
||||
b.Receipts[receipt] = true
|
||||
b.Clears[dungeon]++
|
||||
if dungeon == rules.EnemyDungeon {
|
||||
b.EnemyHP -= min(b.EnemyHP, rules.ClearDamage)
|
||||
if b.EnemyHP == 0 {
|
||||
r.finishWar(g, true)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user