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

499 lines
13 KiB
Go

package wedding
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"sort"
"strconv"
"strings"
"time"
"longheng.io/server/internal/modules/dnf/pvf"
repo "longheng.io/server/internal/modules/dnf/repository"
)
type TreeReward struct {
Item
Weight int64
}
type Tree struct {
Days, DailyWater, MaxGrowth int64
Water map[int64]int64
Rewards []TreeReward
}
type RoomCatalog struct {
Seeds map[int64]int64
Trees map[int64]Tree
Positions map[int64]map[int]bool
}
type RoomState struct {
Installed [28]int64
Cargo [40]Item
Tree, Expires, Growth, WaterDay, WaterCount int64
}
type RoomCommand struct {
ActorID, PartnerID, AccountID string
Opcode uint16
Slot int32
ItemID, Count int64
From, To byte
FromSlot, ToSlot int
FromItem, ToItem int64
Gold int64
Materials []Item
Now time.Time
Grant func(*repo.InventoryRecord, Item) error
}
type RoomResult struct {
Result
Room RoomState
Reward Item
Slot int
}
func LoadRoomCatalog(source pvf.Source) (*RoomCatalog, error) {
c := &RoomCatalog{Seeds: map[int64]int64{}, Trees: map[int64]Tree{}, Positions: map[int64]map[int]bool{}}
for _, path := range []string{"decoration/coupletree.etc", "decoration/coupleroomiteminfo.etc"} {
text, err := source.ReadText(path)
if err != nil {
return nil, err
}
doc, err := pvf.Parse(path, text)
if err != nil {
return nil, err
}
var tree Tree
var index int64
for _, section := range doc.Sections {
tokens := doc.Tokens[section.Start:section.End]
var values []int64
for _, token := range tokens {
if token.Kind == pvf.TokenInt {
values = append(values, token.Int)
}
}
switch strings.ToLower(section.Name) {
case "seed":
if len(values)%2 != 0 {
return nil, Invalid
}
for i := 0; i < len(values); i += 2 {
c.Seeds[values[i]] = values[i+1]
}
case "tree":
tree = Tree{Water: map[int64]int64{}}
index = 0
case "index":
if len(values) == 1 {
index = values[0]
}
case "expire day":
if len(values) == 1 {
tree.Days = values[0]
}
case "day water count":
if len(values) == 1 {
tree.DailyWater = values[0]
}
case "grow point":
if len(values)%2 != 0 {
return nil, Invalid
}
for i := 0; i < len(values); i += 2 {
if values[i] <= 0 || values[i+1] <= 0 {
return nil, Invalid
}
tree.Water[values[i]] = values[i+1]
}
case "step":
for _, v := range values {
tree.MaxGrowth = max(tree.MaxGrowth, v)
}
case "reward":
if len(values)%3 != 0 {
return nil, Invalid
}
for i := 0; i < len(values); i += 3 {
if values[i] <= 0 || values[i+1] <= 0 || values[i+1] > 1000000 || values[i+2] <= 0 {
return nil, Invalid
}
tree.Rewards = append(tree.Rewards, TreeReward{Item{values[i], values[i+2]}, values[i+1]})
}
case "/tree":
if index <= 0 || tree.Days <= 0 || tree.Days > 365 || tree.DailyWater <= 0 || tree.DailyWater > 255 || tree.MaxGrowth <= 0 || len(tree.Water) == 0 || len(tree.Rewards) == 0 {
return nil, Invalid
}
c.Trees[index] = tree
case "coupleroom item":
if len(values) < 3 || values[1] < 1 || values[1] > 28 || len(values) != 3+5*int(values[1]) {
return nil, Invalid
}
positions := map[int]bool{}
for i := int64(0); i < values[1]; i++ {
p := values[2+5*i]
if p < 0 || p >= 28 {
return nil, Invalid
}
positions[int(p)] = true
}
c.Positions[values[0]] = positions
}
}
}
if len(c.Seeds) == 0 || len(c.Positions) == 0 {
return nil, Invalid
}
for item, index := range c.Seeds {
if item <= 0 || c.Trees[index].MaxGrowth <= 0 {
return nil, Invalid
}
}
return c, nil
}
func ReadRoom(character repo.CharacterRecord, pairID uint32, now time.Time) (RoomState, error) {
var room RoomState
v := character.Stats
if v["wedding_house_id"] != int64(pairID) {
return room, nil
}
for i := range room.Installed {
room.Installed[i] = v[fmt.Sprintf("wedding_house_installed_%d", i)]
if room.Installed[i] < 0 || room.Installed[i] > 0xffffffff {
return RoomState{}, Invalid
}
}
for i := range room.Cargo {
room.Cargo[i] = Item{v[fmt.Sprintf("wedding_house_cargo_%d", i)], v[fmt.Sprintf("wedding_house_count_%d", i)]}
if room.Cargo[i].ID < 0 || room.Cargo[i].ID > 0xffffffff || room.Cargo[i].Count < 0 || room.Cargo[i].Count > 65535 || (room.Cargo[i].ID == 0) != (room.Cargo[i].Count == 0) {
return RoomState{}, Invalid
}
}
room.Tree, room.Expires, room.Growth = v["wedding_house_tree"], v["wedding_house_expires"], v["wedding_house_growth"]
room.WaterDay, room.WaterCount = v["wedding_house_day"], v["wedding_house_water"]
if room.Tree < 0 || room.Tree > 0xffffffff || room.Expires < 0 || room.Expires > 0xffffffff || room.Growth < 0 || room.Growth > 0xffffffff || room.WaterCount < 0 || room.WaterCount > 255 {
return RoomState{}, Invalid
}
if room.Tree != 0 && room.Expires <= now.Unix() {
room.Tree, room.Expires, room.Growth = 0, 0, 0
}
day, _ := strconv.ParseInt(now.In(time.FixedZone("CST", 8*3600)).Format("20060102"), 10, 64)
if room.WaterDay != day {
room.WaterDay, room.WaterCount = day, 0
}
return room, nil
}
func clearRoom(c *repo.CharacterRecord) {
for key := range c.Stats {
if strings.HasPrefix(key, "wedding_house_") {
delete(c.Stats, key)
}
}
}
func storeRoom(c *repo.CharacterRecord, pairID uint32, r RoomState) {
clearRoom(c)
c.Stats["wedding_house_id"] = int64(pairID)
for i, id := range r.Installed {
if id != 0 {
c.Stats[fmt.Sprintf("wedding_house_installed_%d", i)] = id
}
}
for i, item := range r.Cargo {
if item.ID != 0 {
c.Stats[fmt.Sprintf("wedding_house_cargo_%d", i)] = item.ID
c.Stats[fmt.Sprintf("wedding_house_count_%d", i)] = item.Count
}
}
for key, value := range map[string]int64{"tree": r.Tree, "expires": r.Expires, "growth": r.Growth, "day": r.WaterDay, "water": r.WaterCount} {
c.Stats["wedding_house_"+key] = value
}
}
func CommitRoom(ctx context.Context, repos repo.Group, cat *Catalog, rules *RoomCatalog, cmd RoomCommand) (RoomResult, error) {
if repos.CharacterTrade == nil || rules == nil || cmd.ActorID == cmd.PartnerID || cmd.Now.IsZero() || cmd.AccountID == "" {
return RoomResult{}, Invalid
}
ids := []string{cmd.ActorID, cmd.PartnerID}
sort.Strings(ids)
var out RoomResult
err := repos.CharacterTrade.WithinCharacterTrade(ctx, ids[0], ids[1], func(chars repo.CharacterRepository, bags repo.InventoryRepository) error {
for i, id := range []string{cmd.ActorID, cmd.PartnerID} {
c, found, err := chars.Load(ctx, id)
if err != nil {
return err
}
if !found {
return Missing
}
out.Characters[i] = repo.CloneCharacter(c)
b, found, err := bags.Load(ctx, id)
if err != nil {
return err
}
if !found || b.CharacterID != id {
return Invalid
}
out.Inventories[i] = repo.CloneInventory(b)
}
a, b := &out.Characters[0], &out.Characters[1]
if a.AccountID != cmd.AccountID {
return Invalid
}
if err := Validate(*a, *b, cat, 9, cmd.Now); err != nil {
return err
}
s, _ := Read(*a)
canonical := a
if b.CharacterID < a.CharacterID {
canonical = b
}
r, err := ReadRoom(*canonical, s.PairID, cmd.Now)
if err != nil {
return err
}
bag := &out.Inventories[0]
switch cmd.Opcode {
case 1045: // Query the shared decoration storage.
case 21: // NPC pricing is resolved by the existing shop catalog.
if len(rules.Positions[cmd.ItemID]) == 0 || cmd.Count < 1 || cmd.Count > 65535 || cmd.Gold < 0 || a.Stats["gold"] < 0 || a.Stats["gold"] > 0x7fffffff {
return Invalid
}
if cmd.Gold > a.Stats["gold"] {
return GoldMissing
}
for _, cost := range cmd.Materials {
if cost.Count <= 0 {
return Invalid
}
if cost.ID == 10002606 {
if a.Stats["wedding_love"] != b.Stats["wedding_love"] || a.Stats["wedding_love"] < cost.Count {
return Error(-9)
}
a.Stats["wedding_love"] -= cost.Count
b.Stats["wedding_love"] -= cost.Count
} else if err := consume(bag, -1, cost, cmd.Now); err != nil {
return Error(-9)
}
}
a.Stats["gold"] -= cmd.Gold
slot := -1
for i, item := range r.Cargo {
if item.ID == cmd.ItemID && item.Count <= 65535-cmd.Count {
slot = i
break
}
}
if slot < 0 {
for i, item := range r.Cargo {
if item.ID == 0 {
slot = i
break
}
}
}
if slot < 0 {
return Error(-20)
}
r.Cargo[slot] = Item{cmd.ItemID, r.Cargo[slot].Count + cmd.Count}
out.Slot = slot
out.AssetsChanged = true
case 1046:
if err := moveRoomItem(&r, rules, cmd); err != nil {
return err
}
case 1047: // Delete the selected number of stored decorations.
if cmd.Slot < 0 || cmd.Slot >= 40 || cmd.Count < 1 {
return Invalid
}
item := &r.Cargo[cmd.Slot]
if item.ID != cmd.ItemID || item.Count < cmd.Count {
return Invalid
}
item.Count -= cmd.Count
if item.Count == 0 {
*item = Item{}
}
case 1048:
index := rules.Seeds[cmd.ItemID]
tree, ok := rules.Trees[index]
if r.Tree != 0 || !ok {
return Error(-4)
}
if err := consume(bag, cmd.Slot, Item{cmd.ItemID, 1}, cmd.Now); err != nil {
return Error(-4)
}
r.Tree, r.Expires, r.Growth = index, cmd.Now.Unix()+tree.Days*86400, 0
out.AssetsChanged = true
case 1049:
tree, ok := rules.Trees[r.Tree]
if !ok || cmd.Now.Unix() >= r.Expires {
return Error(-4)
}
points := tree.Water[cmd.ItemID]
if points <= 0 {
return Error(-9)
}
if r.Growth >= tree.MaxGrowth {
return Error(-9)
}
if r.WaterCount >= tree.DailyWater {
return Error(-6)
}
if err := consume(bag, cmd.Slot, Item{cmd.ItemID, 1}, cmd.Now); err != nil {
return Error(-9)
}
r.Growth = min(tree.MaxGrowth, r.Growth+points)
r.WaterCount++
out.AssetsChanged = true
case 1050:
tree, ok := rules.Trees[r.Tree]
if !ok || cmd.Now.Unix() >= r.Expires {
return Error(-4)
}
if r.Growth < tree.MaxGrowth {
return Error(-6)
}
if cmd.Grant == nil {
return Invalid
}
var total int64
for _, reward := range tree.Rewards {
total += reward.Weight
}
if total <= 0 {
return Invalid
}
roll, err := rand.Int(rand.Reader, big.NewInt(total))
if err != nil {
return err
}
n := roll.Int64()
for _, reward := range tree.Rewards {
n -= reward.Weight
if n < 0 {
out.Reward = reward.Item
break
}
}
for i := range out.Inventories {
if err := cmd.Grant(&out.Inventories[i], out.Reward); err != nil {
return err
}
}
r.Growth = 0
out.AssetsChanged = true
case 1051:
r.Tree, r.Expires, r.Growth = 0, 0, 0
default:
return Invalid
}
if cmd.Opcode != 1045 {
storeRoom(canonical, s.PairID, r)
for _, c := range out.Characters {
c.UpdatedAt = cmd.Now
if err := repo.SaveCharacterFields(ctx, chars, c, repo.CharacterFieldStats); err != nil {
return err
}
}
if out.AssetsChanged {
for _, bag := range out.Inventories {
bag.UpdatedAt = cmd.Now
if err := repo.SaveInventoryFields(ctx, bags, bag, repo.InventoryFieldSlots); err != nil {
return err
}
}
}
}
out.Room = r
return nil
})
if err != nil {
return RoomResult{}, err
}
return out, nil
}
func moveRoomItem(r *RoomState, c *RoomCatalog, cmd RoomCommand) error {
if cmd.From > 1 || cmd.To > 1 || cmd.FromSlot < 0 || cmd.ToSlot < 0 || (cmd.From == cmd.To && cmd.FromSlot == cmd.ToSlot) {
return Invalid
}
get := func(kind byte, slot int) (Item, bool) {
if kind == 0 {
if slot >= len(r.Installed) {
return Item{}, false
}
id := r.Installed[slot]
if id == 0 {
return Item{}, true
}
return Item{id, 1}, true
}
if slot >= len(r.Cargo) {
return Item{}, false
}
return r.Cargo[slot], true
}
from, ok := get(cmd.From, cmd.FromSlot)
if !ok || from.ID == 0 || from.ID != cmd.FromItem {
return Invalid
}
to, ok := get(cmd.To, cmd.ToSlot)
if !ok {
return Invalid
}
// Native empty destination sometimes carries the source ID.
if to.ID != 0 && to.ID != cmd.ToItem {
return Invalid
}
if cmd.From == 0 && cmd.To == 0 {
return Invalid
}
if cmd.To == 0 && !c.Positions[from.ID][cmd.ToSlot] {
return Error(-20)
}
if cmd.From == 1 && cmd.To == 1 {
r.Cargo[cmd.FromSlot], r.Cargo[cmd.ToSlot] = to, from
return nil
}
if cmd.From == 0 {
if to.ID != 0 && to.ID != from.ID {
return Error(-20)
}
if to.Count >= 65535 {
return Invalid
}
r.Cargo[cmd.ToSlot] = Item{from.ID, to.Count + 1}
r.Installed[cmd.FromSlot] = 0
return nil
}
if to.ID != 0 {
if from.Count == 1 {
r.Cargo[cmd.FromSlot] = to
} else {
empty := -1
for i, item := range r.Cargo {
if item.ID == 0 {
empty = i
break
}
}
if empty < 0 {
return Error(-20)
}
r.Cargo[empty] = to
r.Cargo[cmd.FromSlot].Count--
}
} else {
r.Cargo[cmd.FromSlot].Count--
if r.Cargo[cmd.FromSlot].Count == 0 {
r.Cargo[cmd.FromSlot] = Item{}
}
}
r.Installed[cmd.ToSlot] = from.ID
return nil
}