Fix Generated Feat Identity Canonicalization

This commit is contained in:
2026-04-17 21:35:54 +02:00
parent f32dd224fe
commit 1e9a81a6cf
3 changed files with 338 additions and 4 deletions
+194 -2
View File
@@ -765,6 +765,11 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
if parsedID == lockedID {
return override, nil
}
if dataset.Name == "feat" && generatedCanonicalLockCanMove(rawKey, parsedID, lockData) {
delete(lockData, rawKey)
lockModified = true
return override, nil
}
cloned := make(map[string]any, len(override)-1)
for field, value := range override {
if field != "id" {
@@ -779,6 +784,11 @@ func collectBaseDataset(dataset nativeDataset) (nativeCollectedDataset, error) {
}
for lockedKey, lockedID := range lockData {
if lockedKey != key && lockedID == rowID {
if dataset.Name == "feat" && generatedAliasLockForKey(key, lockedKey) {
delete(lockData, lockedKey)
lockModified = true
continue
}
return false
}
}
@@ -2055,13 +2065,24 @@ func featSourceID(value any) (int, error) {
func (c *featGeneratedContext) resolveGeneratedFeatIdentityBySource(familyKey, slug string, rawSource any) (string, int, bool, error) {
if rawSource != nil && rawSource != nullValue {
if parsedID, err := featSourceID(rawSource); err == nil && parsedID > 0 {
if existingKey, ok := c.preferredFeatKeyForID(familyKey, parsedID); ok {
return existingKey, parsedID, true, nil
if generatedKey, ok := c.generatedFeatKeyForID(familyKey, parsedID); ok {
return generatedKey, parsedID, true, nil
}
return c.preferredGeneratedFeatKey(familyKey, slug), parsedID, true, nil
}
}
if rawMap, ok := rawSource.(map[string]any); ok {
if ref, ok := rawMap["id"].(string); ok && strings.HasPrefix(ref, "feat:") {
if generatedKey, rowID, ok := c.generatedFeatKeyFromCanonicalAlias(ref); ok {
return generatedKey, rowID, true, nil
}
if rowID, hasID, err := c.featID(ref, nil); err != nil {
return "", 0, false, err
} else if hasID {
if generatedKey, ok := c.generatedFeatKeyForID(familyKey, rowID); ok {
return generatedKey, rowID, true, nil
}
}
rowID, hasID, err := c.featID(ref, nil)
return ref, rowID, hasID, err
}
@@ -2080,6 +2101,9 @@ func (c *featGeneratedContext) resolveGeneratedFeatIdentity(spec familyExpansion
return c.resolveGeneratedFeatIdentityBySource(spec.FamilyKey, slug, rawSource)
}
featKey := c.preferredGeneratedFeatKey(spec.FamilyKey, slug)
if legacyKey, legacyID, ok := c.generatedFeatKeyFromLegacyAlias(spec.FamilyKey, slug); ok {
return legacyKey, legacyID, true, nil
}
rowID, hasID, err := c.featID(featKey, nil)
return featKey, rowID, hasID, err
}
@@ -2116,6 +2140,174 @@ func generatedFamilyPrereqKey(ctx *featGeneratedContext, sourceRow map[string]an
return featKey, true
}
func generatedFamilyLegacyAliases(familyKey string) []string {
switch familyKey {
case "skillfocus":
return []string{"skillfocus"}
case "greaterskillfocus":
return []string{"epicskillfocus"}
case "greaterweaponfocus":
return []string{"epicweaponfocus"}
case "greaterweaponspecialization":
return []string{"epicweaponspecialization"}
case "overwhelmingcritical":
return []string{"epicoverwhelmingcritical"}
default:
return nil
}
}
func generatedAliasLockForKey(canonicalKey, lockedKey string) bool {
canonicalKey = strings.TrimPrefix(strings.TrimSpace(canonicalKey), "feat:")
lockedKey = strings.TrimPrefix(strings.TrimSpace(lockedKey), "feat:")
if canonicalKey == "" || lockedKey == "" {
return false
}
identity := splitFamilyExpansionIdentity(canonicalKey)
if identity.Parent == "" || identity.Child == "" {
return false
}
legacyChildren := legacyFamilyChildAliases(identity.Parent, identity.Child)
for _, alias := range generatedFamilyLegacyAliases(identity.Parent) {
prefix := alias + "_"
if !strings.HasPrefix(lockedKey, prefix) {
continue
}
lockedChild := strings.TrimPrefix(lockedKey, prefix)
for _, legacyChild := range legacyChildren {
if normalizeKeyIdentity(lockedChild) == normalizeKeyIdentity(legacyChild) {
return true
}
}
}
return false
}
func generatedCanonicalLockCanMove(canonicalKey string, rowID int, lockData map[string]int) bool {
if rowID <= 0 {
return false
}
for lockedKey, lockedID := range lockData {
if lockedID == rowID && generatedAliasLockForKey(canonicalKey, lockedKey) {
return true
}
}
return false
}
func (c *featGeneratedContext) generatedFeatKeyForID(familyKey string, rowID int) (string, bool) {
if rowID <= 0 {
return "", false
}
for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
matches := make([]string, 0)
prefix := "feat:" + familyKey + "_"
for key, candidateID := range candidates {
if candidateID == rowID && strings.HasPrefix(key, prefix) {
matches = append(matches, key)
}
}
if best, ok := preferredExpandedKey(matches); ok {
return best, true
}
}
for _, alias := range generatedFamilyLegacyAliases(familyKey) {
for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
matches := make([]string, 0)
prefix := "feat:" + alias + "_"
for key, candidateID := range candidates {
if candidateID != rowID || !strings.HasPrefix(key, prefix) {
continue
}
child := strings.TrimPrefix(key, prefix)
matches = append(matches, "feat:"+familyKey+"_"+child)
}
if best, ok := preferredExpandedKey(matches); ok {
return best, true
}
}
}
return "", false
}
func (c *featGeneratedContext) generatedFeatKeyFromLegacyAlias(familyKey, sourceSlug string) (string, int, bool) {
targets := legacyFamilyChildAliases(familyKey, sourceSlug)
canonicalKey := c.preferredGeneratedFeatKey(familyKey, sourceSlug)
for _, alias := range generatedFamilyLegacyAliases(familyKey) {
for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
prefix := "feat:" + alias + "_"
for _, target := range targets {
for key, rowID := range candidates {
if rowID <= 0 || !strings.HasPrefix(key, prefix) {
continue
}
child := strings.TrimPrefix(key, prefix)
if normalizeKeyIdentity(child) == normalizeKeyIdentity(target) {
return canonicalKey, rowID, true
}
}
}
}
}
return "", 0, false
}
func legacyFamilyChildAliases(familyKey, sourceSlug string) []string {
aliases := []string{}
if familyKey != "greaterskillfocus" && familyKey != "skillfocus" {
return []string{sourceSlug}
}
switch normalizeKeyIdentity(sourceSlug) {
case "acrobatics":
aliases = append(aliases, "tumble")
case "animalhandling":
aliases = append(aliases, "animalempathy")
case "craftarmorsmithing":
aliases = append(aliases, "craftarmor")
case "crafttinkering":
aliases = append(aliases, "settrap")
case "craftweaponsmithing":
aliases = append(aliases, "craftweapon")
case "craftwoodworking":
aliases = append(aliases, "crafttrap")
case "disabledevice":
aliases = append(aliases, "disabletrap")
case "influence":
aliases = append(aliases, "persuade")
case "sleightofhand":
aliases = append(aliases, "pickpocket")
}
aliases = append(aliases, sourceSlug)
return aliases
}
func (c *featGeneratedContext) generatedFeatKeyFromCanonicalAlias(canonicalKey string) (string, int, bool) {
stripped := strings.TrimPrefix(strings.TrimSpace(canonicalKey), "feat:")
identity := splitFamilyExpansionIdentity(stripped)
if identity.Parent == "" || identity.Child == "" {
return "", 0, false
}
legacyChildren := legacyFamilyChildAliases(identity.Parent, identity.Child)
for _, alias := range generatedFamilyLegacyAliases(identity.Parent) {
for _, legacyChild := range legacyChildren {
if _, rowID, ok := c.featKeyForFamilyChild(alias, legacyChild); ok {
return "feat:" + identity.Parent + "_" + identity.Child, rowID, true
}
}
}
return "", 0, false
}
func (c *featGeneratedContext) featKeyForFamilyChild(familyKey, child string) (string, int, bool) {
key := "feat:" + familyKey + "_" + child
for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
if rowID, ok := candidates[key]; ok && rowID > 0 {
return key, rowID, true
}
}
return "", 0, false
}
func (c *featGeneratedContext) preferredFeatKeyForID(familyKey string, rowID int) (string, bool) {
prefix := "feat:" + familyKey
type candidateBuckets struct {