diff --git a/Runtime/CampaignPacks.meta b/Runtime/CampaignPacks.meta new file mode 100644 index 0000000..9c381dd --- /dev/null +++ b/Runtime/CampaignPacks.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9c1e0b465a5d4d858308fca1b8676f5e +timeCreated: 1780215600 diff --git a/Runtime/CampaignPacks/CampaignPackDefinitions.cs b/Runtime/CampaignPacks/CampaignPackDefinitions.cs new file mode 100644 index 0000000..e1cb44b --- /dev/null +++ b/Runtime/CampaignPacks/CampaignPackDefinitions.cs @@ -0,0 +1,240 @@ +using System.Collections.Generic; + +namespace PatchManager.CampaignPacks +{ + /// + /// Runtime JSON-facing representation of a campaign pack. + /// + public sealed class CampaignPackDefinition + { + /// + /// Stable campaign pack identifier used by other campaign pack definitions and extensions. + /// + public string Id = string.Empty; + + /// + /// Localization key used for the player-facing campaign pack name. + /// + public string NameLocKey = string.Empty; + + /// + /// Localization key used for the player-facing campaign pack description. + /// + public string DescriptionLocKey = string.Empty; + + /// + /// Addressable or data key of the galaxy definition used by this campaign pack. + /// + public string GalaxyDefinitionKey = string.Empty; + + /// + /// Optional identifier of the tech tree set referenced by this campaign pack. + /// + public string TechTreeSetId = string.Empty; + + /// + /// Optional identifier of the mission set referenced by this campaign pack. + /// + public string MissionSetId = string.Empty; + + /// + /// Optional identifier of the science set referenced by this campaign pack. + /// + public string ScienceSetId = string.Empty; + } + + /// + /// Runtime JSON-facing representation of a tech tree set. + /// + public sealed class TechTreeSetDefinition + { + /// + /// Stable tech tree set identifier. + /// + public string Id = string.Empty; + + /// + /// Tech tree node identifiers included by this set before extensions are applied. + /// + public List TechNodeIds = new(); + } + + /// + /// Runtime JSON-facing representation of a mission set. + /// + public sealed class MissionSetDefinition + { + /// + /// Stable mission set identifier. + /// + public string Id = string.Empty; + + /// + /// Mission identifiers included by this set before extensions are applied. + /// + public List MissionIds = new(); + } + + /// + /// Runtime JSON-facing representation of a science set. + /// + public sealed class ScienceSetDefinition + { + /// + /// Stable science set identifier. + /// + public string Id = string.Empty; + + /// + /// Science experiment identifiers included by this set before extensions are applied. + /// + public List ExperimentIds = new(); + + /// + /// Science region identifiers included by this set before extensions are applied. + /// + public List ScienceRegionIds = new(); + + /// + /// Discoverable identifiers included by this set before extensions are applied. + /// + public List DiscoverableIds = new(); + } + + /// + /// Runtime JSON-facing representation of campaign pack extension add/remove operations. + /// + public sealed class CampaignPackExtensionDefinition + { + /// + /// Stable campaign pack extension identifier. + /// + public string Id = string.Empty; + + /// + /// Optional campaign pack identifier this extension applies to. + /// + public string TargetCampaignPackId = string.Empty; + + /// + /// Optional tech tree set identifier this extension applies to. + /// + public string TargetTechTreeSetId = string.Empty; + + /// + /// Optional mission set identifier this extension applies to. + /// + public string TargetMissionSetId = string.Empty; + + /// + /// Optional science set identifier this extension applies to. + /// + public string TargetScienceSetId = string.Empty; + + /// + /// Tech tree node identifiers added to matching campaign packs. + /// + public List AddTechNodeIds = new(); + + /// + /// Tech tree node identifiers removed from matching campaign packs after additions are applied. + /// + public List RemoveTechNodeIds = new(); + + /// + /// Mission identifiers added to matching campaign packs. + /// + public List AddMissionIds = new(); + + /// + /// Mission identifiers removed from matching campaign packs after additions are applied. + /// + public List RemoveMissionIds = new(); + + /// + /// Science experiment identifiers added to matching campaign packs. + /// + public List AddExperimentIds = new(); + + /// + /// Science experiment identifiers removed from matching campaign packs after additions are applied. + /// + public List RemoveExperimentIds = new(); + + /// + /// Science region identifiers added to matching campaign packs. + /// + public List AddScienceRegionIds = new(); + + /// + /// Science region identifiers removed from matching campaign packs after additions are applied. + /// + public List RemoveScienceRegionIds = new(); + + /// + /// Discoverable identifiers added to matching campaign packs. + /// + public List AddDiscoverableIds = new(); + + /// + /// Discoverable identifiers removed from matching campaign packs after additions are applied. + /// + public List RemoveDiscoverableIds = new(); + } + + /// + /// Fully resolved campaign pack contents after base sets and matching extensions are applied. + /// + public sealed class EffectiveCampaignPack + { + /// + /// Identifier of the campaign pack that was resolved. + /// + public string CampaignPackId = string.Empty; + + /// + /// Localization key used for the resolved campaign pack name. + /// + public string NameLocKey = string.Empty; + + /// + /// Localization key used for the resolved campaign pack description. + /// + public string DescriptionLocKey = string.Empty; + + /// + /// Galaxy definition key selected by the resolved campaign pack. + /// + public string GalaxyDefinitionKey = string.Empty; + + /// + /// Effective tech tree node identifiers after base sets and extensions are applied. + /// + public List TechNodeIds = new(); + + /// + /// Effective mission identifiers after base sets and extensions are applied. + /// + public List MissionIds = new(); + + /// + /// Effective science experiment identifiers after base sets and extensions are applied. + /// + public List ExperimentIds = new(); + + /// + /// Effective science region identifiers after base sets and extensions are applied. + /// + public List ScienceRegionIds = new(); + + /// + /// Effective discoverable identifiers after base sets and extensions are applied. + /// + public List DiscoverableIds = new(); + + /// + /// Identifiers of extensions that matched and were applied while resolving this campaign pack. + /// + public List AppliedExtensionIds = new(); + } +} diff --git a/Runtime/CampaignPacks/CampaignPackDefinitions.cs.meta b/Runtime/CampaignPacks/CampaignPackDefinitions.cs.meta new file mode 100644 index 0000000..23d252c --- /dev/null +++ b/Runtime/CampaignPacks/CampaignPackDefinitions.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 34a397478f6248aeb44f2bcaa77d0f61 +timeCreated: 1780215600 diff --git a/Runtime/CampaignPacks/CampaignPackRuntimeCatalog.cs b/Runtime/CampaignPacks/CampaignPackRuntimeCatalog.cs new file mode 100644 index 0000000..ffd21a1 --- /dev/null +++ b/Runtime/CampaignPacks/CampaignPackRuntimeCatalog.cs @@ -0,0 +1,445 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace PatchManager.CampaignPacks +{ + /// + /// In-memory runtime catalog for loaded campaign pack definitions and resolved previews. + /// + public sealed class CampaignPackRuntimeCatalog + { + private readonly Dictionary _packs = new(StringComparer.Ordinal); + private readonly Dictionary _techTreeSets = new(StringComparer.Ordinal); + private readonly Dictionary _missionSets = new(StringComparer.Ordinal); + private readonly Dictionary _scienceSets = new(StringComparer.Ordinal); + private readonly Dictionary _extensions = new(StringComparer.Ordinal); + private readonly List _issues = new(); + + /// + /// Loaded campaign pack definitions keyed by campaign pack id. + /// Prefer , , and + /// for gameplay-facing queries. + /// + public IReadOnlyDictionary Packs => _packs; + + /// + /// Loaded tech tree set definitions keyed by set id. + /// + public IReadOnlyDictionary TechTreeSets => _techTreeSets; + + /// + /// Loaded mission set definitions keyed by set id. + /// + public IReadOnlyDictionary MissionSets => _missionSets; + + /// + /// Loaded science set definitions keyed by set id. + /// + public IReadOnlyDictionary ScienceSets => _scienceSets; + + /// + /// Loaded campaign pack extension definitions keyed by extension id. + /// + public IReadOnlyDictionary Extensions => _extensions; + + /// + /// Load and validation issues discovered while building the catalog. + /// + public IReadOnlyList Issues => _issues; + + /// + /// Gets the loaded campaign pack identifiers in stable id order. + /// + /// Loaded campaign pack identifiers. + public IReadOnlyList GetCampaignPackIds() + { + return _packs.Keys + .OrderBy(id => id, StringComparer.Ordinal) + .ToList(); + } + + /// + /// Checks whether a campaign pack definition is loaded. + /// + /// Campaign pack identifier to check. + /// when the catalog contains the requested campaign pack. + public bool ContainsCampaignPack(string packId) + { + return !string.IsNullOrWhiteSpace(packId) && _packs.ContainsKey(packId); + } + + /// + /// Attempts to fetch the raw campaign pack definition for editor/debug style inspection. + /// + /// Campaign pack identifier to fetch. + /// Loaded campaign pack definition when found; otherwise . + /// when the definition was found. + public bool TryGetCampaignPackDefinition(string packId, out CampaignPackDefinition definition) + { + if (!string.IsNullOrWhiteSpace(packId) && _packs.TryGetValue(packId, out definition)) + { + return true; + } + + definition = null; + return false; + } + + /// + /// Adds a load-time issue to the catalog diagnostics. + /// + /// Human-readable issue text. + public void AddLoadIssue(string issue) + { + _issues.Add($"[Load] {issue}"); + } + + /// + /// Removes all loaded definitions and validation issues. + /// + public void Clear() + { + _packs.Clear(); + _techTreeSets.Clear(); + _missionSets.Clear(); + _scienceSets.Clear(); + _extensions.Clear(); + _issues.Clear(); + } + + /// + /// Adds a campaign pack definition to the catalog. + /// + /// Campaign pack definition to add. + /// Optional source asset name used for diagnostics. + public void AddPack(CampaignPackDefinition definition, string sourceName = "") + { + AddDefinition(_packs, definition?.Id, definition, "Campaign pack", sourceName); + } + + /// + /// Adds a tech tree set definition to the catalog. + /// + /// Tech tree set definition to add. + /// Optional source asset name used for diagnostics. + public void AddTechTreeSet(TechTreeSetDefinition definition, string sourceName = "") + { + AddDefinition(_techTreeSets, definition?.Id, definition, "Tech tree set", sourceName); + } + + /// + /// Adds a mission set definition to the catalog. + /// + /// Mission set definition to add. + /// Optional source asset name used for diagnostics. + public void AddMissionSet(MissionSetDefinition definition, string sourceName = "") + { + AddDefinition(_missionSets, definition?.Id, definition, "Mission set", sourceName); + } + + /// + /// Adds a science set definition to the catalog. + /// + /// Science set definition to add. + /// Optional source asset name used for diagnostics. + public void AddScienceSet(ScienceSetDefinition definition, string sourceName = "") + { + AddDefinition(_scienceSets, definition?.Id, definition, "Science set", sourceName); + } + + /// + /// Adds a campaign pack extension definition to the catalog. + /// + /// Campaign pack extension definition to add. + /// Optional source asset name used for diagnostics. + public void AddExtension(CampaignPackExtensionDefinition definition, string sourceName = "") + { + AddDefinition(_extensions, definition?.Id, definition, "Campaign pack extension", sourceName); + } + + /// + /// Resolves the effective contents for a loaded campaign pack. + /// + /// Campaign pack identifier. + /// Resolved contents, or null when the pack is unknown. + public EffectiveCampaignPack Resolve(string packId) + { + if (string.IsNullOrWhiteSpace(packId) || !_packs.TryGetValue(packId, out var pack)) + { + return null; + } + + var result = new EffectiveCampaignPack + { + CampaignPackId = pack.Id, + NameLocKey = pack.NameLocKey, + DescriptionLocKey = pack.DescriptionLocKey, + GalaxyDefinitionKey = pack.GalaxyDefinitionKey + }; + + if (!string.IsNullOrWhiteSpace(pack.TechTreeSetId) && _techTreeSets.TryGetValue(pack.TechTreeSetId, out var techTreeSet)) + { + result.TechNodeIds.AddRange(CopyDistinct(techTreeSet.TechNodeIds)); + } + + if (!string.IsNullOrWhiteSpace(pack.MissionSetId) && _missionSets.TryGetValue(pack.MissionSetId, out var missionSet)) + { + result.MissionIds.AddRange(CopyDistinct(missionSet.MissionIds)); + } + + if (!string.IsNullOrWhiteSpace(pack.ScienceSetId) && _scienceSets.TryGetValue(pack.ScienceSetId, out var scienceSet)) + { + result.ExperimentIds.AddRange(CopyDistinct(scienceSet.ExperimentIds)); + result.ScienceRegionIds.AddRange(CopyDistinct(scienceSet.ScienceRegionIds)); + result.DiscoverableIds.AddRange(CopyDistinct(scienceSet.DiscoverableIds)); + } + + var matching = GetMatchingExtensions(pack).OrderBy(e => e.Id, StringComparer.Ordinal).ToList(); + foreach (var extension in matching) + { + AddRangeUnique(result.TechNodeIds, extension.AddTechNodeIds); + AddRangeUnique(result.MissionIds, extension.AddMissionIds); + AddRangeUnique(result.ExperimentIds, extension.AddExperimentIds); + AddRangeUnique(result.ScienceRegionIds, extension.AddScienceRegionIds); + AddRangeUnique(result.DiscoverableIds, extension.AddDiscoverableIds); + if (!string.IsNullOrWhiteSpace(extension.Id)) + { + result.AppliedExtensionIds.Add(extension.Id); + } + } + + foreach (var extension in matching) + { + RemoveAll(result.TechNodeIds, extension.RemoveTechNodeIds); + RemoveAll(result.MissionIds, extension.RemoveMissionIds); + RemoveAll(result.ExperimentIds, extension.RemoveExperimentIds); + RemoveAll(result.ScienceRegionIds, extension.RemoveScienceRegionIds); + RemoveAll(result.DiscoverableIds, extension.RemoveDiscoverableIds); + } + + return result; + } + + /// + /// Attempts to resolve the effective contents for a loaded campaign pack. + /// + /// Campaign pack identifier. + /// Resolved contents when found; otherwise . + /// when the campaign pack exists and was resolved. + public bool TryResolve(string packId, out EffectiveCampaignPack effectivePack) + { + effectivePack = Resolve(packId); + return effectivePack != null; + } + + /// + /// Rebuilds runtime validation issues for all loaded definitions. + /// + public void Validate() + { + _issues.RemoveAll(issue => issue.StartsWith("[Validation]", StringComparison.Ordinal)); + + foreach (var pack in _packs.Values) + { + if (string.IsNullOrWhiteSpace(pack.GalaxyDefinitionKey)) + { + AddValidationIssue($"Campaign pack '{pack.Id}' has no galaxy definition key."); + } + + AddMissingReference(pack.Id, "tech tree set", pack.TechTreeSetId, _techTreeSets); + AddMissingReference(pack.Id, "mission set", pack.MissionSetId, _missionSets); + AddMissingReference(pack.Id, "science set", pack.ScienceSetId, _scienceSets); + } + + foreach (var extension in _extensions.Values) + { + var hasTarget = !string.IsNullOrWhiteSpace(extension.TargetCampaignPackId) || + !string.IsNullOrWhiteSpace(extension.TargetTechTreeSetId) || + !string.IsNullOrWhiteSpace(extension.TargetMissionSetId) || + !string.IsNullOrWhiteSpace(extension.TargetScienceSetId); + + if (!hasTarget) + { + AddValidationIssue($"Campaign pack extension '{extension.Id}' does not target a pack or set."); + } + else if (!_packs.Values.Any(pack => TargetsPack(extension, pack))) + { + AddValidationIssue($"Campaign pack extension '{extension.Id}' does not match any loaded campaign pack."); + } + + AddConflictIssue(extension.Id, "tech node", extension.AddTechNodeIds, extension.RemoveTechNodeIds); + AddConflictIssue(extension.Id, "mission", extension.AddMissionIds, extension.RemoveMissionIds); + AddConflictIssue(extension.Id, "science experiment", extension.AddExperimentIds, extension.RemoveExperimentIds); + AddConflictIssue(extension.Id, "science region", extension.AddScienceRegionIds, extension.RemoveScienceRegionIds); + AddConflictIssue(extension.Id, "discoverable", extension.AddDiscoverableIds, extension.RemoveDiscoverableIds); + } + } + + /// + /// Resolves all loaded campaign packs in stable id order. + /// + /// Resolved campaign pack previews. + public IReadOnlyList ResolveAll() + { + return GetCampaignPackIds() + .Select(Resolve) + .OfType() + .ToList(); + } + + /// + /// Builds a human-readable summary of loaded definitions, resolved contents, and diagnostics. + /// + /// Summary text suitable for logs and Patch Manager detail UI. + public string BuildSummaryText() + { + var sb = new StringBuilder(); + sb.AppendLine("Campaign Packs:"); + sb.AppendLine($"Packs: {_packs.Count}"); + sb.AppendLine($"Tech tree sets: {_techTreeSets.Count}"); + sb.AppendLine($"Mission sets: {_missionSets.Count}"); + sb.AppendLine($"Science sets: {_scienceSets.Count}"); + sb.AppendLine($"Extensions: {_extensions.Count}"); + + var effectivePacks = ResolveAll(); + foreach (var pack in effectivePacks) + { + sb.AppendLine(""); + sb.AppendLine($"- {pack.CampaignPackId}"); + sb.AppendLine($" Galaxy: {ValueOrNone(pack.GalaxyDefinitionKey)}"); + AppendIdList(sb, "Tech nodes", pack.TechNodeIds); + AppendIdList(sb, "Missions", pack.MissionIds); + AppendIdList(sb, "Experiments", pack.ExperimentIds); + AppendIdList(sb, "Science regions", pack.ScienceRegionIds); + AppendIdList(sb, "Discoverables", pack.DiscoverableIds); + AppendIdList(sb, "Extensions", pack.AppliedExtensionIds); + } + + if (_issues.Count > 0) + { + sb.AppendLine(""); + sb.AppendLine("Issues:"); + foreach (var issue in _issues) + { + sb.AppendLine($"- {issue}"); + } + } + + return sb.ToString(); + } + + private void AddDefinition(Dictionary target, string id, T definition, string kind, string sourceName) + where T : class + { + if (definition == null) + { + AddLoadIssue($"{kind} from '{ValueOrNone(sourceName)}' could not be read."); + return; + } + + if (string.IsNullOrWhiteSpace(id)) + { + AddLoadIssue($"{kind} from '{ValueOrNone(sourceName)}' has no id."); + return; + } + + if (target.ContainsKey(id)) + { + AddLoadIssue($"{kind} id '{id}' is duplicated; keeping the first loaded definition."); + return; + } + + target[id] = definition; + } + + private IEnumerable GetMatchingExtensions(CampaignPackDefinition pack) + { + return _extensions.Values.Where(extension => TargetsPack(extension, pack)); + } + + private static bool TargetsPack(CampaignPackExtensionDefinition extension, CampaignPackDefinition pack) + { + return Matches(extension.TargetCampaignPackId, pack.Id) || + Matches(extension.TargetTechTreeSetId, pack.TechTreeSetId) || + Matches(extension.TargetMissionSetId, pack.MissionSetId) || + Matches(extension.TargetScienceSetId, pack.ScienceSetId); + } + + private static bool Matches(string target, string actual) + { + return !string.IsNullOrWhiteSpace(target) && + !string.IsNullOrWhiteSpace(actual) && + string.Equals(target, actual, StringComparison.Ordinal); + } + + private void AddMissingReference(string packId, string kind, string id, Dictionary known) + { + if (!string.IsNullOrWhiteSpace(id) && !known.ContainsKey(id)) + { + AddValidationIssue($"Campaign pack '{packId}' references missing {kind} '{id}'."); + } + } + + private void AddConflictIssue(string extensionId, string kind, IEnumerable additions, IEnumerable removals) + { + var addSet = new HashSet((additions ?? Enumerable.Empty()).Where(id => !string.IsNullOrWhiteSpace(id))); + foreach (var id in (removals ?? Enumerable.Empty()).Where(id => !string.IsNullOrWhiteSpace(id)).Distinct()) + { + if (addSet.Contains(id)) + { + AddValidationIssue($"Campaign pack extension '{extensionId}' both adds and removes {kind} '{id}'. Removal will win."); + } + } + } + + private void AddValidationIssue(string issue) + { + _issues.Add($"[Validation] {issue}"); + } + + private static IEnumerable CopyDistinct(IEnumerable values) + { + return (values ?? Enumerable.Empty()).Where(value => !string.IsNullOrWhiteSpace(value)).Distinct(); + } + + private static void AddRangeUnique(List target, IEnumerable values) + { + if (values == null) return; + foreach (var value in values) + { + if (string.IsNullOrWhiteSpace(value) || target.Contains(value)) continue; + target.Add(value); + } + } + + private static void RemoveAll(List target, IEnumerable values) + { + if (values == null) return; + var removals = new HashSet(values.Where(value => !string.IsNullOrWhiteSpace(value))); + target.RemoveAll(removals.Contains); + } + + private static string ValueOrNone(string value) + { + return string.IsNullOrWhiteSpace(value) ? "" : value; + } + + private static void AppendIdList(StringBuilder sb, string label, IReadOnlyCollection ids) + { + sb.AppendLine($" {label} ({ids.Count}):"); + + if (ids.Count == 0) + { + sb.AppendLine(" "); + return; + } + + foreach (var id in ids) + { + sb.AppendLine($" - {id}"); + } + } + } +} diff --git a/Runtime/CampaignPacks/CampaignPackRuntimeCatalog.cs.meta b/Runtime/CampaignPacks/CampaignPackRuntimeCatalog.cs.meta new file mode 100644 index 0000000..782f9ec --- /dev/null +++ b/Runtime/CampaignPacks/CampaignPackRuntimeCatalog.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 70e3392c4d0d42cd987bc47db44667c5 \ No newline at end of file diff --git a/Runtime/CampaignPacks/CampaignPacksModule.cs b/Runtime/CampaignPacks/CampaignPacksModule.cs new file mode 100644 index 0000000..c12feba --- /dev/null +++ b/Runtime/CampaignPacks/CampaignPacksModule.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using KSP.Game; +using Newtonsoft.Json; +using PatchManager.Shared; +using PatchManager.Shared.Modules; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.ResourceManagement.AsyncOperations; +using UnityEngine.UIElements; + +namespace PatchManager.CampaignPacks +{ + /// + /// Loads baked campaign pack JSON and exposes a read-only runtime inspection summary. + /// + public sealed class CampaignPacksModule : BaseModule + { + /// + /// Addressables label used for baked campaign pack JSON assets. + /// + public const string CampaignPacksLabel = "campaign_packs"; + + /// + /// Addressables label used for baked campaign pack tech tree set JSON assets. + /// + public const string TechTreeSetsLabel = "campaign_pack_tech_tree_sets"; + + /// + /// Addressables label used for baked campaign pack mission set JSON assets. + /// + public const string MissionSetsLabel = "campaign_pack_mission_sets"; + + /// + /// Addressables label used for baked campaign pack science set JSON assets. + /// + public const string ScienceSetsLabel = "campaign_pack_science_sets"; + + /// + /// Addressables label used for baked campaign pack extension JSON assets. + /// + public const string ExtensionsLabel = "campaign_pack_extensions"; + + /// + /// Runtime catalog populated from baked campaign pack JSON assets. + /// + public static CampaignPackRuntimeCatalog Catalog { get; } = new(); + + /// + /// Gets the loaded campaign pack identifiers in stable id order. + /// + /// Loaded campaign pack identifiers. + public static IReadOnlyList GetCampaignPackIds() + { + return Catalog.GetCampaignPackIds(); + } + + /// + /// Checks whether a campaign pack definition is loaded. + /// + /// Campaign pack identifier to check. + /// when the catalog contains the requested campaign pack. + public static bool ContainsCampaignPack(string packId) + { + return Catalog.ContainsCampaignPack(packId); + } + + /// + /// Resolves the effective contents for a loaded campaign pack. + /// + /// Campaign pack identifier. + /// Resolved contents, or when the pack is unknown. + public static EffectiveCampaignPack Resolve(string packId) + { + return Catalog.Resolve(packId); + } + + /// + /// Attempts to resolve the effective contents for a loaded campaign pack. + /// + /// Campaign pack identifier. + /// Resolved contents when found; otherwise . + /// when the campaign pack exists and was resolved. + public static bool TryResolve(string packId, out EffectiveCampaignPack effectivePack) + { + return Catalog.TryResolve(packId, out effectivePack); + } + + /// + /// Resolves all loaded campaign packs in stable id order. + /// + /// Resolved campaign pack previews. + public static IReadOnlyList ResolveAll() + { + return Catalog.ResolveAll(); + } + + private int _pendingLoads; + private bool _loadStarted; + private TextElement _detailsText; + + /// + public override void Load() + { + Catalog.Clear(); + _pendingLoads = 5; + _loadStarted = true; + + LoadDefinitions( + CampaignPacksLabel, + (definition, sourceName) => Catalog.AddPack(definition, sourceName)); + LoadDefinitions( + TechTreeSetsLabel, + (definition, sourceName) => Catalog.AddTechTreeSet(definition, sourceName)); + LoadDefinitions( + MissionSetsLabel, + (definition, sourceName) => Catalog.AddMissionSet(definition, sourceName)); + LoadDefinitions( + ScienceSetsLabel, + (definition, sourceName) => Catalog.AddScienceSet(definition, sourceName)); + LoadDefinitions( + ExtensionsLabel, + (definition, sourceName) => Catalog.AddExtension(definition, sourceName)); + } + + /// + public override VisualElement GetDetails() + { + var foldout = new Foldout + { + text = "PatchManager.CampaignPacks", + visible = true, + style = + { + display = DisplayStyle.Flex + } + }; + + _detailsText = new TextElement + { + visible = true, + style = + { + display = DisplayStyle.Flex, + whiteSpace = WhiteSpace.Normal + } + }; + RefreshDetailsText(); + foldout.Add(_detailsText); + return foldout; + } + + private void LoadDefinitions(string label, Action addDefinition) + where T : class + { + var handle = GameManager.Instance.Assets.LoadAssetsAsync( + label, + asset => ImportDefinition(asset, addDefinition)); + + handle.Completed += result => + { + if (result.Status != AsyncOperationStatus.Succeeded) + { + Logging.LogWarning($"[Campaign Packs] Failed to load addressable label '{label}'."); + Catalog.AddLoadIssue($"Failed to load addressable label '{label}'."); + } + + Addressables.Release(handle); + OnLabelLoadFinished(); + }; + } + + private static void ImportDefinition(TextAsset asset, Action addDefinition) + where T : class + { + if (!asset) + { + return; + } + + try + { + var definition = JsonConvert.DeserializeObject(asset.text); + addDefinition(definition, asset.name); + } + catch (Exception ex) + { + Logging.LogWarning($"[Campaign Packs] Failed to parse '{asset.name}': {ex.Message}"); + Catalog.AddLoadIssue($"Failed to parse '{asset.name}': {ex.Message}"); + } + } + + private void OnLabelLoadFinished() + { + _pendingLoads--; + if (_pendingLoads > 0) + { + RefreshDetailsText(); + return; + } + + Catalog.Validate(); + LogSummary(); + RefreshDetailsText(); + } + + private static void LogSummary() + { + Logging.LogInfo($"[Campaign Packs]\n{Catalog.BuildSummaryText()}"); + foreach (var issue in Catalog.Issues) + { + Logging.LogWarning($"[Campaign Packs] {issue}"); + } + } + + private void RefreshDetailsText() + { + if (_detailsText == null) + { + return; + } + + var loading = _loadStarted && _pendingLoads > 0 + ? $"Loading campaign pack labels... {_pendingLoads} remaining.\n\n" + : string.Empty; + _detailsText.text = loading + Catalog.BuildSummaryText(); + } + } +} diff --git a/Runtime/CampaignPacks/CampaignPacksModule.cs.meta b/Runtime/CampaignPacks/CampaignPacksModule.cs.meta new file mode 100644 index 0000000..5adc43b --- /dev/null +++ b/Runtime/CampaignPacks/CampaignPacksModule.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 60a24681846d4179989ac79d4afe1de0 +timeCreated: 1780215600 diff --git a/Runtime/PatchManager.cs b/Runtime/PatchManager.cs index 4ec54dc..8dbc20a 100644 --- a/Runtime/PatchManager.cs +++ b/Runtime/PatchManager.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using PatchManager.CampaignPacks; using PatchManager.Core; using PatchManager.Generic; using PatchManager.LuaPatching; @@ -39,6 +40,7 @@ public void Awake() ModuleManager.Register(typeof(ResourcesModule)); ModuleManager.Register(typeof(ScienceModule)); ModuleManager.Register(typeof(PlanetsModule)); + ModuleManager.Register(typeof(CampaignPacksModule)); Logging.Initialize(SWLogger); foreach (var module in ModuleManager.Modules) {