diff --git a/sources/Commands/ACommandFeature.uc b/sources/Commands/ACommandFeature.uc index 6448aa6..16ed03c 100644 --- a/sources/Commands/ACommandFeature.uc +++ b/sources/Commands/ACommandFeature.uc @@ -17,16 +17,26 @@ * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ -class ACommandFeature extends Command; +class ACommandFeature extends Command + dependson(PendingConfigsTool); -// TODO: autoconf, newconf, deleteconf, setconf -// TODO: when displaying features - display which one is enabled +var private class selectedFeatureClass; +var private Text selectedConfigName; -var private ACommandFeature_Announcer announcer; +var private PendingConfigsTool pendingConfigs; +var private ACommandFeature_Announcer announcer; + +protected function Constructor() +{ + pendingConfigs = + PendingConfigsTool(_.memory.Allocate(class'PendingConfigsTool')); + super.Constructor(); +} protected function Finalizer() { _.memory.Free(announcer); + _.memory.Free(pendingConfigs); super.Finalizer(); } @@ -34,123 +44,271 @@ protected function BuildData(CommandDataBuilder builder) { builder.Name(P("feature")).Group(P("admin")) .Summary(P("Managing features.")) - .Describe(P("Command for displaying and enabling/disabling features.")); + .Describe(P("Command for managing features and their configs.")); builder.SubCommand(P("enable")) .ParamText(P("feature")) .OptionalParams() .ParamText(P("config")) - .Describe(P("Enables specified .")); + .Describe(P("Enables specified . If isn't specified -" + @ "choses the \"default\" one, making new config with default" + @ "settings if it doesn't exist.")); builder.SubCommand(P("disable")) .ParamText(P("feature")) .Describe(P("Disables specified .")); + builder.SubCommand(P("showconf")) + .ParamText(P("feature")) + .OptionalParams() + .ParamText(P("config")) + .Describe(P("Show given for the given .")); + builder.SubCommand(P("editconf")) + .ParamText(P("feature")) + .ParamText(P("config")) + .ParamText(P("variable_path")) + .ParamRemainder(P("value")) + .Describe(P("Changes a value inside given of the given" + @ " by setting value at JSON path to" + @ "the JSON value . Changes won't be immediately applied to" + @ "the game and kept as pending.")); + builder.SubCommand(P("saveconf")) + .ParamText(P("feature")) + .ParamText(P("config")) + .Describe(P("Saves pending changes for the given of the given" + @ ".")); + builder.SubCommand(P("newconf")) + .ParamText(P("feature")) + .ParamText(P("config")) + .Describe(P("Creates new config for the given .")); + builder.SubCommand(P("removeconf")) + .ParamText(P("feature")) + .ParamText(P("config")) + .Describe(P("Removes specified of the specified .")); + builder.SubCommand(P("autoconf")) + .ParamText(P("feature")) + .OptionalParams() + .ParamText(P("config")) + .Describe(P("Changes current auto config config of the specified" + @ ". Auto config is a config that is supposed to be" + @ "automatically enabled at the start of the Acedia, unless" + @ "otherwise specified for the loader.")); + builder.Option(P("all")) + .Describe(F("Affects subcommand {$TextEmphasis showconf} by making it" + @ "show all available configs.")); + builder.Option(P("save")) + .Describe(F("Affects subcommand {$TextEmphasis editconf} by making it" + @ "also save all pending changes.")); announcer = ACommandFeature_Announcer( _.memory.Allocate(class'ACommandFeature_Announcer')); } protected function Executed(CallData arguments, EPlayer instigator) { + local bool saveFlag, allFlag; + announcer.Setup(none, instigator, othersConsole); + saveFlag = arguments.options.HasKey(P("save")); + allFlag = arguments.options.HasKey(P("all")); + SelectFeatureAndConfig(arguments); if (arguments.subCommandName.IsEmpty()) { ShowAllFeatures(); } - else if (arguments.subCommandName.Compare(P("enable"))) - { - EnableFeature( - arguments.parameters.GetText(P("feature")), - arguments.parameters.GetText(P("config"))); + else if (arguments.subCommandName.Compare(P("enable"))) { + EnableFeature(); } else if (arguments.subCommandName.Compare(P("disable"))) { - DisableFeature(arguments.parameters.GetText(P("feature"))); + DisableFeature(); + } + else if (arguments.subCommandName.Compare(P("showconf"))) { + ShowSelectedConfigs(allFlag); + } + else if (arguments.subCommandName.Compare(P("editconf"))) + { + EditFeatureConfig( + arguments.parameters.GetText(P("variable_path")), + arguments.parameters.GetText(P("value")), + saveFlag); + } + else if (arguments.subCommandName.Compare(P("saveconf"))) { + SaveFeatureConfig(); + } + else if (arguments.subCommandName.Compare(P("newconf"))) { + NewFeatureConfig(); + } + else if (arguments.subCommandName.Compare(P("removeconf"))) { + RemoveFeatureConfig(); } + else if (arguments.subCommandName.Compare(P("autoconf"))) { + SetAutoFeatureConfig(); + } + _.memory.Free(selectedConfigName); + selectedConfigName = none; } -protected function EnableFeature(BaseText featureName, BaseText configParameter) +protected function SelectFeatureAndConfig(CallData arguments) { - local bool wasEnabled; - local Text oldConfig, newConfig; - local Feature instance; - local class featureClass; + local Text userGivenFeatureName, userGivenConfigName; - featureClass = LoadFeatureClass(featureName); - if (featureClass == none) { + userGivenFeatureName = arguments.parameters.GetText(P("feature")); + selectedFeatureClass = LoadFeatureClass(userGivenFeatureName); + if (selectedFeatureClass == none && !arguments.subCommandName.IsEmpty()) { return; } - wasEnabled = featureClass.static.IsEnabled(); - oldConfig = featureClass.static.GetCurrentConfig(); - newConfig = GetConfigFromParameter(configParameter, featureClass); + _.memory.Free(userGivenFeatureName); + userGivenConfigName = arguments.parameters.GetText(P("config")); + if (userGivenConfigName != none) + { + selectedConfigName = userGivenConfigName.LowerCopy(); + userGivenConfigName.FreeSelf(); + } + pendingConfigs.SelectConfig(selectedFeatureClass, selectedConfigName); +} + +protected function EnableFeature() +{ + local bool wasEnabled; + local Text oldConfig, newConfig; + local Feature instance; + + wasEnabled = selectedFeatureClass.static.IsEnabled(); + oldConfig = selectedFeatureClass.static.GetCurrentConfig(); + newConfig = PickConfigBasedOnParameter(); // Already enabled with the same config! if (oldConfig != none && oldConfig.Compare(newConfig, SCASE_INSENSITIVE)) { - announcer.AnnounceFailedAlreadyEnabled(featureClass, newConfig); + announcer.AnnounceFailedAlreadyEnabled(selectedFeatureClass, newConfig); _.memory.Free(newConfig); _.memory.Free(oldConfig); return; } // Try enabling and report the result - instance = featureClass.static.EnableMe(newConfig); - if (instance == none) { - announcer.AnnounceFailedCannotEnableFeature(featureClass, newConfig); + instance = selectedFeatureClass.static.EnableMe(newConfig); + if (instance == none) + { + announcer.AnnounceFailedCannotEnableFeature( + selectedFeatureClass, + newConfig); } - else if (wasEnabled) { - announcer.AnnounceSwappedConfig(featureClass, oldConfig, newConfig); + else if (wasEnabled) + { + announcer.AnnounceSwappedConfig( + selectedFeatureClass, + oldConfig, + newConfig); } else { - announcer.AnnounceEnabledFeature(featureClass, newConfig); + announcer.AnnounceEnabledFeature(selectedFeatureClass, newConfig); } _.memory.Free(newConfig); _.memory.Free(oldConfig); } -protected function DisableFeature(Text featureName) +protected function DisableFeature() { - local class featureClass; - - featureClass = LoadFeatureClass(featureName); - if (featureClass == none) { - return; - } - if (!featureClass.static.IsEnabled()) + if (!selectedFeatureClass.static.IsEnabled()) { - announcer.AnnounceFailedAlreadyDisabled(featureClass); + announcer.AnnounceFailedAlreadyDisabled(selectedFeatureClass); return; } - featureClass.static.DisableMe(); + selectedFeatureClass.static.DisableMe(); // It is possible that this command itself is destroyed after above command // so do the check just in case if (IsAllocated()) { - announcer.AnnounceDisabledFeature(featureClass); + announcer.AnnounceDisabledFeature(selectedFeatureClass); } } -protected function Text GetConfigFromParameter( - BaseText configParameter, - class featureClass) +protected function ShowSelectedConfigs(bool showAllFeatures) { - local Text resolvedConfig; + local int i; + local array availableConfigs; + local MutableText configList; local class configClass; - if (featureClass == none) { - return none; + if (showAllFeatures) + { + configClass = selectedFeatureClass.default.configClass; + if (configClass != none) { + availableConfigs = configClass.static.AvailableConfigs(); + } + for (i = 0; i < availableConfigs.length; i += 1) { + ShowFeatureConfig(availableConfigs[i]); + } + _.memory.FreeMany(availableConfigs); + return; } - configClass = featureClass.default.configClass; + if (selectedConfigName != none) + { + ShowFeatureConfig(selectedConfigName); + return; + } + configList = PrintConfigList(selectedFeatureClass); + callerConsole + .Flush() + .Write(P("Available configs: ")) + .WriteLine(configList); + _.memory.Free(configList); +} + +protected function ShowFeatureConfig(BaseText configName) +{ + local MutableText dataAsJSON; + local HashTable currentData, pendingData; + + if (configName == none) { + return; + } + currentData = GetCurrentConfigData(configName); + if (currentData == none) + { + announcer.AnnounceFailedNoDataForConfig( + selectedFeatureClass, + configName); + return; + } + // Display current data + dataAsJSON = _.json.PrettyPrint(currentData); + announcer.AnnounceCurrentConfig(selectedFeatureClass, configName); + callerConsole.Flush().WriteLine(dataAsJSON); + _.memory.Free(dataAsJSON); + // Display pending data + pendingConfigs.SelectConfig(selectedFeatureClass, configName); + pendingData = pendingConfigs.GetPendingConfigData(); + if (pendingData != none) + { + dataAsJSON = _.json.PrettyPrint(pendingData); + announcer.AnnouncePendingConfig( + selectedFeatureClass, + configName); + callerConsole.Flush().WriteLine(dataAsJSON); + _.memory.Free(dataAsJSON); + } + _.memory.Free(pendingData); + _.memory.Free(currentData); +} + +protected function Text PickConfigBasedOnParameter() +{ + local Text resolvedConfig; + local class configClass; + + configClass = selectedFeatureClass.default.configClass; if (configClass == none) { - announcer.AnnounceFailedNoConfigClass(featureClass); + announcer.AnnounceFailedNoConfigClass(selectedFeatureClass); return none; } // If config was specified - simply check that it exists - if (configParameter != none) + if (selectedConfigName != none) { - if (configClass.static.Exists(configParameter)) { - return configParameter.Copy(); + if (configClass.static.Exists(selectedConfigName)) { + return selectedConfigName.Copy(); } - announcer.AnnounceFailedConfigMissing(configParameter); + announcer.AnnounceFailedConfigMissing(selectedConfigName); return none; } // If it wasn't specified - try auto config instead resolvedConfig = configClass.static.GetAutoEnabledConfig(); if (resolvedConfig == none) { - announcer.AnnounceFailedNoConfigProvided(featureClass); + announcer.AnnounceFailedNoConfigProvided(selectedFeatureClass); } return resolvedConfig; } @@ -186,59 +344,299 @@ protected function ShowAllFeatures() } } -protected function ShowFeature(class feature) +protected function ShowFeature(class featureClass) { - local int i; - local Text autoConfig; - local MutableText featureName, builder; - local ListBuilder configList; - local array availableConfigs; - local class configClass; + local MutableText featureName; + local MutableText configList; - if (feature == none) { + if (featureClass == none) { return; } - configClass = feature.default.configClass; - if (configClass != none) { - availableConfigs = configClass.static.AvailableConfigs(); - } featureName = _.text - .FromClassM(feature) + .FromClassM(featureClass) .ChangeDefaultColor(_.color.TextEmphasis); - builder = _.text.Empty(); - if (feature.static.IsEnabled()) { - builder.Append(F("[ {$TextPositive enabled} ] ")); + configList = PrintConfigList(featureClass); + callerConsole.Flush(); + if (featureClass.static.IsEnabled()) { + callerConsole.Write(F("[ {$TextPositive enabled} ] ")); } else { - builder.Append(F("[ {$TextNegative disabled} ] ")); + callerConsole.Write(F("[ {$TextNegative disabled} ] ")); } - builder.Append(featureName); + callerConsole.Write(featureName) + .Write(P(" with configs: ")) + .WriteLine(configList); _.memory.Free(featureName); - if (availableConfigs.length == 1) { - builder.Append(P(" with config:")); - } - else if (availableConfigs.length > 1) { - builder.Append(P(" with configs:")); - } - callerConsole.Write(builder); - _.memory.Free(builder); - configList = ListBuilder(_.memory.Allocate(class'ListBuilder')); - autoConfig = configClass.static.GetAutoEnabledConfig(); + _.memory.Free(configList); +} + +protected function MutableText PrintConfigList(class featureClass) +{ + local int i; + local Text autoConfig; + local ListBuilder configList; + local MutableText result, nextConfig; + local array availableConfigs; + local class configClass; + + if (featureClass == none) return none; + configClass = featureClass.default.configClass; + if (configClass == none) return none; + + availableConfigs = configClass.static.AvailableConfigs(); + autoConfig = configClass.static.GetAutoEnabledConfig(); + configList = ListBuilder(_.memory.Allocate(class'ListBuilder')); for (i = 0; i < availableConfigs.length; i += 1) { - configList.Item(availableConfigs[i]); + nextConfig = availableConfigs[i].MutableCopy(); + if (pendingConfigs.HasPendingConfigFor(featureClass, nextConfig)) { + nextConfig.Append(F("{$TextEmphasis *}")); + } + configList.Item(nextConfig); + _.memory.Free(nextConfig); if ( autoConfig != none && autoConfig.Compare(availableConfigs[i], SCASE_INSENSITIVE)) { configList.Comment(F("{$TextPositive auto enabled}")); } } - builder = configList.GetMutable(); - callerConsole.WriteLine(builder); - _.memory.FreeMany(availableConfigs); + result = configList.GetMutable(); _.memory.Free(configList); _.memory.Free(autoConfig); - _.memory.Free(builder); + _.memory.FreeMany(availableConfigs); + return result; +} + +protected function MutableText PrettyPrintValueAt(BaseText pathToValue) +{ + local MutableText printedValue; + local AcediaObject value; + local HashTable relevantData; + + relevantData = pendingConfigs.GetPendingConfigData(); + if (relevantData == none) { + relevantData = GetCurrentSelectedConfigData(); + } + if (relevantData != none) { + value = relevantData.GetItemBy(pathToValue); + } + if (value != none) + { + printedValue = _.json.PrettyPrint(value); + _.memory.Free(value); + } + _.memory.Free(relevantData); + return printedValue; +} + +protected function EditFeatureConfig( + BaseText pathToValue, + BaseText newValue, + bool saveConfig) +{ + local MutableText printedOldValue; + local MutableText printedNewValue; + local PendingConfigsTool.PendingConfigToolResult error; + + printedOldValue = PrettyPrintValueAt(pathToValue); + error = pendingConfigs.EditConfig(pathToValue, newValue); + if (error == PCTE_None) { + printedNewValue = PrettyPrintValueAt(pathToValue); + } + if (error == PCTE_ConfigMissing) { + announcer.AnnounceFailedConfigMissing(selectedConfigName); + } + else if (error == PCTE_ExpectedObject) { + announcer.AnnounceFailedExpectedObject(); + } + else if (error == PCTE_BadPointer) + { + announcer.AnnounceFailedBadPointer( + selectedFeatureClass, + selectedConfigName, + pathToValue); + } + else if (printedOldValue == none) + { + announcer.AnnounceConfigNewValue( + selectedFeatureClass, + selectedConfigName, + pathToValue, + printedNewValue); + } + else + { + announcer.AnnounceConfigEdited( + selectedFeatureClass, + selectedConfigName, + pathToValue, + printedOldValue, + printedNewValue); + } + if (saveConfig && error == PCTE_None) { + SaveFeatureConfig(); + } + _.memory.Free(printedOldValue); + _.memory.Free(printedNewValue); +} + +protected function SaveFeatureConfig() +{ + local BaseText enabledConfigName; + local HashTable pendingData; + local class configClass; + + configClass = selectedFeatureClass.default.configClass; + if (configClass == none) + { + announcer.AnnounceFailedNoConfigClass(selectedFeatureClass); + return; + } + pendingData = pendingConfigs.GetPendingConfigData(); + if (pendingData == none) + { + announcer.AnnounceFailedPendingConfigMissing(selectedConfigName); + return; + } + // Make sure config exists + configClass.static.NewConfig(selectedConfigName); + configClass.static.SaveData(selectedConfigName, pendingData); + // Re-apply config if it is active? + enabledConfigName = selectedFeatureClass.static.GetCurrentConfig(); + if (selectedConfigName.Compare(enabledConfigName, SCASE_INSENSITIVE)) + { + selectedFeatureClass.static.EnableMe(selectedConfigName); + announcer.AnnouncePublicPendingConfigSaved(selectedFeatureClass); + } + else { + announcer.AnnouncePrivatePendingConfigSaved(selectedFeatureClass); + } + _.memory.Free(enabledConfigName); + pendingData.FreeSelf(); + pendingConfigs.RemoveConfig(); + return; +} + +protected function NewFeatureConfig() +{ + local BaseText enabledConfigName; + local class configClass; + + configClass = selectedFeatureClass.default.configClass; + if (configClass == none) + { + announcer.AnnounceFailedNoConfigClass(selectedFeatureClass); + return; + } + if (configClass.static.Exists(selectedConfigName)) + { + announcer.AnnounceFailedConfigAlreadyExists( + selectedFeatureClass, + selectedConfigName); + return; + } + if (!configClass.static.NewConfig(selectedConfigName)) + { + announcer.AnnounceFailedBadConfigName(selectedConfigName); + return; + } + enabledConfigName = selectedFeatureClass.static.GetCurrentConfig(); + if (selectedConfigName.Compare(enabledConfigName, SCASE_INSENSITIVE)) + { + selectedFeatureClass.static.EnableMe(selectedConfigName); + announcer.AnnouncePublicPendingConfigSaved(selectedFeatureClass); + } + _.memory.Free(enabledConfigName); + announcer.AnnounceConfigCreated(selectedFeatureClass, selectedConfigName); +} + +protected function RemoveFeatureConfig() +{ + local class configClass; + + configClass = selectedFeatureClass.default.configClass; + if (configClass == none) + { + announcer.AnnounceFailedNoConfigClass(selectedFeatureClass); + return; + } + if (!configClass.static.Exists(selectedConfigName)) + { + announcer.AnnounceFailedConfigDoesNotExist( + selectedFeatureClass, + selectedConfigName); + return; + } + pendingConfigs.RemoveConfig(); + configClass.static.DeleteConfig(selectedConfigName); + announcer.AnnounceConfigRemoved(selectedFeatureClass, selectedConfigName); +} + +protected function SetAutoFeatureConfig() +{ + local Text currentAutoEnabledConfig; + local class configClass; + + configClass = selectedFeatureClass.default.configClass; + if (configClass == none) + { + announcer.AnnounceFailedNoConfigClass(selectedFeatureClass); + return; + } + if ( selectedConfigName != none + && !configClass.static.Exists(selectedConfigName)) + { + announcer.AnnounceFailedConfigDoesNotExist( + selectedFeatureClass, + selectedConfigName); + return; + } + currentAutoEnabledConfig = configClass.static.GetAutoEnabledConfig(); + if (selectedConfigName == none && currentAutoEnabledConfig == none) { + announcer.AnnounceFailedAlreadyNoAutoEnabled(selectedFeatureClass); + } + else if (selectedConfigName != none && + selectedConfigName.Compare(currentAutoEnabledConfig, SCASE_INSENSITIVE)) + { + announcer.AnnounceFailedAlreadySameAutoEnabled( + selectedFeatureClass, + selectedConfigName); + } + else + { + configClass.static.SetAutoEnabledConfig(selectedConfigName); + if (selectedConfigName != none) + { + announcer.AnnounceAutoEnabledConfig( + selectedFeatureClass, + selectedConfigName); + } + else { + announcer.AnnounceRemovedAutoEnabledConfig(selectedFeatureClass); + } + } + _.memory.Free(currentAutoEnabledConfig); +} + +private function HashTable GetCurrentConfigData(BaseText configName) +{ + local class configClass; + + if (configName == none) { + return none; + } + configClass = selectedFeatureClass.default.configClass; + if (configClass == none) + { + announcer.AnnounceFailedNoConfigClass(selectedFeatureClass); + return none; + } + return configClass.static.LoadData(configName); +} + +private function HashTable GetCurrentSelectedConfigData() +{ + return GetCurrentConfigData(selectedConfigName); } defaultproperties diff --git a/sources/Commands/ACommandFeature_Announcer.uc b/sources/Commands/ACommandFeature_Announcer.uc index e220c8a..599b5e7 100644 --- a/sources/Commands/ACommandFeature_Announcer.uc +++ b/sources/Commands/ACommandFeature_Announcer.uc @@ -20,25 +20,57 @@ class ACommandFeature_Announcer extends CommandAnnouncer; var private AnnouncementVariations enabledFeature, disabledFeature; -var private AnnouncementVariations swappedConfig; +var private AnnouncementVariations swappedConfig, pendingConfigSaved; +var private AnnouncementVariations showCurrentConfig, showPendingConfig; +var private AnnouncementVariations configCreated, configRemoved, configEdited; +var private AnnouncementVariations configEditedNew; +var private AnnouncementVariations pendingConfigSavedPublic; +var private AnnouncementVariations pendingConfigSavedPrivate; +var private AnnouncementVariations autoEnabled, removedAutoEnabled; +var private AnnouncementVariations failedAlreadyNoAutoEnabled; +var private AnnouncementVariations failedAlreadySameAutoEnabled; +var private AnnouncementVariations failedConfigAlreadyExists; +var private AnnouncementVariations failedConfigDoesNotExists; var private AnnouncementVariations failedToLoadFeatureClass; var private AnnouncementVariations failedNoConfigProvided, failedConfigMissing; var private AnnouncementVariations failedCannotEnableFeature; -var private AnnouncementVariations failedNoConfigClass; +var private AnnouncementVariations failedNoConfigClass, failedBadConfigName; var private AnnouncementVariations failedAlreadyEnabled, failedAlreadyDisabled; +var private AnnouncementVariations failedNoDataForConfig, failedExpectedObject; +var private AnnouncementVariations failedBadPointer, failedPendingConfigMissing; protected function Finalizer() { FreeVariations(enabledFeature); FreeVariations(disabledFeature); FreeVariations(swappedConfig); + FreeVariations(pendingConfigSaved); + FreeVariations(showCurrentConfig); + FreeVariations(showPendingConfig); + FreeVariations(configCreated); + FreeVariations(configRemoved); + FreeVariations(configEdited); + FreeVariations(configEditedNew); + FreeVariations(pendingConfigSavedPublic); + FreeVariations(pendingConfigSavedPrivate); + FreeVariations(autoEnabled); + FreeVariations(removedAutoEnabled); + FreeVariations(failedAlreadyNoAutoEnabled); + FreeVariations(failedAlreadySameAutoEnabled); + FreeVariations(failedConfigAlreadyExists); + FreeVariations(failedConfigDoesNotExists); FreeVariations(failedToLoadFeatureClass); FreeVariations(failedNoConfigProvided); FreeVariations(failedConfigMissing); FreeVariations(failedCannotEnableFeature); FreeVariations(failedNoConfigClass); + FreeVariations(failedBadConfigName); FreeVariations(failedAlreadyEnabled); FreeVariations(failedAlreadyDisabled); + FreeVariations(failedNoDataForConfig); + FreeVariations(failedExpectedObject); + FreeVariations(failedBadPointer); + FreeVariations(failedPendingConfigMissing); super.Finalizer(); } @@ -117,6 +149,268 @@ public final function AnnounceSwappedConfig( MakeAnnouncement(swappedConfig); } +public final function AnnouncePublicPendingConfigSaved( + class featureClass) +{ + local int i; + local array templates; + + if (!pendingConfigSavedPublic.initialized) + { + pendingConfigSavedPublic.initialized = true; + pendingConfigSavedPublic.toSelfReport = _.text.MakeTemplate_S( + "Active config for feature {$TextEmphasis `%1`} was" + @ "{$TextNeutral modified}"); + pendingConfigSavedPublic.toSelfPublic = _.text.MakeTemplate_S( + "%%instigator%% {$TextNeutral modified} active config for feature" + @ "{$TextEmphasis `%1`}"); + } + templates = MakeArray(pendingConfigSavedPublic); + for (i = 0; i < templates.length; i += 1) { + templates[i].Reset().ArgClass(featureClass); + } + MakeAnnouncement(pendingConfigSavedPublic); +} + +public final function AnnouncePrivatePendingConfigSaved( + class featureClass) +{ + if (!pendingConfigSavedPrivate.initialized) + { + pendingConfigSavedPrivate.initialized = true; + pendingConfigSavedPrivate.toSelfReport = _.text.MakeTemplate_S( + "Active config for feature {$TextEmphasis `%1`} was" + @ "{$TextNeutral modified}"); + } + pendingConfigSavedPrivate.toSelfReport + .Reset() + .ArgClass(featureClass); + MakeAnnouncement(pendingConfigSavedPrivate); +} + +public final function AnnounceCurrentConfig( + class featureClass, + BaseText config) +{ + if (!showCurrentConfig.initialized) + { + showCurrentConfig.initialized = true; + showCurrentConfig.toSelfReport = _.text.MakeTemplate_S( + "Current config \"%2\" for feature {$TextEmphasis `%1`}:"); + } + showCurrentConfig.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config); + MakeAnnouncement(showCurrentConfig); +} + +public final function AnnouncePendingConfig( + class featureClass, + BaseText config) +{ + if (!showPendingConfig.initialized) + { + showPendingConfig.initialized = true; + showPendingConfig.toSelfReport = _.text.MakeTemplate_S( + "Pending config \"%2\" for feature {$TextEmphasis `%1`}:"); + } + showPendingConfig.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config); + MakeAnnouncement(showPendingConfig); +} + +public final function AnnounceConfigCreated( + class featureClass, + BaseText config) +{ + if (!configCreated.initialized) + { + configCreated.initialized = true; + configCreated.toSelfReport = _.text.MakeTemplate_S( + "{$TextPositive Created config} \"%2\" for feature" + @ "{$TextEmphasis `%1`}"); + } + configCreated.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config); + MakeAnnouncement(configCreated); +} + +public final function AnnounceConfigRemoved( + class featureClass, + BaseText config) +{ + if (!configRemoved.initialized) + { + configRemoved.initialized = true; + configRemoved.toSelfReport = _.text.MakeTemplate_S( + "{$TextNegative Removed config} \"%2\" for feature" + @ "{$TextEmphasis `%1`}"); + } + configRemoved.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config); + MakeAnnouncement(configRemoved); +} + +public final function AnnounceConfigEdited( + class featureClass, + BaseText config, + BaseText pathToValue, + BaseText oldValue, + BaseText newValue) +{ + if (!configEdited.initialized) + { + configEdited.initialized = true; + configEdited.toSelfReport = _.text.MakeTemplate_S( + "{$TextNeutral Edited config} \"%2\" for feature" + @ "{$TextEmphasis `%1`} by replacing old value %4 at \"%3\"" + @ "with new value %5"); + } + configEdited.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config) + .Arg(pathToValue) + .Arg(oldValue) + .Arg(newValue); + MakeAnnouncement(configEdited); +} + +public final function AnnounceConfigNewValue( + class featureClass, + BaseText config, + BaseText pathToValue, + BaseText newValue) +{ + if (!configEditedNew.initialized) + { + configEditedNew.initialized = true; + configEditedNew.toSelfReport = _.text.MakeTemplate_S( + "{$TextNeutral Edited config} \"%2\" for feature" + @ "{$TextEmphasis `%1`} by adding value %4 at \"%3\""); + } + configEditedNew.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config) + .Arg(pathToValue) + .Arg(newValue); + MakeAnnouncement(configEditedNew); +} + +public final function AnnounceAutoEnabledConfig( + class featureClass, + BaseText config) +{ + if (!autoEnabled.initialized) + { + autoEnabled.initialized = true; + autoEnabled.toSelfReport = _.text.MakeTemplate_S( + "Config \"%2\" for feature {$TextEmphasis `%1`} will now be" + @ "{$TextPositive auto-enabled}!"); + } + autoEnabled.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config); + MakeAnnouncement(autoEnabled); +} + +public final function AnnounceRemovedAutoEnabledConfig( + class featureClass) +{ + if (!removedAutoEnabled.initialized) + { + removedAutoEnabled.initialized = true; + removedAutoEnabled.toSelfReport = _.text.MakeTemplate_S( + "No config for feature {$TextEmphasis `%1`} will now be" + @ "{$TextPositive auto-enabled}!"); + } + removedAutoEnabled.toSelfReport + .Reset() + .ArgClass(featureClass); + MakeAnnouncement(removedAutoEnabled); +} + +public final function AnnounceFailedAlreadyNoAutoEnabled( + class featureClass) +{ + if (!failedAlreadyNoAutoEnabled.initialized) + { + failedAlreadyNoAutoEnabled.initialized = true; + failedAlreadyNoAutoEnabled.toSelfReport = _.text.MakeTemplate_S( + "{$TextFailure Cannot remove} auto-enabled config status for" + @ "feature {$TextEmphasis `%1`}: it already has" + @ "{$TextNeutral no auto-enabled config}!"); + } + failedAlreadyNoAutoEnabled.toSelfReport + .Reset() + .ArgClass(featureClass); + MakeAnnouncement(failedAlreadyNoAutoEnabled); +} + +public final function AnnounceFailedAlreadySameAutoEnabled( + class featureClass, + BaseText config) +{ + if (!failedAlreadySameAutoEnabled.initialized) + { + failedAlreadySameAutoEnabled.initialized = true; + failedAlreadySameAutoEnabled.toSelfReport = _.text.MakeTemplate_S( + "{$TextFailure Cannot make} config \"%2\" auto-enabled for feature" + @ "{$TextEmphasis `%1`}: it already" + @ "{$TextNeutral is auto-enabled}!"); + } + failedAlreadySameAutoEnabled.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config); + MakeAnnouncement(failedAlreadySameAutoEnabled); +} + +public final function AnnounceFailedConfigAlreadyExists( + class featureClass, + BaseText config) +{ + if (!failedConfigAlreadyExists.initialized) + { + failedConfigAlreadyExists.initialized = true; + failedConfigAlreadyExists.toSelfReport = _.text.MakeTemplate_S( + "Config \"%2\" for feature {$TextEmphasis `%1`}" + @ "{$TextFailure already exists}"); + } + failedConfigAlreadyExists.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config); + MakeAnnouncement(failedConfigAlreadyExists); +} + + public final function AnnounceFailedConfigDoesNotExist( + class featureClass, + BaseText config) +{ + if (!failedConfigDoesNotExists.initialized) + { + failedConfigDoesNotExists.initialized = true; + failedConfigDoesNotExists.toSelfReport = _.text.MakeTemplate_S( + "Config \"%2\" for feature {$TextEmphasis `%1`}" + @ "{$TextFailure doesn't exist}"); + } + failedConfigDoesNotExists.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config); + MakeAnnouncement(failedConfigDoesNotExists); +} + public final function AnnounceFailedToLoadFeatureClass(BaseText failedClassName) { if (!failedToLoadFeatureClass.initialized) @@ -136,7 +430,7 @@ public final function AnnounceFailedNoConfigProvided( { failedNoConfigProvided.initialized = true; failedNoConfigProvided.toSelfReport = _.text.MakeTemplate_S( - "{$TextFailue No config specified} and {$TextFailure no" + "{$TextFailure No config specified} and {$TextFailure no" @ "auto-enabled config} exists for feature {$TextEmphasis `%1`}"); } failedNoConfigProvided.toSelfReport.Reset().ArgClass(featureClass); @@ -149,12 +443,25 @@ public final function AnnounceFailedConfigMissing(BaseText config) { failedConfigMissing.initialized = true; failedConfigMissing.toSelfReport = _.text.MakeTemplate_S( - "Specified config \"%1\" {$TextFailue doesn't exist}"); + "Specified config \"%1\" {$TextFailure doesn't exist}"); } failedConfigMissing.toSelfReport.Reset().Arg(config); MakeAnnouncement(failedConfigMissing); } +public final function AnnounceFailedPendingConfigMissing(BaseText config) +{ + if (!failedPendingConfigMissing.initialized) + { + failedPendingConfigMissing.initialized = true; + failedPendingConfigMissing.toSelfReport = _.text.MakeTemplate_S( + "Specified config \"%1\" {$TextFailure doesn't have} any pending" + @ "changes"); + } + failedPendingConfigMissing.toSelfReport.Reset().Arg(config); + MakeAnnouncement(failedPendingConfigMissing); +} + public final function AnnounceFailedCannotEnableFeature( class featureClass, BaseText config) @@ -180,14 +487,26 @@ public final function AnnounceFailedNoConfigClass( { failedNoConfigClass.initialized = true; failedNoConfigClass.toSelfReport = _.text.MakeTemplate_S( - "Feature {$TextEmphasis `%1`} {$TextFailure does not have config" - @ "class}! This is most likely caused by its faulty" + "Feature {$TextEmphasis `%1`} {$TextFailure does not have} config" + @ "class! This is most likely caused by its faulty" @ "implementation"); } failedNoConfigClass.toSelfReport.Reset().ArgClass(featureClass); MakeAnnouncement(failedNoConfigClass); } +public final function AnnounceFailedBadConfigName(BaseText configName) +{ + if (!failedBadConfigName.initialized) + { + failedBadConfigName.initialized = true; + failedBadConfigName.toSelfReport = _.text.MakeTemplate_S( + "{$TextFailure Cannot create} a config with invalid name \"%1\""); + } + failedBadConfigName.toSelfReport.Reset().Arg(configName); + MakeAnnouncement(failedBadConfigName); +} + public final function AnnounceFailedAlreadyDisabled( class featureClass) { @@ -195,7 +514,8 @@ public final function AnnounceFailedAlreadyDisabled( { failedAlreadyDisabled.initialized = true; failedAlreadyDisabled.toSelfReport = _.text.MakeTemplate_S( - "Feature {$TextEmphasis `%1`} is already {$TextNegative disabled}"); + "{$TextFailure Cannot disable} feature {$TextEmphasis `%1`}: it is" + @ "already {$TextNegative disabled}"); } failedAlreadyDisabled.toSelfReport.Reset().ArgClass(featureClass); MakeAnnouncement(failedAlreadyDisabled); @@ -209,8 +529,8 @@ public final function AnnounceFailedAlreadyEnabled( { failedAlreadyEnabled.initialized = true; failedAlreadyEnabled.toSelfReport = _.text.MakeTemplate_S( - "Feature {$TextEmphasis `%1`} is already {$TextNegative enabled}" - @ "with specified config \"%2\""); + "{$TextFailure Cannot enable} feature {$TextEmphasis `%1`}: it is" + @ "already {$TextPositive enabled} with specified config \"%2\""); } failedAlreadyEnabled.toSelfReport .Reset() @@ -219,6 +539,56 @@ public final function AnnounceFailedAlreadyEnabled( MakeAnnouncement(failedAlreadyEnabled); } +public final function AnnounceFailedNoDataForConfig( + class featureClass, + BaseText config) +{ + if (!failedNoDataForConfig.initialized) + { + failedNoDataForConfig.initialized = true; + failedNoDataForConfig.toSelfReport = _.text.MakeTemplate_S( + "Feature {$TextEmphasis `%1`} is {$TextFailure missing data} for" + @ "config \"%2\""); + } + failedNoDataForConfig.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config); + MakeAnnouncement(failedNoDataForConfig); +} + +public final function AnnounceFailedExpectedObject() +{ + if (!failedExpectedObject.initialized) + { + failedExpectedObject.initialized = true; + failedExpectedObject.toSelfReport = _.text.MakeTemplate_S( + "Value change {$TextFailure failed}, because when changing" + @ "the value of the whole config, a JSON object must be provided"); + } + MakeAnnouncement(failedExpectedObject); +} + +public final function AnnounceFailedBadPointer( + class featureClass, + BaseText config, + BaseText pointer) +{ + if (!failedBadPointer.initialized) + { + failedBadPointer.initialized = true; + failedBadPointer.toSelfReport = _.text.MakeTemplate_S( + "Provided JSON pointer \"%3\" is {$TextFailure invalid} for config" + @ "\"%2\" of feature {$TextEmphasis `%1`}"); + } + failedBadPointer.toSelfReport + .Reset() + .ArgClass(featureClass) + .Arg(config) + .Arg(pointer); + MakeAnnouncement(failedBadPointer); +} + defaultproperties { } \ No newline at end of file diff --git a/sources/Tools/PendingConfigsTool.uc b/sources/Tools/PendingConfigsTool.uc new file mode 100644 index 0000000..b938747 --- /dev/null +++ b/sources/Tools/PendingConfigsTool.uc @@ -0,0 +1,353 @@ +/** + * Auxiliary object for `ACommandFeature` to help with managing pending + * configs for `Feature`s. Pending configs are `HashTable`s with config data + * that are yet to be applied to configs and `Feature`s. They allow users to + * make several changes to the data before actually applying changes to + * the gameplay. + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class PendingConfigsTool extends AcediaObject; + +/** + * This tool works by selecting feature (by class) and config (by `Text` + * name) on which it will operate with `SelectConfig()` method and then + * invoking the rest of its methods on these selections. + * There are some expections (`HasPendingConfigFor()` method) that + * explicitly take these values as parameters. + * This tool is supposed to be created once for the "feature" command and + * have its `SelectConfig()` called each execution with user-specified + * parameters. + */ +var private class selectedFeatureClass; +var private Text selectedConfigName; + +// Possible errors that might occur when working with pending configs +enum PendingConfigToolResult +{ + // No error + PCTE_None, + // Pending version of specified config does not exist + PCTE_ConfigMissing, + // JSON object (`HashTable`) was expected as a parameter for the operation, + // but something else was given + PCTE_ExpectedObject, + // Specified JSON pointer points an non-existent location + PCTE_BadPointer +}; + +struct PendingConfigs +{ + var class featureClass; + var HashTable pendingSaves; +}; +var private array featurePendingEdits; + +protected function Finalizer() +{ + local int i; + + for (i = 0; i < featurePendingEdits.length; i ++) { + _.memory.Free(featurePendingEdits[i].pendingSaves); + } + featurePendingEdits.length = 0; +} + +/** + * Selects feature and config to perform all future operations on. + * + * @param featureClass Class of the feature for which to edit pending + * configs. + * @param configName Name of the pending config that caller tool will + * work with. + */ +public final function SelectConfig( + class featureClass, + BaseText configName) +{ + _.memory.Free(selectedConfigName); + selectedFeatureClass = featureClass; + selectedConfigName = none; + if (configName != none) { + selectedConfigName = configName.LowerCopy(); + } +} + +/** + * Checks wither caller tool has recorded pending config named `configName` + * for `Feature` defined by class `featureClass`. + * This method does no checks regarding existence of an actual config for + * the specified `Feature` - it only checks whether caller tool has pending + * version. + * + * @param featureClass Class of the `Feature` to check for pending config. + * @param configName Name of the config to check for existence of its + * pending version. + * @return `true` if specified pending config exists and `false` otherwise. + */ +public function bool HasPendingConfigFor( + class featureClass, + BaseText configName) +{ + local int i; + local bool result; + local Text lowerCaseConfigName; + + if (featureClass == none) return false; + if (configName == none) return false; + + for (i = 0; i < featurePendingEdits.length; i ++) + { + if (featurePendingEdits[i].featureClass == featureClass) + { + lowerCaseConfigName = configName.LowerCopy(); + result = featurePendingEdits[i].pendingSaves + .HasKey(lowerCaseConfigName); + lowerCaseConfigName.FreeSelf(); + return result; + } + } + return false; +} + +/** + * Returns data recorded for the selected pending config inside caller tool. + * + * @param createIfMissing Method only returns data of the pending version of + * the config and if selected config does not yet have a pending version, + * it will, by default, return `none`. This parameter allows this method to + * create a pending config, based on current config with selected name + * (if it exists). + * @return Data recorded for the selected pending config. If selected config + * does not have a pending version, `createIfMissing` is set to `false` + * or not even current config with selected name exists - method returns + * `none`. + */ +public function HashTable GetPendingConfigData(optional bool createIfMissing) +{ + local int editsIndex; + local HashTable result; + local PendingConfigs newRecord; + + if (selectedConfigName == none) { + return none; + } + editsIndex = GetPendingConfigDataIndex(); + if (editsIndex >= 0) + { + result = featurePendingEdits[editsIndex] + .pendingSaves + .GetHashTable(selectedConfigName); + if (result != none) { + return result; + } + } + if (createIfMissing) + { + if (editsIndex < 0) + { + editsIndex = featurePendingEdits.length; + newRecord.featureClass = selectedFeatureClass; + newRecord.pendingSaves = _.collections.EmptyHashTable(); + featurePendingEdits[editsIndex] = newRecord; + } + result = GetCurrentConfigData(); + if (result != none) + { + featurePendingEdits[editsIndex] + .pendingSaves + .SetItem(selectedConfigName, result); + } + } + return result; +} + +/** + * Makes and edit to the config. + * + * @param pathToValue JSON path at which to make a change. + * @param newValue Value to record at the specified path. + * @return Result of the operation that reports any errors that have occured. + * Any changes are made iff result is `PCTE_None`. + */ +public function PendingConfigToolResult EditConfig( + BaseText pathToValue, + BaseText newValue) +{ + local HashTable pendingData; + local JSONPointer pointer; + local Parser parser; + local AcediaObject newValueAsJSON; + local PendingConfigToolResult result; + + if (pathToValue == none) { + return PCTE_BadPointer; + } + pendingData = GetPendingConfigData(true); + if (pendingData == none) { + return PCTE_ConfigMissing; + } + // Get guaranteed not-`none` JSON value, treating it as JSON string + // if necessary + parser = _.text.Parse(newValue); + newValueAsJSON = _.json.ParseWith(parser); + parser.FreeSelf(); + if (newValueAsJSON == none && newValue != none) { + newValueAsJSON = newValue.Copy(); + } + // Set new data + pointer = _.json.Pointer(pathToValue); + result = SetItemByJSON(pendingData, pointer, newValueAsJSON); + pointer.FreeSelf(); + pendingData.FreeSelf(); + _.memory.Free(newValueAsJSON); + return result; +} + +private function PendingConfigToolResult SetItemByJSON( + HashTable data, + JSONPointer pointer, + AcediaObject jsonValue) +{ + local Text containerIndex; + local AcediaObject container; + local PendingConfigToolResult result; + + if (pointer.IsEmpty()) + { + if (HashTable(jsonValue) != none) + { + result = ChangePendingConfigData(HashTable(jsonValue)); + _.memory.Free(jsonValue); + return result; + } + _.memory.Free(jsonValue); + return PCTE_ExpectedObject; + } + // Since `!pointer.IsEmpty()`, we are guaranteed to pop a valid value + containerIndex = pointer.Pop(); + container = data.GetItemByJSON(pointer); + if (container == none) + { + containerIndex.FreeSelf(); + return PCTE_BadPointer; + } + result = SetContainerItemByText(container, containerIndex, jsonValue); + containerIndex.FreeSelf(); + container.FreeSelf(); + return result; +} + +private function PendingConfigToolResult SetContainerItemByText( + AcediaObject container, + BaseText containerIndex, + AcediaObject jsonValue) +{ + local int arrayIndex; + local Parser parser; + local ArrayList arrayListContainer; + local HashTable hashTableContainer; + + hashTableContainer = HashTable(container); + arrayListContainer = ArrayList(container); + if (hashTableContainer != none) { + hashTableContainer.SetItem(containerIndex, jsonValue); + } + if (arrayListContainer != none) + { + parser = containerIndex.Parse(); + if (parser.MInteger(arrayIndex, 10).Ok()) + { + arrayListContainer.SetItem(arrayIndex, jsonValue); + parser.FreeSelf(); + return PCTE_None; + } + parser.FreeSelf(); + if (containerIndex.Compare(P("-"))) { + arrayListContainer.AddItem(jsonValue); + } + else { + return PCTE_BadPointer; + } + } + return PCTE_None; +} + +/** + * Removes selected pending config. + * + * @return Result of the operation that reports any errors that have occured. + * Any changes are made iff result is `PCTE_None`. + */ +public final function PendingConfigToolResult RemoveConfig() +{ + local int editIndex; + local HashTable pendingSaves; + + editIndex = GetPendingConfigDataIndex(); + if (editIndex < 0) return PCTE_ConfigMissing; + pendingSaves = featurePendingEdits[editIndex].pendingSaves; + if (!pendingSaves.HasKey(selectedConfigName)) return PCTE_ConfigMissing; + + pendingSaves.RemoveItem(selectedConfigName); + if (pendingSaves.GetLength() <= 0) + { + pendingSaves.FreeSelf(); + featurePendingEdits.Remove(editIndex, 1); + } + return PCTE_None; +} + +private function int GetPendingConfigDataIndex() +{ + local int i; + + for (i = 0; i < featurePendingEdits.length; i ++) + { + if (featurePendingEdits[i].featureClass == selectedFeatureClass) { + return i; + } + } + return -1; +} + +private function PendingConfigToolResult ChangePendingConfigData( + HashTable newData) +{ + local int editsIndex; + + if (selectedConfigName == none) { + return PCTE_None; + } + editsIndex = GetPendingConfigDataIndex(); + if (editsIndex < 0) { + return PCTE_ConfigMissing; + } + featurePendingEdits[editsIndex].pendingSaves + .SetItem(selectedConfigName, newData); + return PCTE_None; +} + +private function HashTable GetCurrentConfigData() +{ + return selectedFeatureClass.default.configClass.static + .LoadData(selectedConfigName); +} + +defaultproperties +{ +} \ No newline at end of file