package service import ( "context" "encoding/json" "fmt" "log" "time" "equipment-analyzer/internal/capture" "equipment-analyzer/internal/config" "equipment-analyzer/internal/model" "equipment-analyzer/internal/optimizer" "equipment-analyzer/internal/utils" "github.com/wailsapp/wails/v2/pkg/runtime" ) type App struct { ctx context.Context config *config.Config logger *utils.Logger captureService *CaptureService parserService *ParserService database *model.Database databaseService *DatabaseService } func NewApp(cfg *config.Config, logger *utils.Logger) *App { // init database database, err := model.NewDatabase() if err != nil { logger.Error("database init failed", "error", err) // allow app to run without db features parserService := NewParserService(cfg, logger) return &App{ config: cfg, logger: logger, captureService: NewCaptureService(cfg, logger, parserService), parserService: parserService, } } databaseService := NewDatabaseService(database, logger) parserService := NewParserService(cfg, logger) return &App{ config: cfg, logger: logger, captureService: NewCaptureService(cfg, logger, parserService), parserService: parserService, database: database, databaseService: databaseService, } } func (a *App) Startup(ctx context.Context) { a.ctx = ctx a.captureService.SetBeforeRemote(func() { if a.ctx != nil { runtime.EventsEmit(a.ctx, "capture:ready_to_parse") } }) a.logger.Info("app startup") } func (a *App) DomReady(ctx context.Context) { a.logger.Info("dom ready") } func (a *App) BeforeClose(ctx context.Context) (prevent bool) { a.logger.Info("app closing") return false } func (a *App) Shutdown(ctx context.Context) { a.logger.Info("app shutdown") if a.database != nil { if err := a.database.Close(); err != nil { a.logger.Error("failed to close database connection", "error", err) } else { a.logger.Info("database connection closed") } } } // OptimizeBuilds runs the optimizer on latest parsed data. func (a *App) OptimizeBuilds(req model.OptimizeRequest) (*model.OptimizeResponse, error) { log.Printf("[service] OptimizeBuilds entry heroId=%s", req.HeroID) parsedResult, err := a.GetLatestParsedDataFromDatabase() if err != nil { log.Printf("[service] OptimizeBuilds get data failed: %v", err) return nil, err } if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) { log.Printf("[service] OptimizeBuilds no data items=%d heroes=%d", len(parsedResult.Items), len(parsedResult.Heroes)) return &model.OptimizeResponse{TotalCombos: 0, Results: []model.OptimizeResult{}}, nil } items, err := optimizer.ParseItems(parsedResult.Items) if err != nil { log.Printf("[service] OptimizeBuilds parse items failed: %v", err) return nil, fmt.Errorf("parse items failed: %w", err) } logMissingSetStats(parsedResult.Items) heroes, err := optimizer.ParseHeroes(parsedResult.Heroes) if err != nil { log.Printf("[service] OptimizeBuilds parse heroes failed: %v", err) return nil, fmt.Errorf("parse heroes failed: %w", err) } templates, err := a.parserService.GetHeroTemplates() if err != nil { log.Printf("[service] OptimizeBuilds hero templates missing: %v", err) return nil, fmt.Errorf("未读取到英雄信息数据") } templateHeroes := make([]model.OptimizeHero, 0, len(templates)) for _, t := range templates { templateHeroes = append(templateHeroes, model.OptimizeHero{ Code: t.Code, Name: t.Name, BaseAtk: t.BaseAtk, BaseDef: t.BaseDef, BaseHp: t.BaseHp, BaseSpd: t.BaseSpd, BaseCr: t.BaseCr, BaseCd: t.BaseCd, BaseEff: t.BaseEff, BaseRes: t.BaseRes, Atk: t.BaseAtk, Def: t.BaseDef, Hp: t.BaseHp, Spd: t.BaseSpd, Cr: t.BaseCr, Cd: t.BaseCd, Eff: t.BaseEff, Res: t.BaseRes, }) } // Use template heroes for optimizer to match the template-based selection. heroes = templateHeroes log.Printf("[service] OptimizeBuilds parsed items=%d heroes=%d", len(items), len(heroes)) resp, err := optimizer.Optimize(items, heroes, req) if err != nil { log.Printf("[service] OptimizeBuilds optimize failed: %v", err) return nil, err } log.Printf("[service] OptimizeBuilds done results=%d totalCombos=%d", len(resp.Results), resp.TotalCombos) return resp, nil } // GetNetworkInterfaces returns available network interfaces. func (a *App) GetNetworkInterfaces() ([]model.NetworkInterface, error) { interfaces, err := capture.GetNetworkInterfaces() if err != nil { a.logger.Error("get network interfaces failed", "error", err) return nil, err } return interfaces, nil } // GetHeroTemplates returns template heroes from local herodata.json. func (a *App) GetHeroTemplates() ([]model.HeroTemplate, error) { return a.parserService.GetHeroTemplates() } // StartCapture starts capture on the given interface. func (a *App) StartCapture(interfaceName string) error { if a.captureService.IsCapturing() { return fmt.Errorf("capture already running") } timeoutMs := a.config.Capture.DefaultTimeout if timeoutMs > 500 { log.Printf("[service] capture timeout too high (%dms), clamp to 500ms for responsive stop", timeoutMs) timeoutMs = 500 } config := capture.Config{ InterfaceName: interfaceName, Filter: a.config.Capture.DefaultFilter, Timeout: time.Duration(timeoutMs) * time.Millisecond, BufferSize: a.config.Capture.BufferSize, } err := a.captureService.StartCaptureAsync(context.Background(), config, func() { if a.ctx != nil { runtime.EventsEmit(a.ctx, "capture:started") } }, func(err error) { a.logger.Error("start capture failed", "error", err) if a.ctx != nil { runtime.EventsEmit(a.ctx, "capture:start_failed", err.Error()) } }) if err != nil { return err } a.logger.Info("capture start requested", "interface", interfaceName) return nil } // StopCapture stops capture. func (a *App) StopCapture() error { if !a.captureService.IsCapturing() { return fmt.Errorf("capture not running") } err := a.captureService.StopCapture() if err != nil { a.logger.Error("stop capture failed", "error", err) return err } a.captureService.ProcessAllData() a.logger.Info("capture stopped") return nil } // GetCapturedData returns raw captured data. func (a *App) GetCapturedData() ([]string, error) { return a.captureService.GetCapturedData(), nil } // ParseData parses captured data to JSON (remote parser). func (a *App) ParseData(hexDataList []string) (string, error) { _, rawJson, err := a.parserService.ParseHexData(hexDataList) if err != nil { a.logger.Error("parse data failed", "error", err) return "", err } return rawJson, nil } // ExportData exports data to a file. func (a *App) ExportData(hexDataList []string, filename string) error { result, rawJson, err := a.parserService.ParseHexData(hexDataList) if err != nil { a.logger.Error("parse data failed", "error", err) return err } a.logger.Info("export data", "filename", filename, "count", len(result.Items)) err = utils.WriteFile(filename, []byte(rawJson)) if err != nil { a.logger.Error("write file failed", "error", err) return err } return nil } // ExportCurrentData exports latest data from database to a file. func (a *App) ExportCurrentData(filename string) error { if a.databaseService == nil { return fmt.Errorf("database service not initialized") } parsedResult, err := a.GetLatestParsedDataFromDatabase() if err != nil { a.logger.Error("failed to load data from database", "error", err) return err } if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) { return fmt.Errorf("no data to export") } exportData := map[string]interface{}{ "items": parsedResult.Items, "heroes": parsedResult.Heroes, } jsonData, err := json.MarshalIndent(exportData, "", " ") if err != nil { a.logger.Error("failed to marshal data", "error", err) return err } err = utils.WriteFile(filename, jsonData) if err != nil { a.logger.Error("write file failed", "error", err) return err } a.logger.Info("export current data succeeded", "filename", filename, "items_count", len(parsedResult.Items), "heroes_count", len(parsedResult.Heroes)) return nil } // GetCurrentDataForExport returns latest data from database as JSON string. func (a *App) GetCurrentDataForExport() (string, error) { if a.databaseService == nil { return "", fmt.Errorf("database service not initialized") } parsedResult, err := a.GetLatestParsedDataFromDatabase() if err != nil { a.logger.Error("failed to load data from database", "error", err) return "", err } if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) { return "", fmt.Errorf("no data to export") } exportData := map[string]interface{}{ "items": parsedResult.Items, "heroes": parsedResult.Heroes, } jsonData, err := json.MarshalIndent(exportData, "", " ") if err != nil { a.logger.Error("failed to marshal data", "error", err) return "", err } return string(jsonData), nil } // GetCaptureStatus returns capture status. func (a *App) GetCaptureStatus() model.CaptureStatus { return model.CaptureStatus{ IsCapturing: a.captureService.IsCapturing(), Status: a.getStatusMessage(), } } func (a *App) getStatusMessage() string { if a.captureService.IsCapturing() { return "capturing" } if a.captureService.IsStarting() { return "starting" } return "ready" } // ReadRawJsonFile is deprecated; use GetLatestParsedDataFromDatabase. func (a *App) ReadRawJsonFile() (*model.ParsedResult, error) { return a.GetLatestParsedDataFromDatabase() } // StopAndParseCapture stops capture and parses data. func (a *App) StopAndParseCapture() (*model.ParsedResult, error) { log.Printf("[service] StopAndParseCapture entry") result, err := a.captureService.StopAndParseCapture() if err != nil { a.logger.Error("stop and parse capture failed", "error", err) return nil, err } if a.databaseService != nil && result != nil { itemsJSON := "[]" if result.Items != nil { if jsonData, err := json.Marshal(result.Items); err == nil { itemsJSON = string(jsonData) } } heroesJSON := "[]" if result.Heroes != nil { if jsonData, err := json.Marshal(result.Heroes); err == nil { heroesJSON = string(jsonData) } } sessionName := fmt.Sprintf("capture_%d", time.Now().Unix()) if err := a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON); err != nil { a.logger.Error("save parsed data failed", "error", err) } else { a.logger.Info("parsed data saved", "session_name", sessionName) } } return result, nil } // SaveParsedDataToDatabase saves parsed data. func (a *App) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error { if a.databaseService == nil { return fmt.Errorf("database service not initialized") } return a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON) } // GetLatestParsedDataFromDatabase returns latest parsed data from database. func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) { if a.databaseService == nil { return nil, fmt.Errorf("database service not initialized") } itemsJSON, heroesJSON, err := a.databaseService.GetLatestParsedDataFromDatabase() if err != nil { return nil, err } var items []interface{} if itemsJSON != "" { if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil { return nil, fmt.Errorf("failed to unmarshal items: %w", err) } } logMissingSetStats(items) var heroes []interface{} if heroesJSON != "" { if err := json.Unmarshal([]byte(heroesJSON), &heroes); err != nil { return nil, fmt.Errorf("failed to unmarshal heroes: %w", err) } } return &model.ParsedResult{ Items: items, Heroes: heroes, }, nil } // GetParsedSessions returns all parsed sessions. func (a *App) GetParsedSessions() ([]model.ParsedSession, error) { if a.databaseService == nil { return nil, fmt.Errorf("database service not initialized") } return a.databaseService.GetParsedSessions() } // GetParsedDataByID returns parsed data by session id. func (a *App) GetParsedDataByID(id int64) (*model.ParsedResult, error) { if a.databaseService == nil { return nil, fmt.Errorf("database service not initialized") } itemsJSON, heroesJSON, err := a.databaseService.GetParsedDataByID(id) if err != nil { return nil, err } var items []interface{} if itemsJSON != "" { if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil { return nil, fmt.Errorf("failed to unmarshal items: %w", err) } } var heroes []interface{} if heroesJSON != "" { if err := json.Unmarshal([]byte(heroesJSON), &heroes); err != nil { return nil, fmt.Errorf("failed to unmarshal heroes: %w", err) } } return &model.ParsedResult{ Items: items, Heroes: heroes, }, nil } // UpdateParsedSessionName updates session name. func (a *App) UpdateParsedSessionName(id int64, name string) error { if a.databaseService == nil { return fmt.Errorf("database service not initialized") } if name == "" { return fmt.Errorf("session name cannot be empty") } return a.databaseService.UpdateParsedSessionName(id, name) } // DeleteParsedSession deletes a parsed session by id. func (a *App) DeleteParsedSession(id int64) error { if a.databaseService == nil { return fmt.Errorf("database service not initialized") } return a.databaseService.DeleteParsedSession(id) } // SaveAppSetting saves app setting. func (a *App) SaveAppSetting(key, value string) error { if a.databaseService == nil { return fmt.Errorf("database service not initialized") } return a.databaseService.SaveAppSetting(key, value) } // GetAppSetting gets app setting. func (a *App) GetAppSetting(key string) (string, error) { if a.databaseService == nil { return "", fmt.Errorf("database service not initialized") } return a.databaseService.GetAppSetting(key) } // GetAllAppSettings gets all app settings. func (a *App) GetAllAppSettings() (map[string]string, error) { if a.databaseService == nil { return nil, fmt.Errorf("database service not initialized") } return a.databaseService.GetAllAppSettings() } func logMissingSetStats(items []interface{}) { if len(items) == 0 { return } var total int var emptyF int var emptySet int var healthF int var healthSet int var mapHp int setCounts := map[string]int{} uncategorized := 0 emptySetByF := map[string]int{} emptySetByGear := map[string]int{} ingameSetMap := map[string]string{ "set_acc": "HitSet", "set_att": "AttackSet", "set_coop": "UnitySet", "set_counter": "CounterSet", "set_cri_dmg": "DestructionSet", "set_cri": "CriticalSet", "set_def": "DefenseSet", "set_immune": "ImmunitySet", "set_max_hp": "HealthSet", "set_penetrate": "PenetrationSet", "set_rage": "RageSet", "set_res": "ResistSet", "set_revenge": "RevengeSet", "set_scar": "InjurySet", "set_speed": "SpeedSet", "set_vampire": "LifestealSet", "set_shield": "ProtectionSet", "set_torrent": "TorrentSet", "set_revenant": "ReversalSet", "set_riposte": "RiposteSet", "set_chase": "PursuitSet", "set_opener": "WarfareSet", } for _, raw := range items { item, ok := raw.(map[string]interface{}) if !ok { continue } total++ if f, ok := item["f"]; !ok || f == nil || f == "" { emptyF++ } else { if f == "set_max_hp" { healthF++ } } setValue, _ := item["set"].(string) if setValue == "" { emptySet++ fValue, _ := item["f"].(string) if fValue != "" { emptySetByF[fValue] = emptySetByF[fValue] + 1 } if gear, ok := item["gear"].(string); ok && gear != "" { emptySetByGear[gear] = emptySetByGear[gear] + 1 } if fValue != "" { if mapped, ok := ingameSetMap[fValue]; ok { setCounts[mapped] = setCounts[mapped] + 1 } else { uncategorized++ } } else { uncategorized++ } } else { setCounts[setValue] = setCounts[setValue] + 1 } if set, ok := item["set"].(string); ok && set == "HealthSet" { healthSet++ } if f, ok := item["f"]; ok && f == "set_max_hp" { mapHp++ } } log.Printf("[service] set stats: total=%d empty_f=%d empty_set=%d set_max_hp=%d HealthSet=%d mapped_hp=%d", total, emptyF, emptySet, healthF, healthSet, mapHp) log.Printf("[service] set counts: %+v", setCounts) log.Printf("[service] uncategorized items: %d", uncategorized) if emptySet > 0 { log.Printf("[service] empty set by f: %+v", emptySetByF) log.Printf("[service] empty set by gear: %+v", emptySetByGear) } } // StartCaptureWithFilter allows frontend to provide a custom BPF filter. func (a *App) StartCaptureWithFilter(interfaceName string, filter string) error { a.logger.Info("StartCaptureWithFilter requested", "interface", interfaceName, "filter", filter) if a.captureService.IsCapturing() { return fmt.Errorf("capture already running") } useFilter := filter if useFilter == "" { useFilter = a.config.Capture.DefaultFilter } timeoutMs := a.config.Capture.DefaultTimeout if timeoutMs > 500 { log.Printf("[service] capture timeout too high (%dms), clamp to 500ms for responsive stop", timeoutMs) timeoutMs = 500 } config := capture.Config{ InterfaceName: interfaceName, Filter: useFilter, Timeout: time.Duration(timeoutMs) * time.Millisecond, BufferSize: a.config.Capture.BufferSize, } err := a.captureService.StartCaptureAsync(context.Background(), config, func() { if a.ctx != nil { runtime.EventsEmit(a.ctx, "capture:started") } }, func(err error) { a.logger.Error("start capture failed", "error", err) if a.ctx != nil { runtime.EventsEmit(a.ctx, "capture:start_failed", err.Error()) } }) if err != nil { return err } a.logger.Info("capture start requested", "interface", interfaceName, "filter", useFilter) return nil }