|
|
|
@@ -530,6 +530,7 @@ func buildNativeUnchecked(p *project.Project, opts NativeBuildOptions, progress
|
|
|
|
|
groupStats := nativeCompileGroupStats(collected)
|
|
|
|
|
currentGroup := ""
|
|
|
|
|
sidecars := newNativeSidecarCollector()
|
|
|
|
|
racialFeatRules := racialUsableFeatRules(collected)
|
|
|
|
|
for _, dataset := range collected {
|
|
|
|
|
group := nativeCompileGroup(dataset.Dataset.Name)
|
|
|
|
|
if group != currentGroup {
|
|
|
|
@@ -542,7 +543,7 @@ func buildNativeUnchecked(p *project.Project, opts NativeBuildOptions, progress
|
|
|
|
|
stats.SourceFragments,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
compiled, err := resolveNativeDataset(dataset, globalKeyToID, globalRowByKey, tableRegistry, compiler, p.EffectiveConfig().TopData.ClassFeatInjections, sidecars)
|
|
|
|
|
compiled, err := resolveNativeDataset(dataset, globalKeyToID, globalRowByKey, tableRegistry, compiler, p.EffectiveConfig().TopData.ClassFeatInjections, racialFeatRules, sidecars)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return BuildResult{}, err
|
|
|
|
|
}
|
|
|
|
@@ -3996,13 +3997,13 @@ func normalizeGlobalReferenceID(value string) string {
|
|
|
|
|
return parts[0] + ":" + strings.ReplaceAll(parts[1], "_", "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func resolveNativeDataset(dataset nativeCollectedDataset, keyToID map[string]int, globalRowByKey map[string]map[string]any, tableRegistry resolvedTableRegistry, compiler *tlkCompiler, classFeatInjections project.TopDataClassFeatInjectionConfig, sidecars *nativeSidecarCollector) (map[string]any, error) {
|
|
|
|
|
func resolveNativeDataset(dataset nativeCollectedDataset, keyToID map[string]int, globalRowByKey map[string]map[string]any, tableRegistry resolvedTableRegistry, compiler *tlkCompiler, classFeatInjections project.TopDataClassFeatInjectionConfig, racialFeatRules []project.TopDataClassFeatGlobalRule, sidecars *nativeSidecarCollector) (map[string]any, error) {
|
|
|
|
|
rows := dataset.Rows
|
|
|
|
|
if strings.HasPrefix(filepath.ToSlash(dataset.Dataset.Name), "classes/feats/") {
|
|
|
|
|
classKey := "classes:" + dataset.Dataset.Name[strings.LastIndex(dataset.Dataset.Name, "/")+1:]
|
|
|
|
|
featSuccessors := buildFeatSuccessorsIndex(globalRowByKey, keyToID)
|
|
|
|
|
classSkills := buildClassSkillsIndex(tableRegistry, classKey)
|
|
|
|
|
expanded, err := expandClassesFeatRows(rows, keyToID, globalRowByKey, featSuccessors, classSkills, globalRowByKey, classKey, classFeatInjections, !dataset.Dataset.HasGlobalInjections)
|
|
|
|
|
expanded, err := expandClassesFeatRows(rows, keyToID, globalRowByKey, featSuccessors, classSkills, globalRowByKey, classKey, classFeatInjections, racialFeatRules, !dataset.Dataset.HasGlobalInjections)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("dataset %s: %w", dataset.Dataset.Name, err)
|
|
|
|
|
}
|
|
|
|
@@ -4051,12 +4052,18 @@ var (
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func expandClassesFeatRows(rows []map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, featSuccessors map[string]string, classSkills map[string]bool, allRowByKey map[string]map[string]any, classKey string, classFeatInjections project.TopDataClassFeatInjectionConfig, useConfiguredInjections bool) ([]map[string]any, error) {
|
|
|
|
|
func expandClassesFeatRows(rows []map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, featSuccessors map[string]string, classSkills map[string]bool, allRowByKey map[string]map[string]any, classKey string, classFeatInjections project.TopDataClassFeatInjectionConfig, racialFeatRules []project.TopDataClassFeatGlobalRule, useConfiguredInjections bool) ([]map[string]any, error) {
|
|
|
|
|
|
|
|
|
|
globalRules, classSkillRules := []project.TopDataClassFeatGlobalRule{}, []project.TopDataClassFeatMasterfeatRule{}
|
|
|
|
|
if useConfiguredInjections {
|
|
|
|
|
globalRules, classSkillRules = effectiveClassFeatInjectionRules(classFeatInjections)
|
|
|
|
|
}
|
|
|
|
|
// Racial usable-feat rows are generated from the race feats tables, not the
|
|
|
|
|
// hand-authored class-feat injections, so they apply to every class table
|
|
|
|
|
// regardless of whether that dataset carries its own global.json (which is
|
|
|
|
|
// what gates useConfiguredInjections). Deduped below against rows already
|
|
|
|
|
// present, so a leftover hand row in global.json is a no-op, not a double.
|
|
|
|
|
globalRules = append(globalRules, racialFeatRules...)
|
|
|
|
|
injected := make([]map[string]any, 0, len(globalRules)+len(classSkillRules))
|
|
|
|
|
presentRefIDs := make(map[string]struct{}, len(rows))
|
|
|
|
|
for _, row := range rows {
|
|
|
|
@@ -4134,6 +4141,54 @@ func expandClassesFeatRows(rows []map[string]any, keyToID map[string]int, rowByK
|
|
|
|
|
return combined, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// racialUsableFeatRules builds one class-feat injection rule per feat marked
|
|
|
|
|
// UsableFeat=1 in any race feats table (race_feat_*.2da). Racial feats are
|
|
|
|
|
// granted by race, never by a class, so an activatable one needs a menu-only
|
|
|
|
|
// cls_feat row to reach the client radial: List=3 keeps it off every level-up
|
|
|
|
|
// selection list, GrantedOnLevel=99 sits above the level cap so no class ever
|
|
|
|
|
// actually grants it, and OnMenu=1 renders the button once the creature holds
|
|
|
|
|
// the feat (possession comes from chargen / the login racial-feat sync). This
|
|
|
|
|
// replaces the hand-maintained racial rows in classes/feats/global.json - mark
|
|
|
|
|
// UsableFeat in the race table and the radial row follows automatically.
|
|
|
|
|
func racialUsableFeatRules(collected []nativeCollectedDataset) []project.TopDataClassFeatGlobalRule {
|
|
|
|
|
seen := map[string]struct{}{}
|
|
|
|
|
rules := []project.TopDataClassFeatGlobalRule{}
|
|
|
|
|
for _, ds := range collected {
|
|
|
|
|
if !strings.HasPrefix(ds.Dataset.OutputName, "race_feat_") || !strings.HasSuffix(ds.Dataset.OutputName, ".2da") {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, row := range ds.Rows {
|
|
|
|
|
if usable, err := asInt(fieldValue(row, "UsableFeat")); err != nil || usable != 1 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
featRef, ok := row["FeatIndex"].(map[string]any)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
featID, _ := featRef["id"].(string)
|
|
|
|
|
if featID == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, dup := seen[featID]; dup {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[featID] = struct{}{}
|
|
|
|
|
rules = append(rules, project.TopDataClassFeatGlobalRule{
|
|
|
|
|
Feat: featID,
|
|
|
|
|
List: "3",
|
|
|
|
|
GrantedOnLevel: "99",
|
|
|
|
|
OnMenu: "1",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Discovery order across files and the dedup map are both unordered; sort so
|
|
|
|
|
// the injected rows (and the resulting 2DA row numbering) are deterministic.
|
|
|
|
|
slices.SortFunc(rules, func(a, b project.TopDataClassFeatGlobalRule) int {
|
|
|
|
|
return strings.Compare(a.Feat, b.Feat)
|
|
|
|
|
})
|
|
|
|
|
return rules
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func effectiveClassFeatInjectionRules(config project.TopDataClassFeatInjectionConfig) ([]project.TopDataClassFeatGlobalRule, []project.TopDataClassFeatMasterfeatRule) {
|
|
|
|
|
if len(config.GlobalFeats) == 0 && len(config.ClassSkillMasterfeats) == 0 {
|
|
|
|
|
return defaultClassFeatGlobalRules, defaultClassFeatClassSkillMasterfeatRules
|
|
|
|
|