In-line Portrait Generation
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func normalizeMetadataKey(key string) string {
|
||||
replacer := strings.NewReplacer("-", "_", " ", "_")
|
||||
return strings.ToLower(replacer.Replace(strings.TrimSpace(key)))
|
||||
}
|
||||
|
||||
func parseRowMetadata(raw any) (map[string]any, error) {
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
obj, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("meta must be an object")
|
||||
}
|
||||
if len(obj) == 0 {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
meta := make(map[string]any, len(obj))
|
||||
for key, value := range obj {
|
||||
switch normalizeMetadataKey(key) {
|
||||
case "family":
|
||||
family, err := parseFamilyMetadata(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("meta.family: %w", err)
|
||||
}
|
||||
meta["family"] = family
|
||||
case "wiki":
|
||||
wiki, err := parseWikiMetadata(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("meta.wiki: %w", err)
|
||||
}
|
||||
if len(wiki) > 0 {
|
||||
meta["wiki"] = wiki
|
||||
}
|
||||
case "portrait":
|
||||
portrait, err := parsePortraitMetadata(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("meta.portrait: %w", err)
|
||||
}
|
||||
meta["portrait"] = portrait
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown metadata key %q", key)
|
||||
}
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func parsePortraitMetadata(raw any) (map[string]any, error) {
|
||||
obj, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("must be an object")
|
||||
}
|
||||
portrait := map[string]any{}
|
||||
for key, value := range obj {
|
||||
switch normalizeMetadataKey(key) {
|
||||
case "resref":
|
||||
text, ok := value.(string)
|
||||
if !ok || strings.TrimSpace(text) == "" {
|
||||
return nil, fmt.Errorf("resref must be a non-empty string")
|
||||
}
|
||||
portrait["resref"] = text
|
||||
case "sex", "race", "inanimate_type", "plot", "low_gore":
|
||||
portrait[normalizeMetadataKey(key)] = value
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown field %q", key)
|
||||
}
|
||||
}
|
||||
if _, ok := portrait["resref"]; !ok {
|
||||
return nil, fmt.Errorf("resref is required")
|
||||
}
|
||||
return portrait, nil
|
||||
}
|
||||
|
||||
func mergeExpansionData(collected []nativeCollectedDataset) ([]nativeCollectedDataset, error) {
|
||||
datasetIndexMap := make(map[string]int)
|
||||
for i, ds := range collected {
|
||||
datasetIndexMap[ds.Dataset.Name] = i
|
||||
datasetIndexMap[ds.Dataset.OutputName] = i
|
||||
}
|
||||
|
||||
rowsWithExpansion := []struct {
|
||||
datasetName string
|
||||
row map[string]any
|
||||
}{}
|
||||
|
||||
for _, ds := range collected {
|
||||
for _, row := range ds.Rows {
|
||||
if hasExpansionData(row) {
|
||||
rowsWithExpansion = append(rowsWithExpansion, struct {
|
||||
datasetName string
|
||||
row map[string]any
|
||||
}{ds.Dataset.Name, row})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(rowsWithExpansion) == 0 {
|
||||
return collected, nil
|
||||
}
|
||||
|
||||
for _, src := range rowsWithExpansion {
|
||||
expansion, _ := extractExpansionData(src.row)
|
||||
for targetDatasetName, targetRows := range expansion.Data {
|
||||
targetIndex, ok := datasetIndexMap[targetDatasetName]
|
||||
if !ok {
|
||||
outputName := targetDatasetName
|
||||
if !strings.HasSuffix(outputName, ".2da") {
|
||||
outputName = outputName + ".2da"
|
||||
}
|
||||
targetIndex, ok = datasetIndexMap[outputName]
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expansion targets unknown dataset %q", targetDatasetName)
|
||||
}
|
||||
targetDS := collected[targetIndex]
|
||||
lockModified := false
|
||||
usedIDs := map[int]struct{}{}
|
||||
usedKeys := map[string]struct{}{}
|
||||
for _, rowID := range targetDS.LockData {
|
||||
usedIDs[rowID] = struct{}{}
|
||||
}
|
||||
for _, row := range targetDS.Rows {
|
||||
if id, ok := row["id"].(int); ok {
|
||||
usedIDs[id] = struct{}{}
|
||||
}
|
||||
if key, ok := row["key"].(string); ok {
|
||||
usedKeys[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
nextID := nextAvailableID(usedIDs)
|
||||
|
||||
for _, targetRow := range targetRows {
|
||||
var rowID int
|
||||
var hasID bool
|
||||
if rawID, ok := targetRow["id"]; ok {
|
||||
switch typed := rawID.(type) {
|
||||
case int:
|
||||
rowID = typed
|
||||
hasID = true
|
||||
case float64:
|
||||
rowID = int(typed)
|
||||
hasID = true
|
||||
case string:
|
||||
parsed, err := asInt(typed)
|
||||
if err == nil {
|
||||
rowID = parsed
|
||||
hasID = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
key, _ := targetRow["key"].(string)
|
||||
if key == "" && targetDatasetName == "portraits" {
|
||||
if baseResRef, ok := targetRow["BaseResRef"].(string); ok {
|
||||
key = "portraits:" + baseResRef
|
||||
}
|
||||
}
|
||||
|
||||
if key != "" {
|
||||
if existingID, exists := targetDS.LockData[key]; exists {
|
||||
rowID = existingID
|
||||
hasID = true
|
||||
}
|
||||
if _, seen := usedKeys[key]; seen {
|
||||
continue
|
||||
}
|
||||
usedKeys[key] = struct{}{}
|
||||
}
|
||||
|
||||
if !hasID {
|
||||
rowID = nextID
|
||||
usedIDs[rowID] = struct{}{}
|
||||
nextID = nextAvailableID(usedIDs)
|
||||
} else {
|
||||
if _, exists := usedIDs[rowID]; exists && key == "" {
|
||||
return nil, fmt.Errorf("expansion into %s: row id %d already exists", targetDatasetName, rowID)
|
||||
}
|
||||
usedIDs[rowID] = struct{}{}
|
||||
}
|
||||
|
||||
if key != "" {
|
||||
if _, exists := targetDS.LockData[key]; !exists {
|
||||
targetDS.LockData[key] = rowID
|
||||
lockModified = true
|
||||
}
|
||||
}
|
||||
|
||||
newRow := map[string]any{
|
||||
"id": rowID,
|
||||
}
|
||||
for k, v := range targetRow {
|
||||
if k != "id" {
|
||||
newRow[k] = v
|
||||
}
|
||||
}
|
||||
if key != "" {
|
||||
newRow["key"] = key
|
||||
}
|
||||
targetDS.Rows = append(targetDS.Rows, newRow)
|
||||
}
|
||||
|
||||
if lockModified && targetDS.Dataset.LockPath != "" {
|
||||
if err := saveLockfile(targetDS.Dataset.LockPath, targetDS.LockData); err != nil {
|
||||
return nil, fmt.Errorf("dataset %s: %w", targetDS.Dataset.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(targetDS.Rows, func(a, b map[string]any) int {
|
||||
return a["id"].(int) - b["id"].(int)
|
||||
})
|
||||
collected[targetIndex] = targetDS
|
||||
}
|
||||
|
||||
if err := applyExpansionValueToRow(src.row, expansion.Value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return collected, nil
|
||||
}
|
||||
|
||||
type expansionSpec struct {
|
||||
Value string
|
||||
Data map[string][]map[string]any
|
||||
}
|
||||
|
||||
func hasExpansionData(row map[string]any) bool {
|
||||
for _, value := range row {
|
||||
if isExpansionValue(value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isExpansionValue(value any) bool {
|
||||
obj, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, hasValue := obj["value"]
|
||||
_, hasData := obj["data"]
|
||||
return hasValue && hasData
|
||||
}
|
||||
|
||||
func extractExpansionData(row map[string]any) (expansionSpec, bool) {
|
||||
for _, value := range row {
|
||||
if isExpansionValue(value) {
|
||||
obj := value.(map[string]any)
|
||||
valueStr, _ := obj["value"].(string)
|
||||
dataMap, _ := obj["data"].(map[string]any)
|
||||
|
||||
result := expansionSpec{
|
||||
Value: valueStr,
|
||||
Data: make(map[string][]map[string]any),
|
||||
}
|
||||
|
||||
for datasetName, rawRows := range dataMap {
|
||||
switch typed := rawRows.(type) {
|
||||
case []any:
|
||||
rows := make([]map[string]any, 0, len(typed))
|
||||
for _, r := range typed {
|
||||
if rowObj, ok := r.(map[string]any); ok {
|
||||
rows = append(rows, rowObj)
|
||||
}
|
||||
}
|
||||
result.Data[datasetName] = rows
|
||||
case map[string]any:
|
||||
result.Data[datasetName] = []map[string]any{typed}
|
||||
}
|
||||
}
|
||||
|
||||
return result, true
|
||||
}
|
||||
}
|
||||
return expansionSpec{}, false
|
||||
}
|
||||
|
||||
func applyExpansionValueToRow(row map[string]any, value string) error {
|
||||
for field, oldValue := range row {
|
||||
if isExpansionValue(oldValue) {
|
||||
row[field] = value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user