Resolve canonical parties with campaign configuration

This commit is contained in:
2026-08-30 14:01:51 +00:00
parent 7e4ceb3d48
commit 61000a9466
18 changed files with 607 additions and 45 deletions

View File

@@ -167,12 +167,11 @@ func LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath string, sess
if err != nil {
return nil, err
}
return LoadSessionWithPipelineCampaignOptions(LoadedPipelineCampaign{
PipelinePath: pipelinePath,
Pipeline: pipelineCfg,
CampaignPath: campaignPath,
Campaign: campaignCfg,
}, sessionPath, sessionOpts)
loaded, err := LoadPipelineCampaign(pipelinePath, pipelineCfg, campaignPath, campaignCfg)
if err != nil {
return nil, err
}
return LoadSessionWithPipelineCampaignOptions(loaded, sessionPath, sessionOpts)
}
// LoadedPipelineCampaign retains one already loaded pipeline and campaign for
@@ -183,6 +182,35 @@ type LoadedPipelineCampaign struct {
Pipeline *PipelineConfig
CampaignPath string
Campaign *CampaignConfig
Party ResolvedParty
}
// LoadPipelineCampaign combines already loaded pipeline and campaign documents
// with their campaign-owned party source. Callers that later resolve a session
// retain this context rather than independently reimplementing party loading.
func LoadPipelineCampaign(pipelinePath string, pipeline *PipelineConfig, campaignPath string, campaign *CampaignConfig) (LoadedPipelineCampaign, error) {
if pipeline == nil {
return LoadedPipelineCampaign{}, fmt.Errorf("pipeline config is required")
}
if campaign == nil {
return LoadedPipelineCampaign{}, fmt.Errorf("campaign config is required")
}
party, err := resolveCampaignParty(campaignPath, campaign)
if err != nil {
return LoadedPipelineCampaign{}, err
}
if party.Mode == PartyModeCanonical {
if err := validateCanonicalPartySelection(campaign, nil); err != nil {
return LoadedPipelineCampaign{}, err
}
}
return LoadedPipelineCampaign{
PipelinePath: pipelinePath,
Pipeline: pipeline,
CampaignPath: campaignPath,
Campaign: campaign,
Party: party,
}, nil
}
// LoadSessionWithPipelineCampaignOptions loads one local session and combines
@@ -203,6 +231,24 @@ func LoadSessionWithPipelineCampaignOptions(loaded LoadedPipelineCampaign, sessi
// the resolved pipeline/campaign context for callers that need to locate or
// retrieve a session without rereading the root pipeline.
func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath string, sessionCfg *SessionConfig, sessionSource SessionSource) (*Config, error) {
if loaded.Pipeline == nil {
return nil, fmt.Errorf("pipeline config is required")
}
if loaded.Campaign == nil {
return nil, fmt.Errorf("campaign config is required")
}
if loaded.Party.Mode == "" {
party, err := resolveCampaignParty(loaded.CampaignPath, loaded.Campaign)
if err != nil {
return nil, err
}
loaded.Party = party
}
if loaded.Party.Mode == PartyModeCanonical {
if err := validateCanonicalPartySelection(loaded.Campaign, nil); err != nil {
return nil, err
}
}
cfg := &Config{
Pipeline: loaded.Pipeline,
Campaign: loaded.Campaign,
@@ -211,6 +257,12 @@ func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath st
CampaignPath: loaded.CampaignPath,
SessionPath: sessionPath,
SessionSource: sessionSource,
Party: loaded.Party,
}
if cfg.Party.Mode == PartyModeCanonical && sessionCfg != nil {
if err := validateCanonicalPartySelection(loaded.Campaign, sessionCfg); err != nil {
return nil, err
}
}
if sessionCfg == nil {
return cfg, nil
@@ -226,6 +278,22 @@ func ResolveLoadedPipelineCampaign(loaded LoadedPipelineCampaign, sessionPath st
if strings.TrimSpace(cfg.SessionSource.LocalPath) == "" {
cfg.SessionSource.LocalPath = sessionPath
}
if cfg.Party.Mode == PartyModeCanonical {
stableInputs.PlayersFile = virtualPlayersInput()
cfg.Session.Inputs.PlayersFile = ""
} else {
party, err := resolvePartyInput(stableInputs.PartyFile)
if err != nil {
return nil, err
}
if party.Mode == PartyModeCanonical {
return nil, fmt.Errorf("session.inputs.party_file cannot select a canonical party; canonical parties are campaign-owned")
}
cfg.Party = party
if strings.TrimSpace(stableInputs.PlayersFile.Path) == "" {
return nil, fmt.Errorf("players_file is required with a legacy party")
}
}
cfg.StableInputs = stableInputs
return cfg, nil
}
@@ -236,12 +304,11 @@ func Resolve(pipelinePath string, pipelineCfg *PipelineConfig, campaignPath stri
if sessionCfg == nil {
return nil, fmt.Errorf("session config is required")
}
return ResolveLoadedPipelineCampaign(LoadedPipelineCampaign{
PipelinePath: pipelinePath,
Pipeline: pipelineCfg,
CampaignPath: campaignPath,
Campaign: campaignCfg,
}, sessionPath, sessionCfg, sessionSource)
loaded, err := LoadPipelineCampaign(pipelinePath, pipelineCfg, campaignPath, campaignCfg)
if err != nil {
return nil, err
}
return ResolveLoadedPipelineCampaign(loaded, sessionPath, sessionCfg, sessionSource)
}
func campaignSessionPaths(paths ...string) (campaignPath, sessionPath string, err error) {