classFeatExpansionCanUseFeat excludes ALLCLASSESCANUSE=0 feats from class feat table masterfeat expansion. When such a feat also has no MinLevelClass, no class can ever obtain it — a data bug that previously failed silently (e.g. Spell Focus vanishing from the wizard table). New validator warns for exactly that case; deliberate exclusions with an explicit MinLevelClass stay silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
2.9 KiB
Go
99 lines
2.9 KiB
Go
package topdata
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// validateClassFeatMasterfeatAccessibility warns when a class feat table
|
|
// references a masterfeat whose member feats are silently dropped from
|
|
// expansion by classFeatExpansionCanUseFeat. Only the ambiguous case is
|
|
// flagged: ALLCLASSESCANUSE=0 with no MinLevelClass, where the feat cannot
|
|
// be obtained by any class at all. Exclusions with an explicit MinLevelClass
|
|
// are deliberate cross-class restrictions and stay silent.
|
|
func validateClassFeatMasterfeatAccessibility(dataDir string, report *ValidationReport) {
|
|
datasets, err := discoverNativeDatasets(dataDir)
|
|
if err != nil {
|
|
return
|
|
}
|
|
keyToID := map[string]int{}
|
|
var featRowByKey map[string]map[string]any
|
|
type classFeatTable struct {
|
|
path string
|
|
classKey string
|
|
rows []map[string]any
|
|
}
|
|
classTables := []classFeatTable{}
|
|
for _, dataset := range datasets {
|
|
name := filepath.ToSlash(dataset.Name)
|
|
isClassFeats := strings.HasPrefix(name, "classes/feats/")
|
|
if name != "feat" && name != "masterfeats" && name != "classes/core" && !isClassFeats {
|
|
continue
|
|
}
|
|
collected, err := collectNativeDataset(dataset)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for key, id := range collected.LockData {
|
|
keyToID[key] = id
|
|
}
|
|
switch {
|
|
case name == "feat":
|
|
featRowByKey = rowsByKey(collected.Rows)
|
|
case isClassFeats:
|
|
classTables = append(classTables, classFeatTable{
|
|
path: dataset.BasePath,
|
|
classKey: "classes:" + name[strings.LastIndex(name, "/")+1:],
|
|
rows: collected.Rows,
|
|
})
|
|
}
|
|
}
|
|
if featRowByKey == nil {
|
|
return
|
|
}
|
|
for _, table := range classTables {
|
|
for _, row := range table.rows {
|
|
featRef, ok := row["FeatIndex"].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
refKey, _ := featRef["id"].(string)
|
|
if !strings.HasPrefix(refKey, "masterfeats:") {
|
|
continue
|
|
}
|
|
masterfeatKey := resolveCanonicalDatasetKey(refKey, keyToID)
|
|
if _, ok := keyToID[masterfeatKey]; !ok {
|
|
continue
|
|
}
|
|
featKeys := make([]string, 0, len(featRowByKey))
|
|
for featKey := range featRowByKey {
|
|
if strings.HasPrefix(featKey, "feat:") {
|
|
featKeys = append(featKeys, featKey)
|
|
}
|
|
}
|
|
slices.Sort(featKeys)
|
|
for _, featKey := range featKeys {
|
|
featRow := featRowByKey[featKey]
|
|
if !rowReferencesMasterfeat(featRow, masterfeatKey, keyToID) {
|
|
continue
|
|
}
|
|
if classFeatExpansionCanUseFeat(featRow, table.classKey, keyToID) {
|
|
continue
|
|
}
|
|
if minLevelClass, ok := lookupField(featRow, "MinLevelClass"); ok && !isNullishValue(minLevelClass) {
|
|
continue
|
|
}
|
|
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
|
Severity: SeverityWarning,
|
|
Path: table.path,
|
|
Message: fmt.Sprintf(
|
|
"%s references %s but %s is excluded from expansion (ALLCLASSESCANUSE=0 with no MinLevelClass makes it unobtainable); override ALLCLASSESCANUSE or set MinLevelClass",
|
|
table.classKey, masterfeatKey, featKey),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|