package itemregistry import ( "fmt" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/diagnostics" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared/entityreconcile" ) type safeReconciliationGroup struct { members []int canonical int } func reconciliationCandidates(records []normalizedRecord) []entityreconcile.Candidate { candidates := make([]entityreconcile.Candidate, len(records)) for index, record := range records { candidates[index] = entityreconcile.Candidate{Name: record.item.Name, SourceRefs: cloneSourceRefs(record.item.SourceRefs)} } return candidates } func reconciliationGroups(assessment entityreconcile.Assessment, candidateKeys []string) []safeReconciliationGroup { positions := make(map[string]int, len(candidateKeys)) for index, key := range candidateKeys { positions[key] = index } safeGroups := assessment.SafeGroups() groups := make([]safeReconciliationGroup, 0, len(safeGroups)) for _, group := range safeGroups { members := group.Members() memberPositions := make([]int, len(members)) valid := true for index, key := range members { position, ok := positions[key] if !ok { valid = false break } memberPositions[index] = position } canonical, ok := positions[group.Canonical()] if valid && ok { groups = append(groups, safeReconciliationGroup{members: memberPositions, canonical: canonical}) } } return groups } func reconciliationIssues(assessment entityreconcile.Assessment) []string { issues := assessment.Issues() details := make([]string, len(issues)) for index, issue := range issues { details[index] = fmt.Sprintf("group %d: %s", issue.GroupIndex, issue.Category) } return details } func applySafeGroups(records []normalizedRecord, groups []safeReconciliationGroup, order shared.SourceRefOrder) ([]normalizedRecord, []contracts.Warning, int) { byMember := make(map[int]safeReconciliationGroup, len(groups)*2) for _, group := range groups { for _, member := range group.members { byMember[member] = group } } output := make([]normalizedRecord, 0, len(records)-len(groups)) warnings := make([]contracts.Warning, 0, len(groups)) rejectedGroups := 0 for index, record := range records { group, grouped := byMember[index] if !grouped { output = append(output, cloneRecord(record)) continue } if group.members[0] != index { continue } if !canConsolidate(records, group) { for _, member := range group.members { output = append(output, cloneRecord(records[member])) } warnings = append(warnings, contracts.Warning{Scope: itemScope(records[group.members[0]].earliest), ReasonCode: ReasonCodeItemSemanticProposalInvalid, Message: "proposal group preserved because currency may only be consolidated with aliases of one denomination"}) rejectedGroups++ continue } consolidated := consolidateSemanticGroup(records, group, order) output = append(output, consolidated) warnings = append(warnings, semanticDuplicateWarning(consolidated, records[group.canonical])) } return output, warnings, rejectedGroups } func canConsolidate(records []normalizedRecord, group safeReconciliationGroup) bool { denomination := "" hasCurrency := false hasNonCurrency := false hasConflictingDenominations := false for _, member := range group.members { current := currencyDenomination(records[member].item.Name) if current == "" { hasNonCurrency = true continue } hasCurrency = true if denomination != "" && denomination != current { hasConflictingDenominations = true continue } denomination = current } return !hasCurrency || (!hasNonCurrency && !hasConflictingDenominations) } func currencyDenomination(name string) string { switch identity.ComparisonKey(name) { case "copper piece", "copper pieces", "cp": return "copper" case "silver piece", "silver pieces", "sp": return "silver" case "electrum piece", "electrum pieces", "ep": return "electrum" case "gold piece", "gold pieces", "gp": return "gold" case "platinum piece", "platinum pieces", "pp": return "platinum" default: return "" } } func consolidateSemanticGroup(records []normalizedRecord, group safeReconciliationGroup, order shared.SourceRefOrder) normalizedRecord { output := cloneRecord(records[group.members[0]]) output.item.Name = records[group.canonical].item.Name for _, member := range group.members[1:] { output.item.SourceRefs = append(output.item.SourceRefs, records[member].item.SourceRefs...) output.inputIndexes = append(output.inputIndexes, records[member].inputIndexes...) if records[member].earliest < output.earliest { output.earliest = records[member].earliest } } output.inputIndexes = sortedUniqueIndexes(output.inputIndexes) output.item.SourceRefs = order.Canonicalize(output.item.SourceRefs) output.item.ID = identity.DeriveID(output.item.Name) return output } func semanticDuplicateWarning(record normalizedRecord, canonical normalizedRecord) contracts.Warning { details := make([]string, 0, len(record.inputIndexes)+1) for _, inputIndex := range record.inputIndexes { details = append(details, fmt.Sprintf("input index %d", inputIndex)) } if canonical.earliest != record.earliest { details = append(details, fmt.Sprintf("canonical display name from input index %d", canonical.earliest)) } return contracts.Warning{Scope: itemScope(record.earliest), ReasonCode: ReasonCodeDuplicateItemCollapsed, Message: diagnostics.Aggregate("semantic duplicate consolidation", details)} }