377 lines
8.4 KiB
Go
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)))
|
|
}
|