You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
520 lines
16 KiB
520 lines
16 KiB
/** |
|
* Command for managing features. |
|
* 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 <https://www.gnu.org/licenses/>. |
|
*/ |
|
class ACommandFeature extends Command; |
|
|
|
// TODO: autoconf, newconf, deleteconf, setconf |
|
|
|
struct EditedConfigs |
|
{ |
|
var class<Feature> featureClass; |
|
var HashTable pendingSaves; |
|
}; |
|
var private array<EditedConfigs> configEdits; |
|
|
|
var private ACommandFeature_Announcer announcer; |
|
|
|
protected function Finalizer() |
|
{ |
|
local int i; |
|
|
|
_.memory.Free(announcer); |
|
for (i = 0; i < configEdits.length; i ++) { |
|
_.memory.Free(configEdits[i].pendingSaves); |
|
} |
|
configEdits.length = 0; |
|
super.Finalizer(); |
|
} |
|
|
|
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.")); |
|
builder.SubCommand(P("enable")) |
|
.ParamText(P("feature")) |
|
.OptionalParams() |
|
.ParamText(P("config")) |
|
.Describe(P("Enables specified <feature>.")); |
|
builder.SubCommand(P("disable")) |
|
.ParamText(P("feature")) |
|
.Describe(P("Disables specified <feature>.")); |
|
builder.SubCommand(P("showconf")) |
|
.ParamText(P("feature")) |
|
.ParamText(P("config")); |
|
builder.SubCommand(P("editconf")) |
|
.ParamText(P("feature")) |
|
.ParamText(P("config")) |
|
.ParamText(P("variable path")) |
|
.ParamRemainder(P("value")); |
|
announcer = ACommandFeature_Announcer( |
|
_.memory.Allocate(class'ACommandFeature_Announcer')); |
|
} |
|
|
|
protected function Executed(CallData arguments, EPlayer instigator) |
|
{ |
|
announcer.Setup(none, instigator, othersConsole); |
|
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("disable"))) { |
|
DisableFeature(arguments.parameters.GetText(P("feature"))); |
|
} |
|
else if (arguments.subCommandName.Compare(P("showconf"))) |
|
{ |
|
ShowFeatureConfig( |
|
arguments.parameters.GetText(P("feature")), |
|
arguments.parameters.GetText(P("config"))); |
|
} |
|
else if (arguments.subCommandName.Compare(P("editconf"))) |
|
{ |
|
EditFeatureConfig( |
|
arguments.parameters.GetText(P("feature")), |
|
arguments.parameters.GetText(P("config")), |
|
arguments.parameters.GetText(P("variable path")), |
|
arguments.parameters.GetText(P("value"))); |
|
} |
|
} |
|
|
|
protected function EnableFeature(BaseText featureName, BaseText configParameter) |
|
{ |
|
local bool wasEnabled; |
|
local Text oldConfig, newConfig; |
|
local Feature instance; |
|
local class<Feature> featureClass; |
|
|
|
featureClass = LoadFeatureClass(featureName); |
|
if (featureClass == none) { |
|
return; |
|
} |
|
wasEnabled = featureClass.static.IsEnabled(); |
|
oldConfig = featureClass.static.GetCurrentConfig(); |
|
newConfig = PickConfigBasedOnParameter(featureClass, configParameter); |
|
// Already enabled with the same config! |
|
if (oldConfig != none && oldConfig.Compare(newConfig, SCASE_INSENSITIVE)) |
|
{ |
|
announcer.AnnounceFailedAlreadyEnabled(featureClass, 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); |
|
} |
|
else if (wasEnabled) { |
|
announcer.AnnounceSwappedConfig(featureClass, oldConfig, newConfig); |
|
} |
|
else { |
|
announcer.AnnounceEnabledFeature(featureClass, newConfig); |
|
} |
|
_.memory.Free(newConfig); |
|
_.memory.Free(oldConfig); |
|
} |
|
|
|
protected function DisableFeature(Text featureName) |
|
{ |
|
local class<Feature> featureClass; |
|
|
|
featureClass = LoadFeatureClass(featureName); |
|
if (featureClass == none) { |
|
return; |
|
} |
|
if (!featureClass.static.IsEnabled()) |
|
{ |
|
announcer.AnnounceFailedAlreadyDisabled(featureClass); |
|
return; |
|
} |
|
featureClass.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); |
|
} |
|
} |
|
|
|
protected function ShowFeatureConfig( |
|
BaseText featureName, |
|
BaseText configParameter) |
|
{ |
|
local MutableText dataAsJSON; |
|
local HashTable currentData, pendingData; |
|
local class<Feature> featureClass; |
|
|
|
featureClass = LoadFeatureClass(featureName); |
|
if (featureClass == none) return; |
|
if (configParameter == none) return; |
|
|
|
currentData = GetCurrentConfigData(featureClass, configParameter); |
|
if (currentData == none) |
|
{ |
|
announcer.AnnounceFailedNoDataForConfig(featureClass, configParameter); |
|
return; |
|
} |
|
// Display current data |
|
dataAsJSON = _.json.PrettyPrint(currentData); |
|
announcer.AnnounceCurrentConfig(featureClass, configParameter); |
|
callerConsole.Flush().WriteLine(dataAsJSON); |
|
_.memory.Free(dataAsJSON); |
|
// Display pending data |
|
pendingData = GetPendingConfigData(featureClass, configParameter); |
|
if (pendingData != none) |
|
{ |
|
dataAsJSON = _.json.PrettyPrint(pendingData); |
|
announcer.AnnouncePendingConfig(featureClass, configParameter); |
|
callerConsole.Flush().WriteLine(dataAsJSON); |
|
_.memory.Free(dataAsJSON); |
|
} |
|
_.memory.Free(pendingData); |
|
_.memory.Free(currentData); |
|
} |
|
|
|
protected function Text PickConfigBasedOnParameter( |
|
class<Feature> featureClass, |
|
BaseText configParameter) |
|
{ |
|
local Text resolvedConfig; |
|
local class<FeatureConfig> configClass; |
|
|
|
if (featureClass == none) { |
|
return none; |
|
} |
|
configClass = featureClass.default.configClass; |
|
if (configClass == none) |
|
{ |
|
announcer.AnnounceFailedNoConfigClass(featureClass); |
|
return none; |
|
} |
|
// If config was specified - simply check that it exists |
|
if (configParameter != none) |
|
{ |
|
if (configClass.static.Exists(configParameter)) { |
|
return configParameter.Copy(); |
|
} |
|
announcer.AnnounceFailedConfigMissing(configParameter); |
|
return none; |
|
} |
|
// If it wasn't specified - try auto config instead |
|
resolvedConfig = configClass.static.GetAutoEnabledConfig(); |
|
if (resolvedConfig == none) { |
|
announcer.AnnounceFailedNoConfigProvided(featureClass); |
|
} |
|
return resolvedConfig; |
|
} |
|
|
|
protected function class<Feature> LoadFeatureClass(BaseText featureName) |
|
{ |
|
local Text featureClassName; |
|
local class<Feature> featureClass; |
|
if (featureName == none) { |
|
return none; |
|
} |
|
if (featureName.StartsWith(P("$"))) { |
|
featureClassName = _.alias.ResolveFeature(featureName, true); |
|
} |
|
else { |
|
featureClassName = featureName.Copy(); |
|
} |
|
featureClass = class<Feature>(_.memory.LoadClass(featureClassName)); |
|
if (featureClass == none) { |
|
announcer.AnnounceFailedToLoadFeatureClass(featureName); |
|
} |
|
_.memory.Free(featureClassName); |
|
return featureClass; |
|
} |
|
|
|
protected function ShowAllFeatures() |
|
{ |
|
local int i; |
|
local array< class<Feature> > availableFeatures; |
|
availableFeatures = _.environment.GetAvailableFeatures(); |
|
for (i = 0; i < availableFeatures.length; i ++) { |
|
ShowFeature(availableFeatures[i]); |
|
} |
|
} |
|
|
|
protected function ShowFeature(class<Feature> feature) |
|
{ |
|
local int i; |
|
local Text autoConfig; |
|
local MutableText featureName, builder; |
|
local ListBuilder configList; |
|
local array<Text> availableConfigs; |
|
local class<FeatureConfig> configClass; |
|
|
|
if (feature == none) { |
|
return; |
|
} |
|
configClass = feature.default.configClass; |
|
if (configClass != none) { |
|
availableConfigs = configClass.static.AvailableConfigs(); |
|
} |
|
featureName = _.text |
|
.FromClassM(feature) |
|
.ChangeDefaultColor(_.color.TextEmphasis); |
|
builder = _.text.Empty(); |
|
if (feature.static.IsEnabled()) { |
|
builder.Append(F("[ {$TextPositive enabled} ] ")); |
|
} |
|
else { |
|
builder.Append(F("[ {$TextNegative disabled} ] ")); |
|
} |
|
builder.Append(featureName); |
|
_.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(); |
|
for (i = 0; i < availableConfigs.length; i += 1) |
|
{ |
|
configList.Item(availableConfigs[i]); |
|
if ( autoConfig != none |
|
&& autoConfig.Compare(availableConfigs[i], SCASE_INSENSITIVE)) |
|
{ |
|
configList.Comment(F("{$TextPositive auto enabled}")); |
|
} |
|
} |
|
builder = configList.GetMutable(); |
|
callerConsole.WriteLine(builder); |
|
_.memory.FreeMany(availableConfigs); |
|
_.memory.Free(configList); |
|
_.memory.Free(autoConfig); |
|
_.memory.Free(builder); |
|
} |
|
//TODO: find where `null` spam is from |
|
//TODO add failure color to fail announcements and good color to good ones - add them too! |
|
protected function EditFeatureConfig( |
|
BaseText featureName, |
|
BaseText configParameter, |
|
BaseText pathToValue, |
|
BaseText newValue) |
|
{ |
|
local int arrayIndex; |
|
local Text topValue; |
|
local HashTable pendingData; |
|
local JSONPointer pointer; |
|
local Parser parser; |
|
local AcediaObject jsonValue, container; |
|
local class<Feature> featureClass; |
|
|
|
featureClass = LoadFeatureClass(featureName); |
|
if (featureClass == none) { |
|
return; |
|
} |
|
pendingData = GetPendingConfigData(featureClass, configParameter, true); |
|
if (pendingData == none) |
|
{ |
|
announcer.AnnounceFailedConfigMissing(configParameter); |
|
return; |
|
} |
|
// Get guaranteed not-`none` JSON value |
|
parser = newValue.Parse(); |
|
jsonValue = _.json.ParseWith(parser); |
|
parser.FreeSelf(); |
|
if (jsonValue == none) { |
|
jsonValue = newValue.Copy(); |
|
} |
|
// |
|
pointer = _.json.Pointer(pathToValue); |
|
if (pointer.IsEmpty()) |
|
{ |
|
if (jsonValue.class != class'HashTable') { |
|
announcer.AnnounceFailedExpectedObject(); |
|
} |
|
else { |
|
ChangePendingConfigData(featureClass, configParameter, HashTable(jsonValue)); |
|
} |
|
jsonValue.FreeSelf(); |
|
return; |
|
} |
|
topValue = pointer.Pop(); |
|
container = pendingData.GetItemByJSON(pointer); |
|
if (container == none) |
|
{ |
|
announcer.AnnounceFailedBadPointer(featureClass, configParameter, pathToValue); |
|
pointer.FreeSelf(); |
|
topValue.FreeSelf(); |
|
return; |
|
} |
|
if (HashTable(container) != none) { |
|
HashTable(container).SetItem(topValue, jsonValue); |
|
} |
|
if (ArrayList(container) != none) |
|
{ |
|
parser = topValue.Parse(); |
|
if (parser.MInteger(arrayIndex, 10).Ok()) { |
|
ArrayList(container).SetItem(arrayIndex, jsonValue); |
|
} |
|
else if (topValue.Compare(P("-"))) { |
|
ArrayList(container).AddItem(jsonValue); |
|
} |
|
else { |
|
announcer.AnnounceFailedBadPointer(featureClass, configParameter, pathToValue); |
|
} |
|
parser.FreeSelf(); |
|
} |
|
pointer.FreeSelf(); |
|
topValue.FreeSelf(); |
|
} |
|
|
|
/*// TODO: autoconf, newconf, deleteconf, setconf |
|
|
|
struct EditedConfigs |
|
{ |
|
var class<Feature> featureClass; |
|
var HashTable pendingSaves; |
|
}; |
|
var private array<EditedConfigs> configEdits;*/ |
|
|
|
private function HashTable GetConfigData( |
|
class<Feature> featureClass, |
|
BaseText configName) |
|
{ |
|
local HashTable result; |
|
|
|
if (featureClass == none) return none; |
|
if (configName == none) return none; |
|
|
|
result = GetPendingConfigData(featureClass, configName); |
|
if (result != none) { |
|
return result; |
|
} |
|
return GetCurrentConfigData(featureClass, configName); |
|
} |
|
|
|
private function HashTable GetCurrentConfigData( |
|
class<Feature> featureClass, |
|
BaseText configName) |
|
{ |
|
local class<FeatureConfig> configClass; |
|
|
|
if (featureClass == none) return none; |
|
if (configName == none) return none; |
|
|
|
configClass = featureClass.default.configClass; |
|
if (configClass == none) |
|
{ |
|
announcer.AnnounceFailedNoConfigClass(featureClass); |
|
return none; |
|
} |
|
return configClass.static.LoadData(configName); |
|
} |
|
|
|
private function int GetPendingConfigDataIndex( |
|
class<Feature> featureClass) |
|
{ |
|
local int i; |
|
|
|
for (i = 0; i < configEdits.length; i ++) |
|
{ |
|
if (configEdits[i].featureClass == featureClass) { |
|
return i; |
|
} |
|
} |
|
return -1; |
|
} |
|
|
|
private function ChangePendingConfigData( |
|
class<Feature> featureClass, |
|
BaseText configName, |
|
HashTable newData) |
|
{ |
|
local int editsIndex; |
|
local Text lowerCaseConfigName; |
|
|
|
if (newData == none) return; |
|
if (configName == none) return; |
|
editsIndex = GetPendingConfigDataIndex(featureClass); |
|
if (editsIndex < 0) return; |
|
|
|
lowerCaseConfigName = configName.LowerCopy(); |
|
configEdits[editsIndex].pendingSaves.SetItem(configName, newData); |
|
lowerCaseConfigName.FreeSelf(); |
|
} |
|
|
|
private function HashTable GetPendingConfigData( |
|
class<Feature> featureClass, |
|
BaseText configName, |
|
optional bool createIfMissing) |
|
{ |
|
local int editsIndex; |
|
local Text lowerCaseConfigName; |
|
local HashTable result; |
|
local EditedConfigs newRecord; |
|
|
|
if (featureClass == none) return none; |
|
if (configName == none) return none; |
|
|
|
lowerCaseConfigName = configName.LowerCopy(); |
|
editsIndex = GetPendingConfigDataIndex(featureClass); |
|
if (editsIndex >= 0) |
|
{ |
|
result = configEdits[editsIndex] |
|
.pendingSaves |
|
.GetHashTable(lowerCaseConfigName); |
|
if (result != none) |
|
{ |
|
lowerCaseConfigName.FreeSelf(); |
|
return result; |
|
} |
|
} |
|
if (createIfMissing) |
|
{ |
|
if (editsIndex < 0) |
|
{ |
|
editsIndex = configEdits.length; |
|
newRecord.featureClass = featureClass; |
|
newRecord.pendingSaves = _.collections.EmptyHashTable(); |
|
configEdits[editsIndex] = newRecord; |
|
} |
|
result = GetCurrentConfigData(featureClass, configName); |
|
if (result != none) |
|
{ |
|
configEdits[editsIndex] |
|
.pendingSaves |
|
.SetItem(lowerCaseConfigName, result); |
|
} |
|
} |
|
lowerCaseConfigName.FreeSelf(); |
|
return result; |
|
} |
|
|
|
// 3. Add `editconf` subcommand |
|
// 4. Add '*' for edited configs in feature display |
|
// 5. Add `saveconf` and `--save` flag |
|
// 6. Add `removeconf` |
|
// 7. Add `newconf` |
|
// 8. Add `autoconf` for setting auto config |
|
|
|
defaultproperties |
|
{ |
|
} |