Files
sow-tools/internal/topdata/wiki_data_providers.go
T

261 lines
8.2 KiB
Go

package topdata
import (
"fmt"
"os"
"strings"
"gopkg.in/yaml.v3"
)
type wikiDataProvidersDocument struct {
Providers map[string]wikiDataProviderDefinition `json:"providers" yaml:"providers"`
}
type wikiDataProviderDefinition struct {
Source string `json:"source" yaml:"source"`
Levels string `json:"levels" yaml:"levels"`
ClassFeats wikiDataProviderClassFeatConfig `json:"class_feats" yaml:"class_feats"`
Provider string `json:"provider" yaml:"provider"`
Columns int `json:"columns" yaml:"columns"`
ItemsPerColumn int `json:"items_per_column" yaml:"items_per_column"`
ForceColumns bool `json:"force_columns" yaml:"force_columns"`
}
type wikiDataProviderClassFeatConfig struct {
Fields map[string]wikiDataProviderProjectionField `json:"fields" yaml:"fields"`
}
type wikiDataProviderProjectionField struct {
When string `json:"when" yaml:"when"`
}
func loadWikiDataProviders(path string) (map[string]wikiDataProviderDefinition, error) {
raw, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return map[string]wikiDataProviderDefinition{}, nil
}
return nil, fmt.Errorf("read wiki data providers %s: %w", path, err)
}
var doc wikiDataProvidersDocument
if err := yaml.Unmarshal(raw, &doc); err != nil {
return nil, fmt.Errorf("parse wiki data providers %s: %w", path, err)
}
for name, def := range doc.Providers {
if strings.TrimSpace(name) == "" {
return nil, fmt.Errorf("wiki data providers %s: provider name must not be empty", path)
}
if strings.TrimSpace(def.Source) == "" {
return nil, fmt.Errorf("wiki data providers %s: provider %s missing source", path, name)
}
source := strings.TrimSpace(def.Source)
switch source {
case "class_progression":
for field, projection := range def.ClassFeats.Fields {
if strings.TrimSpace(field) == "" {
return nil, fmt.Errorf("wiki data providers %s: provider %s has class feat projection without field name", path, name)
}
if strings.TrimSpace(projection.When) == "" {
return nil, fmt.Errorf("wiki data providers %s: provider %s class feat projection %s missing when", path, name, field)
}
}
case "columns":
if len(def.ClassFeats.Fields) > 0 {
return nil, fmt.Errorf("wiki data providers %s: provider %s class feat projections require source class_progression", path, name)
}
if strings.TrimSpace(def.Provider) == "" {
return nil, fmt.Errorf("wiki data providers %s: provider %s columns provider missing provider", path, name)
}
if strings.TrimSpace(def.Provider) == name {
return nil, fmt.Errorf("wiki data providers %s: provider %s columns provider must not reference itself", path, name)
}
if def.Columns <= 0 {
return nil, fmt.Errorf("wiki data providers %s: provider %s columns must be a positive integer", path, name)
}
if def.ItemsPerColumn <= 0 {
return nil, fmt.Errorf("wiki data providers %s: provider %s items_per_column must be a positive integer", path, name)
}
case "class_feats", "class_skills", "class_summary", "race_feats", "race_name_forms":
if len(def.ClassFeats.Fields) > 0 {
return nil, fmt.Errorf("wiki data providers %s: provider %s class feat projections require source class_progression", path, name)
}
default:
return nil, fmt.Errorf("wiki data providers %s: provider %s has unknown source %q", path, name, def.Source)
}
}
for name, def := range doc.Providers {
if strings.TrimSpace(def.Source) != "columns" {
continue
}
if _, ok := doc.Providers[strings.TrimSpace(def.Provider)]; !ok {
return nil, fmt.Errorf("wiki data providers %s: provider %s columns provider references missing provider %q", path, name, def.Provider)
}
}
if doc.Providers == nil {
return map[string]wikiDataProviderDefinition{}, nil
}
return doc.Providers, nil
}
func (ctx *wikiContext) wikiDataProviderRows(name string, page wikiTemplatePage) ([]map[string]any, error) {
def, ok := ctx.wikiDataProviders[name]
if !ok {
return nil, fmt.Errorf("missing wiki data provider %q in %s", name, ctx.wikiDataPath)
}
switch strings.TrimSpace(def.Source) {
case "columns":
sourceRows, err := ctx.wikiDataProviderRows(strings.TrimSpace(def.Provider), page)
if err != nil {
return nil, err
}
return wikiColumnProviderRows(sourceRows, def.Columns, def.ItemsPerColumn, def.ForceColumns), nil
case "class_progression":
if page.Category != "classes" {
return nil, nil
}
return ctx.classProgressionRows(def.Levels, def.ClassFeats.Fields, page.Row)
case "class_feats":
if page.Category != "classes" {
return nil, nil
}
return ctx.classFeatRows(page.Row), nil
case "class_skills":
if page.Category != "classes" {
return nil, nil
}
return ctx.classSkillRows(page.Row), nil
case "class_summary":
if page.Category != "classes" {
return nil, nil
}
return ctx.classSummaryRows(page.Row), nil
case "race_feats":
if page.Category != "racialtypes" {
return nil, nil
}
return ctx.raceFeatRows(page.Row), nil
case "race_name_forms":
if page.Category != "racialtypes" {
return nil, nil
}
return ctx.raceNameFormRows(page.Row), nil
default:
return nil, fmt.Errorf("unknown wiki data provider source %q", def.Source)
}
}
func wikiColumnProviderRows(sourceRows []map[string]any, configuredColumns, itemsPerColumn int, forceColumns bool) []map[string]any {
if configuredColumns <= 0 || itemsPerColumn <= 0 {
return nil
}
needed := 0
if len(sourceRows) > 0 {
needed = (len(sourceRows) + itemsPerColumn - 1) / itemsPerColumn
}
total := needed
if forceColumns && configuredColumns > total {
total = configuredColumns
}
if total == 0 {
return nil
}
rows := make([]map[string]any, 0, total)
for i := 0; i < total; i++ {
start := i * itemsPerColumn
end := start + itemsPerColumn
if start > len(sourceRows) {
start = len(sourceRows)
}
if end > len(sourceRows) {
end = len(sourceRows)
}
items := make([]map[string]any, 0, end-start)
for _, source := range sourceRows[start:end] {
items = append(items, cloneRowMap(source))
}
rows = append(rows, map[string]any{
"Index": i + 1,
"Count": len(items),
"Items": items,
})
}
return rows
}
func (ctx *wikiContext) classSkillRows(classRow map[string]any) []map[string]any {
table := ctx.tableForValue(fieldValue(classRow, "SkillsTable"), ctx.classSkillTables)
if table == nil {
return nil
}
rows := []map[string]any{}
for _, source := range table.Rows {
if stringValue(source, "ClassSkill") != "1" {
continue
}
key := resolveReferenceKey(fieldValue(source, "SkillIndex"), ctx.skillIDToKey)
if key == "" {
key = resolveReferenceKey(fieldValue(source, "SkillIndex"), nil)
}
if key == "" || !ctx.canReferenceWikiKey(key) {
continue
}
row := cloneRowMap(source)
row["SkillKey"] = key
row["SkillName"] = ctx.resolveSkillName(key)
rows = append(rows, row)
}
return rows
}
func (ctx *wikiContext) raceFeatRows(raceRow map[string]any) []map[string]any {
values := []any{}
switch typed := fieldValue(raceRow, "FeatsTable").(type) {
case []any:
values = append(values, typed...)
case []string:
for _, value := range typed {
values = append(values, value)
}
default:
if table := ctx.tableForValue(typed, ctx.raceFeatTables); table != nil {
for _, row := range table.Rows {
values = append(values, fieldValue(row, "FeatIndex"))
}
}
}
rows := []map[string]any{}
for _, value := range values {
key := resolveReferenceKey(value, ctx.featIDToKey)
if key == "" {
key = resolveReferenceKey(value, nil)
}
if key == "" || !ctx.canReferenceWikiKey(key) {
continue
}
rows = append(rows, map[string]any{
"FeatKey": key,
"FeatName": ctx.resolveFeatName(key),
})
}
return rows
}
func (ctx *wikiContext) raceNameFormRows(raceRow map[string]any) []map[string]any {
candidates := []struct {
label string
field string
}{
{"Plural", "NamePlural"},
{"Converted Name", "ConverName"},
{"Lower Name", "ConverNameLower"},
}
rows := []map[string]any{}
for _, candidate := range candidates {
if value := ctx.resolveTextValue(fieldValue(raceRow, candidate.field), nil); value != "" {
rows = append(rows, map[string]any{"Label": candidate.label, "Value": value})
}
}
return rows
}