Files
dcalc/backend/go/internal/service/skilltree.go
T
2026-04-02 15:34:18 +08:00

377 lines
8.4 KiB
Go

package service
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"unicode/utf16"
)
type SkillTreeService struct {
adventure *AdventureService
factory CharacterFactory
indexToUID map[int][]string
}
type decodedSkillCode struct {
Header skillCodeHeader
Skills []skillLearnItem
Evolutions []skillChoice
Enhancements []skillChoice
}
type skillCodeHeader struct {
Name string
Level int
JobBytes [2]int
}
type skillLearnItem struct {
SkillIndex int
Level int
SkillType int
}
type skillChoice struct {
SkillIndex int
Type int
}
type LearnedSkill struct {
Lv int `json:"lv"`
Up int `json:"up"`
VP int `json:"vp"`
Name string `json:"name"`
}
type SkillTreeResult struct {
Name string `json:"name"`
Level int `json:"level"`
Job map[string]string `json:"job"`
Skills map[string]LearnedSkill `json:"skills"`
}
var roleMap = map[int]int{
0: 0,
1: 3,
2: 4,
3: 7,
4: 8,
5: 5,
6: 10,
7: 2,
8: 6,
9: 15,
10: 15,
11: 1,
12: 11,
13: 12,
14: 9,
15: 13,
16: 14,
}
func NewSkillTreeService(projectRoot string, adventure *AdventureService, factory CharacterFactory) (*SkillTreeService, error) {
indexToUID, err := loadSkillIndexMap(filepath.Join(projectRoot, "dataFiles", "skillMap.json"))
if err != nil {
return nil, err
}
return &SkillTreeService{adventure: adventure, factory: factory, indexToUID: indexToUID}, nil
}
func (s *SkillTreeService) Parse(code string, equVersion string) (SkillTreeResult, error) {
decoded, err := decodeSkillCode(code)
if err != nil {
return SkillTreeResult{}, err
}
alter, err := alterFromJobBytes(decoded.Header.JobBytes[0], decoded.Header.JobBytes[1], s.adventure)
if err != nil {
return SkillTreeResult{}, err
}
skills, err := s.factory.CharacterSkills(alter, equVersion)
if err != nil {
return SkillTreeResult{}, err
}
skillByUUID := make(map[string][]CharacterSkillMeta)
for _, skill := range skills {
skillByUUID[skill.UUID] = append(skillByUUID[skill.UUID], skill)
}
indexCandidates := make(map[int][]CharacterSkillMeta)
for _, item := range decoded.Skills {
if _, exists := indexCandidates[item.SkillIndex]; exists {
continue
}
indexCandidates[item.SkillIndex] = s.buildCandidates(item.SkillIndex, skillByUUID)
}
learned := make(map[string]LearnedSkill)
for _, item := range decoded.Skills {
candidates := indexCandidates[item.SkillIndex]
chosen := chooseSkillCandidate(candidates, item.SkillType)
if chosen == nil {
continue
}
current, exists := learned[chosen.ID]
if !exists || current.Lv < item.Level {
learned[chosen.ID] = LearnedSkill{Lv: item.Level, Up: 0, VP: 0, Name: chosen.Name}
}
}
for _, evo := range decoded.Evolutions {
for _, skill := range indexCandidates[evo.SkillIndex] {
if !skill.HasVP {
continue
}
if learnedSkill, ok := learned[skill.ID]; ok {
learnedSkill.VP = evo.Type
learned[skill.ID] = learnedSkill
}
}
}
for _, up := range decoded.Enhancements {
for _, skill := range indexCandidates[up.SkillIndex] {
if !skill.HasUP {
continue
}
if learnedSkill, ok := learned[skill.ID]; ok {
learnedSkill.Up = up.Type
learned[skill.ID] = learnedSkill
}
}
}
for _, skill := range skills {
if _, exists := learned[skill.ID]; exists {
continue
}
level := 0
if skill.LearnLv == 50 || skill.LearnLv == 85 || skill.Name == "基础精通" {
level = skill.MaxLearnLv
}
learned[skill.ID] = LearnedSkill{Lv: level, Up: 0, VP: 0, Name: skill.Name}
}
return SkillTreeResult{
Name: decoded.Header.Name,
Level: decoded.Header.Level,
Job: map[string]string{"class": alter},
Skills: learned,
}, nil
}
func (s *SkillTreeService) buildCandidates(index int, skillByUUID map[string][]CharacterSkillMeta) []CharacterSkillMeta {
uuids := s.indexToUID[index]
seen := make(map[string]struct{})
candidates := make([]CharacterSkillMeta, 0)
for _, uuid := range uuids {
for _, skill := range skillByUUID[uuid] {
if _, exists := seen[skill.ID]; exists {
continue
}
seen[skill.ID] = struct{}{}
candidates = append(candidates, skill)
}
}
if len(candidates) > 1 {
filtered := make([]CharacterSkillMeta, 0)
for _, skill := range candidates {
if skill.LearnLv == 50 || skill.LearnLv == 85 {
continue
}
filtered = append(filtered, skill)
}
if len(filtered) > 0 {
return filtered
}
}
return candidates
}
func chooseSkillCandidate(candidates []CharacterSkillMeta, skillType int) *CharacterSkillMeta {
if len(candidates) == 0 {
return nil
}
if len(candidates) == 1 {
return &candidates[0]
}
wantedType := "passive"
if skillType == 1 {
wantedType = "active"
}
for i := range candidates {
if candidates[i].Type == wantedType {
return &candidates[i]
}
}
return &candidates[0]
}
func loadSkillIndexMap(path string) (map[int][]string, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
uuidToIndexes := map[string][]int{}
if err := json.Unmarshal(content, &uuidToIndexes); err != nil {
return nil, err
}
indexToUUID := make(map[int][]string)
for uuid, indexes := range uuidToIndexes {
for _, idx := range indexes {
indexToUUID[idx] = append(indexToUUID[idx], uuid)
}
}
return indexToUUID, nil
}
func alterFromJobBytes(roleByte int, jobByte int, adventure *AdventureService) (string, error) {
parent, ok := roleMap[roleByte]
if !ok {
return "", fmt.Errorf("unsupported role byte: %d", roleByte)
}
jobID := jobByte - 1
if roleByte == 9 {
jobID = 1
}
if roleByte == 10 {
jobID = 0
}
token := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%02d%02d00", parent, jobID)))
return adventure.AlterByToken(token)
}
func decodeSkillCode(encoded string) (decodedSkillCode, error) {
result := decodedSkillCode{}
compressed, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return result, fmt.Errorf("decode base64: %w", err)
}
r, err := zlib.NewReader(bytes.NewReader(compressed))
if err != nil {
return result, fmt.Errorf("create zlib reader: %w", err)
}
defer r.Close()
raw, err := io.ReadAll(r)
if err != nil {
return result, fmt.Errorf("read zlib stream: %w", err)
}
if len(raw) < 14 {
return result, fmt.Errorf("invalid skill code payload")
}
nameLen := int(raw[13])
if 14+nameLen > len(raw) {
return result, fmt.Errorf("invalid name length in skill code")
}
result.Header = skillCodeHeader{
Name: decodeUTF16LE(raw[14 : 14+nameLen]),
Level: int(raw[5]),
JobBytes: [2]int{int(raw[2]), int(raw[3])},
}
data := raw[14+nameLen:]
if len(data)%2 == 1 {
data = append(data, 0)
}
uint16s := make([]uint16, 0, len(data)/2)
for i := 0; i < len(data); i += 2 {
uint16s = append(uint16s, binary.LittleEndian.Uint16(data[i:i+2]))
}
if len(uint16s) == 0 {
return result, nil
}
high := make([]byte, len(uint16s))
low := make([]byte, len(uint16s))
for i, value := range uint16s {
high[i] = byte(value >> 8)
low[i] = byte(value & 0xFF)
}
pairCount := int(high[0])
for i := 0; i < pairCount; i++ {
base := 1 + i*2
if base+1 >= len(high) {
break
}
result.Skills = append(result.Skills, skillLearnItem{
SkillIndex: int(high[base]),
Level: int(high[base+1]),
SkillType: int(low[base+1]),
})
}
sectionStart := (1 + pairCount*2) * 2
if sectionStart >= len(data) {
return result, nil
}
sec2 := data[sectionStart:]
if len(sec2) < 2 {
return result, nil
}
offset := 1
evoCount := int(sec2[offset])
offset++
for i := 0; i < evoCount; i++ {
if offset+3 > len(sec2) {
break
}
result.Evolutions = append(result.Evolutions, skillChoice{
SkillIndex: int(sec2[offset]),
Type: int(sec2[offset+2]) + 1,
})
offset += 3
}
if offset >= len(sec2) {
return result, nil
}
enhCount := int(sec2[offset])
offset++
for i := 0; i < enhCount; i++ {
if offset+3 > len(sec2) {
break
}
result.Enhancements = append(result.Enhancements, skillChoice{
SkillIndex: int(sec2[offset]),
Type: int(sec2[offset+2]) + 1,
})
offset += 3
}
return result, nil
}
func decodeUTF16LE(value []byte) string {
if len(value)%2 == 1 {
value = append(value, 0)
}
words := make([]uint16, 0, len(value)/2)
for i := 0; i < len(value); i += 2 {
words = append(words, binary.LittleEndian.Uint16(value[i:i+2]))
}
return strings.TrimSpace(string(utf16.Decode(words)))
}