服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
105 lines
3.8 KiB
Go
105 lines
3.8 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
|
|
"longheng.io/server/internal/modules/dnf/actionobject"
|
|
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
|
)
|
|
|
|
// passiveObjectActionScript resolves the action script a placed passive object
|
|
// runs: passiveobject.lst gives its `.obj`, the `.obj`'s `[basic action]` gives
|
|
// the `.act` next to it.
|
|
//
|
|
// These scripts are what drive `[DUNGEON SYMBOL CALC]` / `[GATE ENABLE]`, and
|
|
// nothing in this server ran them before: 能源储藏室's boss door is held shut by
|
|
// `storage gate open` and stayed shut forever because no one was counting.
|
|
//
|
|
// A missing or unparsable definition is not an error a room transition should
|
|
// die on, so it is cached as "no script" and reported by the bool.
|
|
func (c *pvfDungeonMonsterCatalog) passiveObjectActionScript(id int64) (actionobject.Script, bool, error) {
|
|
if c == nil || c.source == nil {
|
|
return actionobject.Script{}, false, errDungeonMonsterCatalogUnavailable
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.ensurePassiveObjectIndexLocked()
|
|
if c.passiveObjectErr != nil {
|
|
return actionobject.Script{}, false, c.passiveObjectErr
|
|
}
|
|
if c.actionScriptLoaded[id] {
|
|
script, found := c.actionScripts[id]
|
|
return script, found, nil
|
|
}
|
|
if c.actionScriptLoaded == nil {
|
|
c.actionScriptLoaded = make(map[int64]bool)
|
|
}
|
|
c.actionScriptLoaded[id] = true
|
|
|
|
listedPath, listed := c.passiveObjectPaths[id]
|
|
if !listed {
|
|
return actionobject.Script{}, false, nil
|
|
}
|
|
objectPath, objectText, err := readDungeonPassiveObjectText(c.source, listedPath)
|
|
if err != nil {
|
|
if isDungeonPassiveObjectDefinitionMissing(err) {
|
|
return actionobject.Script{}, false, nil
|
|
}
|
|
return actionobject.Script{}, false, fmt.Errorf("read passive object id=%d path=%s: %w", id, listedPath, err)
|
|
}
|
|
objectDocument, err := dnfpvf.Parse(objectPath, objectText)
|
|
if err != nil {
|
|
return actionobject.Script{}, false, fmt.Errorf("parse passive object id=%d path=%s: %w", id, objectPath, err)
|
|
}
|
|
basicAction, _ := objectDocument.Text("basic action")
|
|
scriptPath := resolveDungeonActionObjectScriptPath(objectPath, basicAction)
|
|
if scriptPath == "" {
|
|
return actionobject.Script{}, false, nil
|
|
}
|
|
scriptText, err := c.source.ReadText(scriptPath)
|
|
if err != nil {
|
|
if isDungeonPassiveObjectDefinitionMissing(err) {
|
|
return actionobject.Script{}, false, nil
|
|
}
|
|
return actionobject.Script{}, false, fmt.Errorf("read action script id=%d path=%s: %w", id, scriptPath, err)
|
|
}
|
|
scriptDocument, err := dnfpvf.Parse(scriptPath, scriptText)
|
|
if err != nil {
|
|
return actionobject.Script{}, false, fmt.Errorf("parse action script id=%d path=%s: %w", id, scriptPath, err)
|
|
}
|
|
script, err := actionobject.Parse(scriptDocument)
|
|
if err != nil {
|
|
return actionobject.Script{}, false, fmt.Errorf("decode action script id=%d path=%s: %w", id, scriptPath, err)
|
|
}
|
|
if !script.Drives() {
|
|
// Animation-only scripts are the overwhelming majority. Dropping them
|
|
// here keeps the cache and every room's instance list small.
|
|
return actionobject.Script{}, false, nil
|
|
}
|
|
if c.actionScripts == nil {
|
|
c.actionScripts = make(map[int64]actionobject.Script)
|
|
}
|
|
c.actionScripts[id] = script
|
|
return script, true, nil
|
|
}
|
|
|
|
// resolveDungeonActionObjectScriptPath joins a `.obj`'s `[basic action]` to the
|
|
// directory that holds it: `Action/Gate_Control_Left.act` beside
|
|
// passiveobject/.../enegy_save/gate_control_left.obj.
|
|
func resolveDungeonActionObjectScriptPath(objectPath, basicAction string) string {
|
|
action := strings.TrimPrefix(strings.TrimSpace(strings.ReplaceAll(basicAction, "\\", "/")), "/")
|
|
if action == "" {
|
|
return ""
|
|
}
|
|
directory := path.Dir(cleanDungeonPassiveObjectPath(objectPath))
|
|
if directory == "." || directory == "" {
|
|
return path.Clean(action)
|
|
}
|
|
if strings.HasPrefix(strings.ToLower(action), strings.ToLower(directory)+"/") {
|
|
return path.Clean(action)
|
|
}
|
|
return path.Join(directory, action)
|
|
}
|