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,925 @@
|
||||
package worldmap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
)
|
||||
|
||||
func ParseMap(id int64, mapPath string, doc *dnfpvf.Document) Map {
|
||||
result := Map{ID: id, Path: mapPath}
|
||||
sections, diagnostics := rawSections(doc)
|
||||
result.SourceSections = sections
|
||||
result.Diagnostics = append(result.Diagnostics, diagnostics...)
|
||||
|
||||
inBackground := false
|
||||
backgroundIndex := -1
|
||||
for _, section := range sections {
|
||||
key := sectionKey(section.Name)
|
||||
if isClosingSection(key) {
|
||||
if key == "/background animation" {
|
||||
inBackground = false
|
||||
backgroundIndex = -1
|
||||
}
|
||||
continue
|
||||
}
|
||||
if inBackground {
|
||||
switch key {
|
||||
case "ani info":
|
||||
result.BackgroundAnimations = append(result.BackgroundAnimations, BackgroundAnimation{})
|
||||
backgroundIndex = len(result.BackgroundAnimations) - 1
|
||||
case "filename", "layer", "order":
|
||||
if backgroundIndex < 0 {
|
||||
result.Diagnostics = append(result.Diagnostics, diagnostic(section, "background field appears outside ani info"))
|
||||
continue
|
||||
}
|
||||
value, ok := firstText(section)
|
||||
if !ok {
|
||||
result.Diagnostics = append(result.Diagnostics, diagnostic(section, "expected text value"))
|
||||
continue
|
||||
}
|
||||
switch key {
|
||||
case "filename":
|
||||
result.BackgroundAnimations[backgroundIndex].Filename = value
|
||||
case "layer":
|
||||
result.BackgroundAnimations[backgroundIndex].Layer = value
|
||||
case "order":
|
||||
result.BackgroundAnimations[backgroundIndex].Order = value
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if scopeContains(section.Scope, "summon") || (key == "summon" && section.Block) {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := knownMapSections[key]; !ok {
|
||||
result.UnknownSections = append(result.UnknownSections, cloneRawSection(section))
|
||||
result.Extensions = append(result.Extensions, structureExtension(section))
|
||||
continue
|
||||
}
|
||||
if key == "tournament enemies" {
|
||||
if !parseTournamentEnemies(&result, section) {
|
||||
result.TournamentMalformed = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if key == "tournament start area" {
|
||||
if !parseTournamentStartAreas(&result, section) {
|
||||
result.TournamentMalformed = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isBloodAltarMapSection(key) {
|
||||
// These four mutually-referencing tables are parsed together after
|
||||
// all sections have been collected, so phase indexes can be checked
|
||||
// against the complete corresponding monster table.
|
||||
continue
|
||||
}
|
||||
if _, ok := opaqueMapSections[key]; ok {
|
||||
result.OpaqueSections = append(result.OpaqueSections, cloneRawSection(section))
|
||||
result.Extensions = append(result.Extensions, structureExtension(section))
|
||||
continue
|
||||
}
|
||||
|
||||
switch key {
|
||||
case "map name":
|
||||
setText(&result.Name, section, &result.Diagnostics)
|
||||
case "player number":
|
||||
result.PlayerNumber = appendInts(result.PlayerNumber, section, &result.Diagnostics)
|
||||
case "pvp start area":
|
||||
result.PVPStartArea = appendInts(result.PVPStartArea, section, &result.Diagnostics)
|
||||
case "dungeon":
|
||||
result.DungeonIDs = appendInts(result.DungeonIDs, section, &result.Diagnostics)
|
||||
if len(result.DungeonIDs) > 0 && !result.DungeonID.Set {
|
||||
result.DungeonID = OptionalInt{Value: result.DungeonIDs[0], Set: true}
|
||||
}
|
||||
case "type":
|
||||
setText(&result.Type, section, &result.Diagnostics)
|
||||
case "greed":
|
||||
setText(&result.Greed, section, &result.Diagnostics)
|
||||
case "tile":
|
||||
result.Tiles = append(result.Tiles, texts(section)...)
|
||||
case "tile files":
|
||||
result.TileFiles = append(result.TileFiles, parseTileFiles(section, &result.Diagnostics)...)
|
||||
case "tile map":
|
||||
row, valid := ints(section)
|
||||
if !valid {
|
||||
result.Diagnostics = append(result.Diagnostics, diagnostic(section, "expected integer tile-map row"))
|
||||
}
|
||||
result.TileMapRows = append(result.TileMapRows, row)
|
||||
case "far sight scroll":
|
||||
setOptionalInt(&result.FarSightScroll, section, &result.Diagnostics)
|
||||
case "middle sight scroll":
|
||||
setOptionalInt(&result.MiddleSightScroll, section, &result.Diagnostics)
|
||||
case "near sight scroll":
|
||||
setOptionalInt(&result.NearSightScroll, section, &result.Diagnostics)
|
||||
case "background animation":
|
||||
inBackground = true
|
||||
backgroundIndex = -1
|
||||
case "pathgate pos":
|
||||
result.PathgatePositions = append(result.PathgatePositions, parsePoints(section, &result.Diagnostics)...)
|
||||
case "pathgate object":
|
||||
result.PathgateObjects = appendInts(result.PathgateObjects, section, &result.Diagnostics)
|
||||
case "sound":
|
||||
result.Sounds = append(result.Sounds, texts(section)...)
|
||||
case "animation":
|
||||
result.AnimationObjects = append(result.AnimationObjects, parseAnimationObjects(section, &result.Diagnostics)...)
|
||||
case "passive object":
|
||||
result.PassiveObjects = append(result.PassiveObjects, parsePassiveObjects(section, &result.Diagnostics)...)
|
||||
case "special passive object":
|
||||
result.SpecialPassiveObjects = append(result.SpecialPassiveObjects, parseSpecialPassiveObjects(section, &result.Diagnostics)...)
|
||||
case "monster":
|
||||
result.Monsters = append(result.Monsters, parseMonsters(section, &result.Diagnostics)...)
|
||||
case "event monster position":
|
||||
result.EventMonsterPositions = append(result.EventMonsterPositions, parsePoints3(section, &result.Diagnostics)...)
|
||||
case "npc":
|
||||
result.NPCs = append(result.NPCs, parseNPCs(section, &result.Diagnostics)...)
|
||||
case "ai character":
|
||||
result.AICharacters = append(result.AICharacters, parseAICharacters(section, &result.Diagnostics)...)
|
||||
case "town movable area":
|
||||
result.TownMovableAreas = append(result.TownMovableAreas, parseMovableAreas(section, &result.Diagnostics)...)
|
||||
case "dungeon movable area":
|
||||
result.DungeonMovableAreas = append(result.DungeonMovableAreas, parseDungeonMovableAreas(section, &result.Diagnostics)...)
|
||||
case "fix champion":
|
||||
setOptionalInt(&result.FixChampion, section, &result.Diagnostics)
|
||||
case "heroes mode map index":
|
||||
setOptionalInt(&result.HeroesModeMapIndex, section, &result.Diagnostics)
|
||||
case "background correction":
|
||||
setOptionalInt(&result.BackgroundCorrection, section, &result.Diagnostics)
|
||||
case "background pos":
|
||||
setOptionalInt(&result.BackgroundPos, section, &result.Diagnostics)
|
||||
case "foreground pattern alpha":
|
||||
setOptionalInt(&result.ForegroundPatternAlpha, section, &result.Diagnostics)
|
||||
case "apc random point":
|
||||
setOptionalInt(&result.APCRandomPoint, section, &result.Diagnostics)
|
||||
case "monster lock":
|
||||
setOptionalInt(&result.MonsterLock, section, &result.Diagnostics)
|
||||
case "draw monster count":
|
||||
setOptionalInt(&result.DrawMonsterCount, section, &result.Diagnostics)
|
||||
case "sort bottom":
|
||||
setOptionalInt(&result.SortBottom, section, &result.Diagnostics)
|
||||
case "add gravity":
|
||||
setOptionalInt(&result.AddGravity, section, &result.Diagnostics)
|
||||
case "jump power rate":
|
||||
setOptionalInt(&result.JumpPowerRate, section, &result.Diagnostics)
|
||||
case "dungeon start area":
|
||||
result.DungeonStartArea = appendInts(result.DungeonStartArea, section, &result.Diagnostics)
|
||||
case "screen pos":
|
||||
result.ScreenPosition = appendInts(result.ScreenPosition, section, &result.Diagnostics)
|
||||
case "monster team":
|
||||
result.MonsterTeam = appendInts(result.MonsterTeam, section, &result.Diagnostics)
|
||||
case "pvp practice start area":
|
||||
result.PVPPracticeStartArea = appendInts(result.PVPPracticeStartArea, section, &result.Diagnostics)
|
||||
case "virtual movable area":
|
||||
result.VirtualMovableArea = appendInts(result.VirtualMovableArea, section, &result.Diagnostics)
|
||||
case "opening bgm":
|
||||
result.OpeningBGM = append(result.OpeningBGM, texts(section)...)
|
||||
case "map loading image path":
|
||||
setText(&result.MapLoadingImagePath, section, &result.Diagnostics)
|
||||
case "basic action":
|
||||
setText(&result.BasicAction, section, &result.Diagnostics)
|
||||
case "absolute start path":
|
||||
result.AbsoluteStartPath = append(result.AbsoluteStartPath, texts(section)...)
|
||||
case "random start map":
|
||||
setOptionalInt(&result.RandomStartMap, section, &result.Diagnostics)
|
||||
case "pathgate recognize range":
|
||||
setOptionalInt(&result.PathgateRecognizeRange, section, &result.Diagnostics)
|
||||
default:
|
||||
setMapFlag(&result.Flags, key)
|
||||
}
|
||||
}
|
||||
parseBloodAltarMapSections(&result, sections)
|
||||
result.Summons = parseMapSummons(sections, &result.Diagnostics)
|
||||
attachHellParty(sections, &result)
|
||||
result.Portals = buildPortals(result.PathgatePositions, result.PathgateObjects)
|
||||
return result
|
||||
}
|
||||
|
||||
func isBloodAltarMapSection(key string) bool {
|
||||
switch sectionKey(key) {
|
||||
case "blood monster", "blood phase time", "ultimate monster", "ultimate phase time":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func parseBloodAltarMapSections(result *Map, sections []RawSection) {
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
result.BloodMonsters = parseBloodAltarMonsterSections(result, sections, "blood monster")
|
||||
result.UltimateMonsters = parseBloodAltarMonsterSections(result, sections, "ultimate monster")
|
||||
result.BloodPhases = parseBloodAltarPhaseSections(
|
||||
result, sections, "blood phase time", result.BloodMonsters, false,
|
||||
)
|
||||
result.UltimatePhases = parseBloodAltarPhaseSections(
|
||||
result, sections, "ultimate phase time", result.UltimateMonsters, true,
|
||||
)
|
||||
}
|
||||
|
||||
func parseBloodAltarMonsterSections(result *Map, sections []RawSection, name string) []BloodAltarMonster {
|
||||
var parsed []BloodAltarMonster
|
||||
valid := true
|
||||
for _, section := range sections {
|
||||
if sectionKey(section.Name) != name {
|
||||
continue
|
||||
}
|
||||
rows, err := parseBloodAltarMonsterTable(section)
|
||||
if err != nil {
|
||||
result.Diagnostics = append(result.Diagnostics, diagnostic(section, err.Error()))
|
||||
valid = false
|
||||
continue
|
||||
}
|
||||
parsed = append(parsed, rows...)
|
||||
}
|
||||
if !valid {
|
||||
return nil
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func parseBloodAltarMonsterTable(section RawSection) ([]BloodAltarMonster, error) {
|
||||
const width = 10
|
||||
tokens := values(section)
|
||||
if len(tokens) == 0 || len(tokens)%width != 0 {
|
||||
return nil, fmt.Errorf("blood altar monster table must contain complete 10-field rows; got %d fields", len(tokens))
|
||||
}
|
||||
parsed := make([]BloodAltarMonster, 0, len(tokens)/width)
|
||||
for offset := 0; offset < len(tokens); offset += width {
|
||||
row := tokens[offset : offset+width]
|
||||
for field, token := range row {
|
||||
if !bloodAltarInt32Token(token) {
|
||||
return nil, fmt.Errorf("blood altar monster row %d field %d must be an int32", offset/width, field)
|
||||
}
|
||||
}
|
||||
if row[0].Int <= 0 ||
|
||||
!bloodAltarCoordinate(row[2].Int) ||
|
||||
!bloodAltarCoordinate(row[3].Int) ||
|
||||
!bloodAltarCoordinate(row[4].Int) ||
|
||||
row[5].Int < 0 || row[6].Int < 0 || row[7].Int < 0 || row[9].Int < 0 {
|
||||
return nil, fmt.Errorf("blood altar monster row %d contains an invalid code, coordinate, duration or count", offset/width)
|
||||
}
|
||||
parsed = append(parsed, BloodAltarMonster{
|
||||
MonsterCode: row[0].Int,
|
||||
TemplateType: row[1].Int,
|
||||
Position: Point3{X: row[2].Int, Y: row[3].Int, Z: row[4].Int},
|
||||
DurationMilliseconds: row[5].Int,
|
||||
SpawnIntervalMilliseconds: row[6].Int,
|
||||
BaseSpawnCount: row[7].Int,
|
||||
SpawnCountIncrement: row[8].Int,
|
||||
BatchCount: row[9].Int,
|
||||
})
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func parseBloodAltarPhaseSections(
|
||||
result *Map,
|
||||
sections []RawSection,
|
||||
name string,
|
||||
monsters []BloodAltarMonster,
|
||||
ultimate bool,
|
||||
) []BloodAltarPhase {
|
||||
var matching []RawSection
|
||||
for _, section := range sections {
|
||||
if sectionKey(section.Name) == name {
|
||||
matching = append(matching, section)
|
||||
}
|
||||
}
|
||||
if len(matching) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(monsters) == 0 {
|
||||
for _, section := range matching {
|
||||
result.Diagnostics = append(result.Diagnostics, diagnostic(section, "blood altar phase table requires a valid matching monster table"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var parsed []BloodAltarPhase
|
||||
valid := true
|
||||
for _, section := range matching {
|
||||
rows, err := parseBloodAltarPhaseTable(section, len(monsters), ultimate)
|
||||
if err != nil {
|
||||
result.Diagnostics = append(result.Diagnostics, diagnostic(section, err.Error()))
|
||||
valid = false
|
||||
continue
|
||||
}
|
||||
parsed = append(parsed, rows...)
|
||||
}
|
||||
if !valid {
|
||||
return nil
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func parseBloodAltarPhaseTable(section RawSection, monsterCount int, ultimate bool) ([]BloodAltarPhase, error) {
|
||||
width := 6
|
||||
if ultimate {
|
||||
width = 7
|
||||
}
|
||||
tokens := values(section)
|
||||
if len(tokens) == 0 || len(tokens)%width != 0 {
|
||||
return nil, fmt.Errorf("blood altar phase table must contain complete %d-field rows; got %d fields", width, len(tokens))
|
||||
}
|
||||
parsed := make([]BloodAltarPhase, 0, len(tokens)/width)
|
||||
for offset := 0; offset < len(tokens); offset += width {
|
||||
row := tokens[offset : offset+width]
|
||||
for _, field := range []int{0, 1, 2, 4, 5} {
|
||||
if !bloodAltarInt32Token(row[field]) {
|
||||
return nil, fmt.Errorf("blood altar phase row %d field %d must be an int32", offset/width, field)
|
||||
}
|
||||
}
|
||||
scale, ok := bloodAltarScale(row[3])
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("blood altar phase row %d scale must be a finite positive number", offset/width)
|
||||
}
|
||||
if row[0].Int < 0 || row[1].Int < 0 || row[1].Int >= int64(monsterCount) ||
|
||||
row[2].Int < 0 || row[5].Int < 0 || row[5].Int > 10 {
|
||||
return nil, fmt.Errorf("blood altar phase row %d contains an invalid round, monster index, delay or concurrency", offset/width)
|
||||
}
|
||||
phase := BloodAltarPhase{
|
||||
Round: row[0].Int,
|
||||
MonsterTemplateIndex: row[1].Int,
|
||||
DelayMilliseconds: row[2].Int,
|
||||
Scale: scale,
|
||||
Flag: row[4].Int,
|
||||
ConcurrentPhaseCount: row[5].Int,
|
||||
}
|
||||
if ultimate {
|
||||
if !isText(row[6]) {
|
||||
return nil, fmt.Errorf("blood altar phase row %d difficulty must be A or B", offset/width)
|
||||
}
|
||||
switch strings.ToUpper(strings.TrimSpace(row[6].Value)) {
|
||||
case string(BloodAltarDifficultyA):
|
||||
phase.Difficulty = BloodAltarDifficultyA
|
||||
case string(BloodAltarDifficultyB):
|
||||
phase.Difficulty = BloodAltarDifficultyB
|
||||
default:
|
||||
return nil, fmt.Errorf("blood altar phase row %d difficulty must be A or B", offset/width)
|
||||
}
|
||||
}
|
||||
parsed = append(parsed, phase)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func bloodAltarInt32Token(token dnfpvf.Token) bool {
|
||||
const (
|
||||
minInt32 = int64(-1 << 31)
|
||||
maxInt32 = int64(1<<31 - 1)
|
||||
)
|
||||
return token.Kind == dnfpvf.TokenInt && token.Int >= minInt32 && token.Int <= maxInt32
|
||||
}
|
||||
|
||||
func bloodAltarCoordinate(value int64) bool {
|
||||
return value >= 0 && value <= 1<<16-1
|
||||
}
|
||||
|
||||
func bloodAltarScale(token dnfpvf.Token) (float64, bool) {
|
||||
var value float64
|
||||
switch token.Kind {
|
||||
case dnfpvf.TokenInt:
|
||||
value = float64(token.Int)
|
||||
case dnfpvf.TokenFloat:
|
||||
value = token.Float
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
return value, value > 0 && !math.IsNaN(value) && !math.IsInf(value, 0)
|
||||
}
|
||||
|
||||
func parseTournamentEnemies(result *Map, section RawSection) bool {
|
||||
if result == nil || len(section.Tokens) < 5 {
|
||||
return false
|
||||
}
|
||||
values := make([]dnfpvf.Token, 0, len(section.Tokens))
|
||||
for _, token := range section.Tokens {
|
||||
if token.Kind != dnfpvf.TokenSymbol {
|
||||
values = append(values, token)
|
||||
}
|
||||
}
|
||||
if len(values) < 5 || values[0].Kind != dnfpvf.TokenInt ||
|
||||
(values[1].Kind != dnfpvf.TokenIdent && values[1].Kind != dnfpvf.TokenString) || (len(values)-2)%3 != 0 {
|
||||
return false
|
||||
}
|
||||
partyCount := values[0].Int
|
||||
kind := strings.TrimSpace(values[1].Value)
|
||||
if partyCount <= 0 || (!strings.EqualFold(kind, "[monster]") && !strings.EqualFold(kind, "[apc]")) {
|
||||
return false
|
||||
}
|
||||
for i := 2; i < len(values); i += 3 {
|
||||
if values[i].Kind != dnfpvf.TokenInt || values[i+1].Kind != dnfpvf.TokenInt {
|
||||
return false
|
||||
}
|
||||
name := strings.TrimSpace(values[i+2].Value)
|
||||
if name == "" || (values[i+2].Kind != dnfpvf.TokenIdent && values[i+2].Kind != dnfpvf.TokenString) {
|
||||
return false
|
||||
}
|
||||
result.TournamentEnemies = append(result.TournamentEnemies, TournamentEnemyCandidate{
|
||||
PartyCount: partyCount, Kind: kind, Code: values[i].Int, Strength: values[i+1].Int, Name: name,
|
||||
})
|
||||
}
|
||||
return len(result.TournamentEnemies) > 0
|
||||
}
|
||||
|
||||
func parseTournamentStartAreas(result *Map, section RawSection) bool {
|
||||
if result == nil {
|
||||
return false
|
||||
}
|
||||
values := make([]int64, 0, len(section.Tokens))
|
||||
for _, token := range section.Tokens {
|
||||
if token.Kind == dnfpvf.TokenInt {
|
||||
values = append(values, token.Int)
|
||||
}
|
||||
}
|
||||
if len(values) == 0 || len(values)%4 != 0 {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(values); i += 4 {
|
||||
if values[i] <= 0 || values[i+1] < 0 || values[i+2] < 0 || values[i+3] < 0 {
|
||||
return false
|
||||
}
|
||||
result.TournamentStartAreas = append(result.TournamentStartAreas, TournamentStartArea{
|
||||
PartyCount: values[i], X: values[i+1], Y: values[i+2], Direction: values[i+3],
|
||||
})
|
||||
}
|
||||
return len(result.TournamentStartAreas) > 0
|
||||
}
|
||||
|
||||
func parseTileFiles(section RawSection, diagnostics *[]Diagnostic) []TileFile {
|
||||
tokens := values(section)
|
||||
var out []TileFile
|
||||
for index := 0; index < len(tokens); {
|
||||
if index+1 >= len(tokens) || tokens[index].Kind != dnfpvf.TokenInt || !isText(tokens[index+1]) {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, fmt.Sprintf("malformed tile-file pair at token %d", index)))
|
||||
index++
|
||||
continue
|
||||
}
|
||||
out = append(out, TileFile{Index: tokens[index].Int, Path: tokens[index+1].Value})
|
||||
index += 2
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseMapSummons(sections []RawSection, diagnostics *[]Diagnostic) []MapSummon {
|
||||
var out []MapSummon
|
||||
current := -1
|
||||
for _, section := range sections {
|
||||
key := sectionKey(section.Name)
|
||||
if key == "summon" && section.Block {
|
||||
out = append(out, MapSummon{})
|
||||
current = len(out) - 1
|
||||
out[current].SourceSections = append(out[current].SourceSections, cloneRawSection(section))
|
||||
continue
|
||||
}
|
||||
if current < 0 {
|
||||
continue
|
||||
}
|
||||
if key == "/summon" {
|
||||
out[current].SourceSections = append(out[current].SourceSections, cloneRawSection(section))
|
||||
current = -1
|
||||
continue
|
||||
}
|
||||
if isClosingSection(key) {
|
||||
out[current].SourceSections = append(out[current].SourceSections, cloneRawSection(section))
|
||||
continue
|
||||
}
|
||||
if !scopeContains(section.Scope, "summon") {
|
||||
continue
|
||||
}
|
||||
summon := &out[current]
|
||||
summon.SourceSections = append(summon.SourceSections, cloneRawSection(section))
|
||||
extension := structureExtension(section)
|
||||
summon.Extensions = append(summon.Extensions, extension)
|
||||
if scopeContains(section.Scope, "info") || key == "info" {
|
||||
summon.Info = append(summon.Info, extension)
|
||||
}
|
||||
switch key {
|
||||
case "summon key":
|
||||
setOptionalInt(&summon.Key, section, diagnostics)
|
||||
case "position":
|
||||
summon.Position = appendInts(summon.Position, section, diagnostics)
|
||||
case "type":
|
||||
setText(&summon.Type, section, diagnostics)
|
||||
case "itemcount":
|
||||
setOptionalInt(&summon.ItemCount, section, diagnostics)
|
||||
case "index":
|
||||
setOptionalInt(&summon.Index, section, diagnostics)
|
||||
case "life time":
|
||||
setOptionalInt(&summon.LifeTime, section, diagnostics)
|
||||
case "info", "distance":
|
||||
default:
|
||||
summon.UnknownSections = append(summon.UnknownSections, cloneRawSection(section))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func scopeContains(scope []string, name string) bool {
|
||||
want := sectionKey(name)
|
||||
for _, value := range scope {
|
||||
if sectionKey(value) == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setOptionalInt(target *OptionalInt, section RawSection, diagnostics *[]Diagnostic) {
|
||||
value, ok := firstInt(section)
|
||||
if !ok {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "expected integer value"))
|
||||
return
|
||||
}
|
||||
target.Value = value
|
||||
target.Set = true
|
||||
}
|
||||
|
||||
func setText(target *string, section RawSection, diagnostics *[]Diagnostic) {
|
||||
value, ok := firstText(section)
|
||||
if !ok {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "expected text value"))
|
||||
return
|
||||
}
|
||||
*target = value
|
||||
}
|
||||
|
||||
func appendInts(target []int64, section RawSection, diagnostics *[]Diagnostic) []int64 {
|
||||
parsed, valid := ints(section)
|
||||
if !valid {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "contains non-integer values"))
|
||||
}
|
||||
return append(target, parsed...)
|
||||
}
|
||||
|
||||
func parsePoints(section RawSection, diagnostics *[]Diagnostic) []Point {
|
||||
parsed, valid := ints(section)
|
||||
if !valid || len(parsed)%2 != 0 {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "expected x/y integer pairs"))
|
||||
}
|
||||
out := make([]Point, 0, len(parsed)/2)
|
||||
for i := 0; i+1 < len(parsed); i += 2 {
|
||||
out = append(out, Point{X: parsed[i], Y: parsed[i+1]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parsePoints3(section RawSection, diagnostics *[]Diagnostic) []Point3 {
|
||||
parsed, valid := ints(section)
|
||||
if !valid || len(parsed)%3 != 0 {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "expected x/y/z integer records"))
|
||||
}
|
||||
out := make([]Point3, 0, len(parsed)/3)
|
||||
for i := 0; i+2 < len(parsed); i += 3 {
|
||||
out = append(out, Point3{X: parsed[i], Y: parsed[i+1], Z: parsed[i+2]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseAnimationObjects(section RawSection, diagnostics *[]Diagnostic) []AnimationObject {
|
||||
tokens := values(section)
|
||||
var out []AnimationObject
|
||||
for i := 0; i+4 < len(tokens); i += 5 {
|
||||
if !isText(tokens[i]) || !isText(tokens[i+1]) || !areInts(tokens[i+2:i+5]) {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, fmt.Sprintf("malformed animation record at token %d", i)))
|
||||
break
|
||||
}
|
||||
out = append(out, AnimationObject{
|
||||
Filename: tokens[i].Value, Layer: tokens[i+1].Value,
|
||||
Position: Point3{X: tokens[i+2].Int, Y: tokens[i+3].Int, Z: tokens[i+4].Int},
|
||||
})
|
||||
}
|
||||
if len(tokens)%5 != 0 {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "animation section has trailing tokens"))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parsePassiveObjects(section RawSection, diagnostics *[]Diagnostic) []PassiveObject {
|
||||
parsed, valid := ints(section)
|
||||
if !valid || len(parsed)%4 != 0 {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "expected object_id/x/y/flags integer records"))
|
||||
}
|
||||
out := make([]PassiveObject, 0, len(parsed)/4)
|
||||
for i := 0; i+3 < len(parsed); i += 4 {
|
||||
out = append(out, PassiveObject{ObjectID: parsed[i], X: parsed[i+1], Y: parsed[i+2], Flags: parsed[i+3]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseSpecialPassiveObjects(section RawSection, diagnostics *[]Diagnostic) []SpecialPassiveObject {
|
||||
tokens := values(section)
|
||||
if out, ok := parseZeroSpawnSpecialPassiveObjectRows(tokens); ok {
|
||||
return out
|
||||
}
|
||||
if len(tokens)%4 == 0 && areInts(tokens) {
|
||||
out := make([]SpecialPassiveObject, 0, len(tokens)/4)
|
||||
for i := 0; i+3 < len(tokens); i += 4 {
|
||||
out = append(out, SpecialPassiveObject{PassiveObject: PassiveObject{
|
||||
ObjectID: tokens[i].Int, X: tokens[i+1].Int, Y: tokens[i+2].Int, Flags: tokens[i+3].Int,
|
||||
}})
|
||||
}
|
||||
return out
|
||||
}
|
||||
var out []SpecialPassiveObject
|
||||
for i := 0; i < len(tokens); {
|
||||
if i+3 >= len(tokens) || !areInts(tokens[i:i+4]) {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, fmt.Sprintf("malformed special object at token %d", i)))
|
||||
break
|
||||
}
|
||||
obj := SpecialPassiveObject{PassiveObject: PassiveObject{
|
||||
ObjectID: tokens[i].Int, X: tokens[i+1].Int, Y: tokens[i+2].Int, Flags: tokens[i+3].Int,
|
||||
}}
|
||||
i += 4
|
||||
if i >= len(tokens) || tokens[i].Kind != dnfpvf.TokenInt {
|
||||
out = append(out, obj)
|
||||
continue
|
||||
}
|
||||
spawnCount := tokens[i].Int
|
||||
if spawnCount < 0 || spawnCount > int64((len(tokens)-i-1)/6) {
|
||||
out = append(out, obj)
|
||||
continue
|
||||
}
|
||||
i++
|
||||
for n := int64(0); n < spawnCount; n++ {
|
||||
if i < len(tokens) && isHellPartyOpen(tokens[i]) {
|
||||
i++
|
||||
for {
|
||||
if i >= len(tokens) {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "hellparty block is not closed"))
|
||||
return out
|
||||
}
|
||||
if isHellPartyClose(tokens[i]) {
|
||||
i++
|
||||
break
|
||||
}
|
||||
if i+2 >= len(tokens) || !areInts(tokens[i:i+3]) {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "malformed hellparty entry"))
|
||||
return out
|
||||
}
|
||||
obj.HellParty = append(obj.HellParty, HellPartyEntry{
|
||||
GroupID: tokens[i].Int,
|
||||
Rate: tokens[i+1].Int,
|
||||
Order: tokens[i+2].Int,
|
||||
})
|
||||
i += 3
|
||||
}
|
||||
continue
|
||||
}
|
||||
if i+5 >= len(tokens) || !isText(tokens[i]) || !areInts(tokens[i+1:i+6]) {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "malformed special object spawn"))
|
||||
return out
|
||||
}
|
||||
obj.Spawns = append(obj.Spawns, SpecialObjectSpawn{
|
||||
Kind: tokens[i].Value, Code: tokens[i+1].Int, Level: tokens[i+2].Int,
|
||||
Params: [3]int64{tokens[i+3].Int, tokens[i+4].Int, tokens[i+5].Int},
|
||||
})
|
||||
i += 6
|
||||
}
|
||||
out = append(out, obj)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isHellPartyOpen(token dnfpvf.Token) bool {
|
||||
return isText(token) && strings.EqualFold(strings.TrimSpace(token.Value), "[hellparty]")
|
||||
}
|
||||
|
||||
func isHellPartyClose(token dnfpvf.Token) bool {
|
||||
return isText(token) && strings.EqualFold(strings.TrimSpace(token.Value), "[/hellparty]")
|
||||
}
|
||||
|
||||
// Some story maps encode every special-object header as five integers on its
|
||||
// own line, with the fifth integer being a zero child count. When four such
|
||||
// rows are flattened, the token count is also divisible by four; line-aware
|
||||
// recognition must therefore run before the legacy four-field flat grammar.
|
||||
func parseZeroSpawnSpecialPassiveObjectRows(tokens []dnfpvf.Token) ([]SpecialPassiveObject, bool) {
|
||||
if len(tokens) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
var out []SpecialPassiveObject
|
||||
for start := 0; start < len(tokens); {
|
||||
line := tokens[start].Line
|
||||
end := start + 1
|
||||
for end < len(tokens) && tokens[end].Line == line {
|
||||
end++
|
||||
}
|
||||
row := tokens[start:end]
|
||||
if len(row) != 5 || !areInts(row) || row[4].Int != 0 {
|
||||
return nil, false
|
||||
}
|
||||
out = append(out, SpecialPassiveObject{PassiveObject: PassiveObject{
|
||||
ObjectID: row[0].Int,
|
||||
X: row[1].Int,
|
||||
Y: row[2].Int,
|
||||
Flags: row[3].Int,
|
||||
}})
|
||||
start = end
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
func parseMonsters(section RawSection, diagnostics *[]Diagnostic) []MonsterSpawn {
|
||||
tokens := values(section)
|
||||
var out []MonsterSpawn
|
||||
for i := 0; i < len(tokens); {
|
||||
if len(tokens)-i < 10 {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, fmt.Sprintf("monster section has %d trailing tokens at token %d", len(tokens)-i, i)))
|
||||
break
|
||||
}
|
||||
if !areInts(tokens[i:i+8]) || !isText(tokens[i+8]) || !isText(tokens[i+9]) {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, fmt.Sprintf("malformed monster record at token %d", i)))
|
||||
break
|
||||
}
|
||||
spawn := MonsterSpawn{
|
||||
MonsterID: tokens[i].Int, Level: tokens[i+1].Int, AutoLevel: tokens[i+2].Int,
|
||||
Position: Point3{X: tokens[i+3].Int, Y: tokens[i+4].Int, Z: tokens[i+5].Int},
|
||||
RandomDropCount: tokens[i+6].Int, FixedDropCount: tokens[i+7].Int,
|
||||
Placement: tokens[i+8].Value, Rank: tokens[i+9].Value,
|
||||
}
|
||||
i += 10
|
||||
if i < len(tokens) && isMonsterSuffixMarker(tokens[i]) {
|
||||
spawn.SuffixMarker = tokens[i].Value
|
||||
i++
|
||||
}
|
||||
out = append(out, spawn)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isMonsterSuffixMarker(token dnfpvf.Token) bool {
|
||||
return token.Kind == dnfpvf.TokenString && token.Value == "[boss]"
|
||||
}
|
||||
|
||||
func parseNPCs(section RawSection, diagnostics *[]Diagnostic) []NPCSpawn {
|
||||
tokens := values(section)
|
||||
var out []NPCSpawn
|
||||
for i := 0; i < len(tokens); {
|
||||
if i+4 < len(tokens) && tokens[i].Kind == dnfpvf.TokenInt && isText(tokens[i+1]) && areInts(tokens[i+2:i+5]) {
|
||||
out = append(out, NPCSpawn{
|
||||
NPCID: tokens[i].Int, Direction: tokens[i+1].Value,
|
||||
Position: Point3{X: tokens[i+2].Int, Y: tokens[i+3].Int, Z: tokens[i+4].Int},
|
||||
})
|
||||
i += 5
|
||||
continue
|
||||
}
|
||||
if i+3 < len(tokens) && areInts(tokens[i:i+4]) {
|
||||
out = append(out, NPCSpawn{NPCID: tokens[i].Int, Position: Point3{X: tokens[i+1].Int, Y: tokens[i+2].Int, Z: tokens[i+3].Int}})
|
||||
i += 4
|
||||
continue
|
||||
}
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, fmt.Sprintf("malformed npc record at token %d", i)))
|
||||
break
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseAICharacters(section RawSection, diagnostics *[]Diagnostic) []AICharacter {
|
||||
tokens := values(section)
|
||||
var out []AICharacter
|
||||
for i := 0; i < len(tokens); {
|
||||
if i+5 >= len(tokens) || !areInts(tokens[i:i+4]) || !isText(tokens[i+4]) || !isText(tokens[i+5]) {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, fmt.Sprintf("malformed ai character at token %d", i)))
|
||||
break
|
||||
}
|
||||
entry := AICharacter{
|
||||
Code: tokens[i].Int, Position: Point{X: tokens[i+1].Int, Y: tokens[i+2].Int},
|
||||
Direction: tokens[i+3].Int, Faction: tokens[i+4].Value, AIType: tokens[i+5].Value,
|
||||
}
|
||||
i += 6
|
||||
for len(entry.Params) < 2 && i < len(tokens) && tokens[i].Kind == dnfpvf.TokenInt {
|
||||
entry.Params = append(entry.Params, tokens[i].Int)
|
||||
i++
|
||||
}
|
||||
out = append(out, entry)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseMovableAreas(section RawSection, diagnostics *[]Diagnostic) []MovableArea {
|
||||
parsed, valid := ints(section)
|
||||
if !valid || len(parsed)%6 != 0 {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "expected six-integer movable-area records"))
|
||||
}
|
||||
out := make([]MovableArea, 0, len(parsed)/6)
|
||||
for i := 0; i+5 < len(parsed); i += 6 {
|
||||
out = append(out, MovableArea{
|
||||
X: parsed[i], Y: parsed[i+1], Width: parsed[i+2], Height: parsed[i+3],
|
||||
Params: [2]int64{parsed[i+4], parsed[i+5]},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseDungeonMovableAreas(section RawSection, diagnostics *[]Diagnostic) []MovableArea {
|
||||
parsed, valid := ints(section)
|
||||
if !valid || len(parsed)%4 != 0 {
|
||||
*diagnostics = append(*diagnostics, diagnostic(section, "expected four-integer dungeon movable-area records"))
|
||||
}
|
||||
out := make([]MovableArea, 0, len(parsed)/4)
|
||||
for i := 0; i+3 < len(parsed); i += 4 {
|
||||
out = append(out, MovableArea{X: parsed[i], Y: parsed[i+1], Width: parsed[i+2], Height: parsed[i+3]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func attachHellParty(sections []RawSection, result *Map) {
|
||||
for _, section := range sections {
|
||||
if sectionKey(section.Name) != "hellparty" || len(result.SpecialPassiveObjects) == 0 {
|
||||
continue
|
||||
}
|
||||
parsed, valid := ints(section)
|
||||
if !valid || len(parsed)%3 != 0 {
|
||||
result.Diagnostics = append(result.Diagnostics, diagnostic(section, "expected group/rate/order records"))
|
||||
}
|
||||
last := &result.SpecialPassiveObjects[len(result.SpecialPassiveObjects)-1]
|
||||
for i := 0; i+2 < len(parsed); i += 3 {
|
||||
last.HellParty = append(last.HellParty, HellPartyEntry{GroupID: parsed[i], Rate: parsed[i+1], Order: parsed[i+2]})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func buildPortals(positions []Point, objects []int64) []Portal {
|
||||
count := max(len(positions), len(objects))
|
||||
out := make([]Portal, count)
|
||||
for i := 0; i < count; i++ {
|
||||
out[i].Slot = i
|
||||
if i < len(positions) {
|
||||
position := positions[i]
|
||||
out[i].Position = &position
|
||||
}
|
||||
if i < len(objects) {
|
||||
out[i].ObjectID = OptionalInt{Value: objects[i], Set: true}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func setMapFlag(flags *MapFlags, key string) {
|
||||
switch key {
|
||||
case "block use stackable item":
|
||||
flags.BlockUseStackableItem = true
|
||||
case "block use active skill":
|
||||
flags.BlockUseActiveSkill = true
|
||||
case "visible on dungeon clear":
|
||||
flags.VisibleOnDungeonClear = true
|
||||
case "loop y axis":
|
||||
flags.LoopYAxis = true
|
||||
case "all dead case passable":
|
||||
flags.AllDeadCasePassable = true
|
||||
case "disable item escape stuck":
|
||||
flags.DisableItemEscapeStuck = true
|
||||
case "disable character escape stuck":
|
||||
flags.DisableCharacterEscapeStuck = true
|
||||
case "cannot use coin map":
|
||||
flags.CannotUseCoinMap = true
|
||||
case "no revival timer limit":
|
||||
flags.NoRevivalTimerLimit = true
|
||||
case "ignore diehard":
|
||||
flags.IgnoreDiehard = true
|
||||
case "disable rebirth":
|
||||
flags.DisableRebirth = true
|
||||
case "preserve player corpse":
|
||||
flags.PreservePlayerCorpse = true
|
||||
case "cannot use resolution change zoom":
|
||||
flags.CannotUseResolutionChangeZoom = true
|
||||
case "center fixed camera":
|
||||
flags.CenterFixedCamera = true
|
||||
case "force draw pattern":
|
||||
flags.ForceDrawPattern = true
|
||||
case "is revival":
|
||||
flags.IsRevival = true
|
||||
case "is moive end", "is movie end":
|
||||
flags.IsMovieEnd = true
|
||||
case "quest start map":
|
||||
flags.QuestStartMap = true
|
||||
case "hide monster":
|
||||
flags.HideMonster = true
|
||||
case "show dust":
|
||||
flags.ShowDust = true
|
||||
}
|
||||
}
|
||||
|
||||
func isText(token dnfpvf.Token) bool {
|
||||
return token.Kind == dnfpvf.TokenString || token.Kind == dnfpvf.TokenIdent
|
||||
}
|
||||
|
||||
func areInts(tokens []dnfpvf.Token) bool {
|
||||
for _, token := range tokens {
|
||||
if token.Kind != dnfpvf.TokenInt {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func cloneRawSection(section RawSection) RawSection {
|
||||
section.Tokens = append([]dnfpvf.Token(nil), section.Tokens...)
|
||||
section.Scope = append([]string(nil), section.Scope...)
|
||||
return section
|
||||
}
|
||||
Reference in new issue
Block a user