package dnfbridge import ( "bytes" "encoding/json" "net/url" "strconv" "strings" dnfcompanion "longheng.io/server/internal/modules/dnf/companion" ) // 本文件提供 GM 工具(GmTool90CN v2「魔改 / 支援兵」页)所用的 // **camelCase + 字符串 ID** 线协议解析。 // // 为什么需要一层转换 // 服务端内部(以及 Server-90 的 /local/companion)用的是 snake_case 与数字 ID: // { "character_id": 1, "source_character_ids": [3, 11] } // 而 GM 工具前端发的是 camelCase 与字符串 ID(浏览器的角色列表来自另一套 // JSON,ID 是字符串): // { "characterId": "1", "sourceCharacterIds": ["3","11"] } // // 这里做一层**宽松解析**:两种写法都接受。解析结果喂给原来的业务函数, // 业务语义完全不变。 // companionFlexUint16 接受 JSON 数字、数字字符串或空串(视为 0)。 type companionFlexUint16 uint16 func (v *companionFlexUint16) UnmarshalJSON(raw []byte) error { if v == nil { return nil } raw = bytes.TrimSpace(raw) if len(raw) == 0 || bytes.Equal(raw, []byte("null")) { *v = 0 return nil } if raw[0] == '"' { var text string if err := json.Unmarshal(raw, &text); err != nil { return err } text = strings.TrimSpace(text) if text == "" { *v = 0 return nil } parsed, err := strconv.ParseUint(text, 10, 16) if err != nil { return err } *v = companionFlexUint16(parsed) return nil } var number uint64 if err := json.Unmarshal(raw, &number); err != nil { return err } *v = companionFlexUint16(number) return nil } // companionFlexUint16Slice 接受 ["3",11] 这类混合数组。 type companionFlexUint16Slice []uint16 func (s *companionFlexUint16Slice) UnmarshalJSON(raw []byte) error { if s == nil { return nil } raw = bytes.TrimSpace(raw) if len(raw) == 0 || bytes.Equal(raw, []byte("null")) { *s = nil return nil } var items []json.RawMessage if err := json.Unmarshal(raw, &items); err != nil { return err } out := make([]uint16, 0, len(items)) for _, item := range items { var value companionFlexUint16 if err := value.UnmarshalJSON(item); err != nil { return err } out = append(out, uint16(value)) } *s = out return nil } // companionToolRequest 是解析后的统一请求(与工具前端的字段一一对应)。 type CompanionToolRequest struct { CharacterID uint16 ExpectedRevision uint64 Enabled bool BalanceEnabled bool ProgressionEnabled bool RandomEquipmentEnabled bool AI dnfcompanion.AIConfig AIProfiles map[string]dnfcompanion.AIProfile SourceCharacterID uint16 SourceCharacterIDs []uint16 } // companionToolWireRequest 同时声明 camelCase 与 snake_case 两组标签。 // // encoding/json 在同一个字段上只认一个标签,所以这里对每个字段声明两个 // 并列字段(camel 与 snake),解析后由 decodeCompanionToolRequest 合并: // 优先取非零的那个。这样两种客户端都能工作。 type companionToolWireRequest struct { CharacterIDcamel companionFlexUint16 `json:"characterId"` CharacterIDsnake companionFlexUint16 `json:"character_id"` Revisioncamel uint64 `json:"expectedRevision"` Revisionsnake uint64 `json:"expected_revision"` Enabled bool `json:"enabled"` Balancecamel bool `json:"balanceEnabled"` Balancesnake bool `json:"balance_enabled"` Progressioncamel bool `json:"progressionEnabled"` ProgressionSnake bool `json:"progression_enabled"` RandomEquipcamel bool `json:"randomEquipmentEnabled"` RandomEquipSnake bool `json:"random_equipment_enabled"` AI dnfcompanion.AIConfig `json:"ai"` AIProfilescamel map[string]dnfcompanion.AIProfile `json:"aiProfiles"` AIProfilessnake map[string]dnfcompanion.AIProfile `json:"ai_profiles"` SourceIDcamel companionFlexUint16 `json:"sourceCharacterId"` SourceIDsnake companionFlexUint16 `json:"source_character_id"` SourceIDscamel companionFlexUint16Slice `json:"sourceCharacterIds"` SourceIDssnake companionFlexUint16Slice `json:"source_character_ids"` } // DecodeCompanionToolRequest 解析 GM 工具页面的请求体(同时接受 camelCase 与 snake_case)。 func DecodeCompanionToolRequest(body []byte) (CompanionToolRequest, error) { var request CompanionToolRequest if len(bytes.TrimSpace(body)) == 0 { return request, nil } var wire companionToolWireRequest if err := json.Unmarshal(body, &wire); err != nil { return CompanionToolRequest{}, err } request.CharacterID = firstNonZeroUint16(uint16(wire.CharacterIDcamel), uint16(wire.CharacterIDsnake)) request.ExpectedRevision = wire.Revisioncamel if request.ExpectedRevision == 0 { request.ExpectedRevision = wire.Revisionsnake } request.Enabled = wire.Enabled request.BalanceEnabled = wire.Balancecamel || wire.Balancesnake request.ProgressionEnabled = wire.Progressioncamel || wire.ProgressionSnake request.RandomEquipmentEnabled = wire.RandomEquipcamel || wire.RandomEquipSnake request.AI = wire.AI if len(wire.AIProfilescamel) != 0 { request.AIProfiles = wire.AIProfilescamel } else { request.AIProfiles = wire.AIProfilessnake } request.SourceCharacterID = firstNonZeroUint16(uint16(wire.SourceIDcamel), uint16(wire.SourceIDsnake)) if len(wire.SourceIDscamel) != 0 { request.SourceCharacterIDs = wire.SourceIDscamel } else { request.SourceCharacterIDs = wire.SourceIDssnake } return request, nil } func firstNonZeroUint16(values ...uint16) uint16 { for _, value := range values { if value != 0 { return value } } return 0 } // decodeCompanionToolCharacterID 从查询串里取 character_id 或 characterId。 func DecodeCompanionToolCharacterID(rawQuery string) (uint16, bool) { values, err := url.ParseQuery(rawQuery) if err != nil { return 0, false } for _, key := range []string{"characterId", "character_id"} { text := strings.TrimSpace(values.Get(key)) if text == "" { continue } parsed, err := strconv.ParseUint(text, 10, 16) if err != nil || parsed == 0 { return 0, false } return uint16(parsed), true } return 0, false }