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,498 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user