Prepare fixtures

This commit is contained in:
dkanus 2026-07-14 20:27:09 +07:00
commit 9c94356263
6021 changed files with 722805 additions and 22 deletions

View file

@ -0,0 +1,482 @@
/**
* Command for working with databases.
* Copyright 2021-2023 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 ACommandDB extends Command
dependson(Database);
/**
* This command provides a text user interface to databases.
* It can perform two types of tasks:
* 1. Tasks that have to do with managing set of databases as a whole:
* listing them, their creation and deletion. For currently implemented
* local databases they work synchronously and are just simple bindings
* to the Acedia's API.
* 2. Tasks that edit a particular database: these require us to send
* database a query and then wait for the response. With these we
* cannot give an immediate reply, so we have to remember player that
* requested these queries to then relay him database's reply.
* Main problem with remembering a player for 2nd-type tasks is that,
* while highly unlikely, several different players may make their own requests
* while we are still waiting for the reply to the previous query.
* We could simply remember a queue of several players and then return them in
* a FIFO order (since databases guarantee orderly replies to their queries),
* but requests can also be towards different databases and, therefore,
* completed in a random order.
* To solve this we fill-in the waiting queue of pairs player-database that
* link player that made a request and database he made this request to.
* Once reply from any database arrives - we simply search and return
* the first player in our queue that made a request to this database.
* Thanks to the fact that databases reply to their queries in order of
* arrival - this will let us fetch precisely the players that were responsible
* for these requests. This logic is implemented in `PushPlayer()` and
* `PopPlayer()` methods.
* The rest of the methods are mostly straightforward callbacks that
* transform `Database`'s reply into text message for the player.
*/
// Array of pairs is represented by two arrays of single values.
// Arrays should be kept same length, elements with the same index
// correspond to the same pair.
var protected array<Database> queueWaitingListDatabases;
var protected array<EPlayer> queueWaitingListPlayers;
// Auxiliary structure that corresponds to database + JSON path from resolved
// database link.
struct DBPointerPair
{
var public Database database;
var public JSONPointer pointer;
};
var protected const int TCREATE, TDELETE, TLIST, TREAD, TSIZE, TKEYS, TREMOVE;
var protected const int TWRITE, TINCREMENT, TDATABASE_NAME, TDATABASE_LINK;
var protected const int TJSON_VALUE, TQUERY_INVALID_POINTER, TQUERY_INVALID_DB;
var protected const int TOBJECT_KEYS_ARE, TOBJECT_SIZE_IS, TQUERY_COMPLETED;
var protected const int TQUERY_INVALID_DATA, TAVAILABLE_DATABASES, TDA_DELETED;
var protected const int TDB_DOESNT_EXIST, TDB_ALREADY_EXISTS, TDB_CREATED;
var protected const int TDB_CANNOT_BE_CREATED, TNO_DEFAULT_COMMAND, TBAD_DBLINK;
protected function BuildData(CommandDataBuilder builder)
{
builder.Group(P("admin"));
builder.Summary(P("Read and edit data in your databases."
@ "Databases' values are addressed with links:"
@ "\"<db_name>:<json_path>\""));
builder.SubCommand(T(TCREATE));
builder.ParamText(T(TDATABASE_NAME));
builder.Describe(P("Creates new database with a specified name."));
builder.SubCommand(T(TDELETE));
builder.ParamText(T(TDATABASE_NAME));
builder.Describe(P("Completely deletes specified database."));
builder.SubCommand(T(TLIST));
builder.Describe(P("Lists available databases."));
builder.SubCommand(T(TREAD));
builder.ParamText(T(TDATABASE_LINK));
builder.Describe(P("Reads data from location given by the `databaseLink`."));
builder.SubCommand(T(TSIZE));
builder.ParamText(T(TDATABASE_LINK));
builder.Describe(P("Gets amount of elements inside JSON array or object at"
@ "location given by the `databaseLink`."));
builder.SubCommand(T(TKEYS));
builder.ParamText(T(TDATABASE_LINK));
builder.Describe(P("Lists keys of JSON object at location given by"
@ "the `databaseLink`."));
builder.SubCommand(T(TREMOVE));
builder.ParamText(T(TDATABASE_LINK));
builder.Describe(P("Removes data from location given by the `databaseLink`."));
builder.SubCommand(T(TWRITE));
builder.ParamText(T(TDATABASE_LINK));
builder.ParamJSON(T(TJSON_VALUE));
builder.Describe(P("Writes specified JSON value into location given by"
@ "the `databaseLink`."));
builder.Option(T(TINCREMENT));
builder.Describe(F("Specifying this option for any of the"
@ "{$TextEmphasis 'write'} subcommands will cause them to append"
@ "data to the old one, instead of rewriting it."));
}
protected function PushPlayer(EPlayer nextPlayer, Database callDatabase)
{
local EPlayer playerCopy;
if (nextPlayer != none) {
playerCopy = nextPlayer;
nextPlayer.NewRef();
}
if (callDatabase != none) {
callDatabase.NewRef();
}
queueWaitingListPlayers[queueWaitingListPlayers.length] = playerCopy;
queueWaitingListDatabases[queueWaitingListDatabases.length] = callDatabase;
}
protected function EPlayer PopPlayer(Database relevantDatabase)
{
local int i;
local EPlayer result;
if (queueWaitingListPlayers.length <= 0) return none;
if (queueWaitingListDatabases.length <= 0) return none;
while (i < queueWaitingListDatabases.length)
{
if (queueWaitingListDatabases[i].IsEqual(relevantDatabase))
{
result = queueWaitingListPlayers[i];
queueWaitingListDatabases[i].FreeSelf();
queueWaitingListPlayers.Remove(i, 1);
queueWaitingListDatabases.Remove(i, 1);
break;
}
i += 1;
}
if (result != none && result.IsExistent()) {
return result;
}
_.memory.Free(result);
return none;
}
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local AcediaObject valueToWrite;
local DBPointerPair pair;
local Text subCommand;
subCommand = arguments.subCommandName;
// Try executing on of the operation that manage multiple databases
if (TryAPICallCommands(subCommand, instigator, arguments.parameters)) {
return;
}
// If we have failed - it has got to be one of the operations on
// a single database
pair = TryLoadingDB(arguments.parameters.GetText(T(TDATABASE_LINK)));
if (pair.database == none)
{
callerConsole.WriteLine(T(TBAD_DBLINK));
return;
}
// Remember the last player we are making a query to and make that query
PushPlayer(instigator, pair.database);
if (subCommand.Compare(T(TWRITE)))
{
valueToWrite = arguments.parameters.GetItem(T(TJSON_VALUE));
if (arguments.options.HasKey(T(TINCREMENT)))
{
pair.database.IncrementData(pair.pointer, valueToWrite)
.connect = DisplayResponse;
}
else
{
pair.database.WriteData(pair.pointer, valueToWrite)
.connect = DisplayResponse;
}
}
else if (subCommand.Compare(T(TREAD))) {
pair.database.ReadData(pair.pointer).connect = DisplayData;
}
else if (subCommand.Compare(T(TSIZE))) {
pair.database.GetDataSize(pair.pointer).connect = DisplaySize;
}
else if (subCommand.Compare(T(TKEYS))) {
pair.database.GetDataKeys(pair.pointer).connect = DisplayKeys;
}
else if (subCommand.Compare(T(TREMOVE))) {
pair.database.RemoveData(pair.pointer).connect = DisplayResponse;
}
_.memory.Free(pair.pointer);
}
// Simple API calls
private function bool TryAPICallCommands(
BaseText subCommand,
EPlayer instigator,
HashTable commandParameters)
{
local Text databaseName;
if (subCommand.IsEmpty())
{
callerConsole.WriteLine(T(TNO_DEFAULT_COMMAND));
return true;
}
else if (subCommand.Compare(T(TLIST)))
{
ListDatabases(instigator);
return true;
}
else if (subCommand.Compare(T(TCREATE)))
{
databaseName = commandParameters.GetText(T(TDATABASE_NAME));
CreateDatabase(instigator, databaseName);
_.memory.Free(databaseName);
return true;
}
else if (subCommand.Compare(T(TDELETE)))
{
databaseName = commandParameters.GetText(T(TDATABASE_NAME));
DeleteDatabase(instigator, databaseName);
_.memory.Free(databaseName);
return true;
}
return false;
}
// json pointer as `Text` -> `DBPointerPair` representation converter method
private function DBPointerPair TryLoadingDB(BaseText databaseLink)
{
local DBPointerPair result;
if (databaseLink == none) {
return result;
}
result.database = _server.db.Load(databaseLink);
if (result.database == none) {
return result;
}
result.pointer = _server.db.GetPointer(databaseLink);
return result;
}
protected function CreateDatabase(EPlayer instigator, Text databaseName)
{
if (instigator == none) {
return;
}
if (_server.db.ExistsLocal(databaseName))
{
callerConsole.WriteLine(T(TDB_ALREADY_EXISTS));
return;
}
if (_server.db.NewLocal(databaseName) != none) {
callerConsole.WriteLine(T(TDB_CREATED));
}
else {
callerConsole.WriteLine(T(TDB_CANNOT_BE_CREATED));
}
}
protected function DeleteDatabase(EPlayer instigator, Text databaseName)
{
if (instigator == none) {
return;
}
if (_server.db.DeleteLocal(databaseName)) {
callerConsole.WriteLine(T(TDA_DELETED));
}
else {
callerConsole.WriteLine(T(TDB_DOESNT_EXIST));
}
}
protected function ListDatabases(EPlayer instigator)
{
local int i;
local array<Text> availableDatabases;
local ConsoleWriter console;
if (instigator == none) {
return;
}
availableDatabases = _server.db.ListLocal();
console = callerConsole;
console.Write(T(TAVAILABLE_DATABASES));
for (i = 0; i < availableDatabases.length; i += 1)
{
if (i > 0) {
console.ResetColor().Write(P(", "));
}
console.UseColor(_.color.TextSubtle).Write(availableDatabases[i]);
}
console.ResetColor().Flush();
_.memory.FreeMany(availableDatabases);
}
protected function OutputStatus(
EPlayer instigator,
Database.DBQueryResult error)
{
if (instigator == none) {
return;
}
if (error == DBR_Success) {
instigator.BorrowConsole().WriteLine(T(TQUERY_COMPLETED));
}
if (error == DBR_InvalidPointer) {
instigator.BorrowConsole().WriteLine(T(TQUERY_INVALID_POINTER));
}
if (error == DBR_InvalidDatabase) {
instigator.BorrowConsole().WriteLine(T(TQUERY_INVALID_DB));
}
if (error == DBR_InvalidData) {
instigator.BorrowConsole().WriteLine(T(TQUERY_INVALID_DATA));
}
}
protected function DisplayData(
Database.DBQueryResult result,
AcediaObject data,
Database source,
int requestID)
{
local Text printedJSON;
local EPlayer instigator;
instigator = PopPlayer(source);
OutputStatus(instigator, result);
if (instigator != none && result == DBR_Success)
{
printedJSON = _.json.PrettyPrint(data).IntoText();
instigator.BorrowConsole().Write(printedJSON).Flush();
_.memory.Free(printedJSON);
_.memory.Free(instigator);
instigator = none;
}
_.memory.Free(data);
}
protected function DisplaySize(
Database.DBQueryResult result,
int size,
Database source,
int requestID)
{
local Text sizeAsText;
local EPlayer instigator;
instigator = PopPlayer(source);
OutputStatus(instigator, result);
if (instigator != none && result == DBR_Success)
{
sizeAsText = _.text.FromInt(size);
instigator.BorrowConsole()
.Write(T(TOBJECT_SIZE_IS))
.Write(sizeAsText)
.Flush();
_.memory.Free(sizeAsText);
_.memory.Free(instigator);
instigator = none;
}
}
protected function DisplayKeys(
Database.DBQueryResult result,
ArrayList keys,
Database source,
int requestID)
{
local int i;
local Text nextKey;
local EPlayer instigator;
local ConsoleWriter console;
instigator = PopPlayer(source);
OutputStatus(instigator, result);
if (keys == none) {
return;
}
if (instigator != none && result == DBR_Success)
{
console = instigator.BorrowConsole();
console.Write(T(TOBJECT_KEYS_ARE));
for (i = 0; i < keys.GetLength(); i += 1)
{
if (i > 0) {
console.ResetColor().Write(P(", "));
}
nextKey = keys.GetText(i);
console.UseColor(_.color.jPropertyName).Write(nextKey);
_.memory.Free(nextKey);
}
console.Flush();
_.memory.Free(instigator);
instigator = none;
}
_.memory.Free(keys);
}
protected function DisplayResponse(
Database.DBQueryResult result,
Database source,
int requestID)
{
local EPlayer instigator;
instigator = PopPlayer(source);
OutputStatus(instigator, result);
_.memory.Free(instigator);
}
defaultproperties
{
preferredName = "db"
TCREATE = 0
stringConstants(0) = "create"
TDELETE = 1
stringConstants(1) = "delete"
TLIST = 2
stringConstants(2) = "list"
TREAD = 3
stringConstants(3) = "read"
TSIZE = 4
stringConstants(4) = "size"
TKEYS = 5
stringConstants(5) = "keys"
TREMOVE = 6
stringConstants(6) = "remove"
TWRITE = 7
stringConstants(7) = "write"
TINCREMENT = 8
stringConstants(8) = "increment"
TDATABASE_NAME = 9
stringConstants(9) = "databaseName"
TDATABASE_LINK = 10
stringConstants(10) = "databaseLink"
TJSON_VALUE = 11
stringConstants(11) = "jsonValue"
TOBJECT_KEYS_ARE = 12
stringConstants(12) = "{$TextEmphasis Object keys are:} "
TOBJECT_SIZE_IS = 13
stringConstants(13) = "{$TextEmphasis Object size is:} "
TQUERY_COMPLETED = 14
stringConstants(14) = "{$TextPositive Database query was completed!}"
TQUERY_INVALID_POINTER = 15
stringConstants(15) = "{$TextFailure Query was provided with an invalid JSON pointer.}"
TQUERY_INVALID_DB = 16
stringConstants(16) = "{$TextFailure Operation could not finish because database is damaged and unusable.}"
TQUERY_INVALID_DATA = 17
stringConstants(17) = "{$TextFailure Query data is invalid.}"
TAVAILABLE_DATABASES = 18
stringConstants(18) = "{$TextEmphasis Available databases:} "
TDA_DELETED = 19
stringConstants(19) = "{$TextPositive Database was deleted.}"
TDB_DOESNT_EXIST = 20
stringConstants(20) = "{$TextFailure Database with specified name does not exist.}"
TDB_ALREADY_EXISTS = 21
stringConstants(21) = "{$TextFailure Database with specified name already exists.}"
TDB_CREATED = 22
stringConstants(22) = "{$TextPositive Database was created.}"
TDB_CANNOT_BE_CREATED = 23
stringConstants(23) = "{$TextFailure Database cannot be created.}"
TNO_DEFAULT_COMMAND = 24
stringConstants(24) = "{$TextFailure Default command does nothing. Use on of the sub-commands.}"
TBAD_DBLINK = 25
stringConstants(25) = "{$TextFailure Database could not be read for the specified link.}"
}

View file

@ -0,0 +1,94 @@
/**
* Command for changing amount of money players have.
* Copyright 2021 - 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 ACommandDosh extends Command;
var private ACommandDosh_Announcer announcer;
protected function Finalizer()
{
_.memory.Free(announcer);
super.Finalizer();
}
protected function BuildData(CommandDataBuilder builder)
{
builder.Group(P("gameplay"));
builder.Summary(P("Changes amount of money."));
builder.RequireTarget();
builder.ParamInteger(P("amount"));
builder.Describe(P("Gives (or takes if negative) players a specified <amount>"
@ "of money."));
builder.SubCommand(P("set"));
builder.ParamInteger(P("amount"));
builder.Describe(P("Sets player's money to a specified <amount>."));
builder.Option(P("min"));
builder.ParamInteger(P("minValue"));
builder.Describe(F("Players will retain at least this amount of dosh after"
@ "the command's execution. In case of conflict, overrides"
@ "'{$TextEmphasis --max}' option. `0` is assumed by default."));
builder.Option(P("max"), P("M"));
builder.ParamInteger(P("maxValue"));
builder.Describe(F("Players will have at most this amount of dosh after"
@ "the command's execution. In case of conflict, it is overridden"
@ "by '{$TextEmphasis --min}' option."));
announcer = ACommandDosh_Announcer(
_.memory.Allocate(class'ACommandDosh_Announcer'));
}
protected function ExecutedFor(
EPlayer target,
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local int oldAmount, newAmount;
local int amount, minValue, maxValue;
// Find min and max value boundaries
minValue = arguments.options.GetIntBy(P("/min/minValue"), 0);
maxValue = arguments.options.GetIntBy(P("/max/maxValue"), MaxInt);
if (minValue > maxValue) {
maxValue = minValue;
}
// Change dosh
oldAmount = target.GetDosh();
amount = arguments.parameters.GetInt(P("amount"));
if (arguments.subCommandName.IsEmpty()) {
newAmount = oldAmount + amount;
}
else {
// This has to be "dosh set"
newAmount = amount;
}
newAmount = Clamp(newAmount, minValue, maxValue);
target.SetDosh(newAmount);
// Announce dosh change, if necessary
announcer.Setup(target, instigator, othersConsole);
if (newAmount > oldAmount) {
announcer.AnnounceGainedDosh(newAmount - oldAmount);
}
if (newAmount < oldAmount) {
announcer.AnnounceLostDosh(oldAmount - newAmount);
}
}
defaultproperties {
preferredName = "dosh"
}

View file

@ -0,0 +1,92 @@
/**
* Announcer for `ACommandDosh`.
* 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 ACommandDosh_Announcer extends CommandAnnouncer;
var private AnnouncementVariations gainedDosh, lostDosh;
protected function Finalizer()
{
FreeVariations(gainedDosh);
FreeVariations(lostDosh);
super.Finalizer();
}
public final function AnnounceGainedDosh(int doshAmount)
{
local int i;
local array<TextTemplate> templates;
if (!gainedDosh.initialized)
{
gainedDosh.initialized = true;
gainedDosh.toSelfReport = _.text.MakeTemplate_S(
"You {$TextPositive gave} yourself {$TypeNumber %1} do$h!");
gainedDosh.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive gave} themselves {$TypeNumber %1}"
@ "do$h!");
gainedDosh.toOtherReport = _.text.MakeTemplate_S(
"You {$TextPositive gave} %%target%% {$TypeNumber %1} do$h!");
gainedDosh.toOtherPrivate = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive gave} you {$TypeNumber %1} do$h!");
gainedDosh.toOtherPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive gave} %%target%% {$TypeNumber %1}"
@ "do$h!");
}
templates = MakeArray(gainedDosh);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().ArgInt(doshAmount);
}
MakeAnnouncement(gainedDosh);
}
public final function AnnounceLostDosh(int doshAmount)
{
local int i;
local array<TextTemplate> templates;
if (!lostDosh.initialized)
{
lostDosh.initialized = true;
lostDosh.toSelfReport = _.text.MakeTemplate_S(
"You {$TextNegative took} {$TypeNumber %1} do$h from"
@ "yourself!");
lostDosh.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNegative took} {$TypeNumber %1} do$h from"
@ "themselves!");
lostDosh.toOtherReport = _.text.MakeTemplate_S(
"You {$TextNegative took} {$TypeNumber %1} do$h from"
@ "%%target%%!");
lostDosh.toOtherPrivate = _.text.MakeTemplate_S(
"%%instigator%% {$TextNegative took} {$TypeNumber %1} do$h from"
@ "you!");
lostDosh.toOtherPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNegative took} {$TypeNumber %1} do$h from"
@ "%%target%%!");
}
templates = MakeArray(lostDosh);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().ArgInt(doshAmount);
}
MakeAnnouncement(lostDosh);
}
defaultproperties
{
}

View file

@ -0,0 +1,579 @@
/**
* 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
dependson(PendingConfigsTool);
var private class<Feature> selectedFeatureClass;
var private Text selectedFeatureName;
var private Text selectedConfigName;
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();
}
protected function BuildData(CommandDataBuilder builder) {
builder.Group(P("admin"));
builder.Summary(P("Managing features."));
builder.Describe(P("Command for managing features and their configs."));
builder.SubCommand(P("enable"));
builder.ParamText(P("feature"),, P("feature"));
builder.OptionalParams();
builder.ParamText(P("config"));
builder.Describe(P("Enables specified <feature>. If <config> isn't specified -"
@ "choses the \"default\" one, making new config with default"
@ "settings if it doesn't exist."));
builder.SubCommand(P("disable"));
builder.ParamText(P("feature"),, P("feature"));
builder.Describe(P("Disables specified <feature>."));
builder.SubCommand(P("showconf"));
builder.ParamText(P("feature"),, P("feature"));
builder.OptionalParams();
builder.ParamText(P("config"));
builder.Describe(P("Show given <config> for the given <feature>."));
builder.SubCommand(P("editconf"));
builder.ParamText(P("feature"),, P("feature"));
builder.ParamText(P("config"));
builder.ParamText(P("variable_path"));
builder.ParamJSON(P("value"));
builder.Describe(P("Changes a value inside given <config> of the given"
@ "<feature> by setting value at JSON path <variable_path> to"
@ "the JSON value <value>. Changes won't be immediately applied to"
@ "the game and kept as pending."));
builder.SubCommand(P("saveconf"));
builder.ParamText(P("feature"),, P("feature"));
builder.ParamText(P("config"));
builder.Describe(P("Saves pending changes for the given <config> of the given"
@ "<feature>."));
builder.SubCommand(P("newconf"));
builder.ParamText(P("feature"),, P("feature"));
builder.ParamText(P("config"));
builder.Describe(P("Creates new config for the given <feature>."));
builder.SubCommand(P("removeconf"));
builder.ParamText(P("feature"),, P("feature"));
builder.ParamText(P("config"));
builder.Describe(P("Removes specified <config> of the specified <feature>."));
builder.SubCommand(P("autoconf"));
builder.ParamText(P("feature"),, P("feature"));
builder.OptionalParams();
builder.ParamText(P("config"));
builder.Describe(P("Changes current auto config config of the specified"
@ "<feature>. 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"));
builder.Describe(F("Affects subcommand {$TextEmphasis showconf} by making it"
@ "show all available configs."));
builder.Option(P("save"));
builder.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,
CommandPermissions permissions
) {
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();
} else if (arguments.subCommandName.Compare(P("disable"))) {
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.GetItem(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.Free2(selectedConfigName, selectedFeatureName);
selectedConfigName = none;
selectedFeatureName = none;
}
protected function SelectFeatureAndConfig(CallData arguments) {
local Text featureClassName, userGivenConfigName;
selectedFeatureName = arguments.parameters.GetTextBy(P("/feature/alias"));
featureClassName = arguments.parameters.GetTextBy(P("/feature/value"));
selectedFeatureClass = LoadFeatureClass(featureClassName);
if (selectedFeatureClass == none && !arguments.subCommandName.IsEmpty()) {
_.memory.Free(selectedFeatureName);
selectedFeatureName = none;
return;
}
_.memory.Free(featureClassName);
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(selectedFeatureName, newConfig);
_.memory.Free(newConfig);
_.memory.Free(oldConfig);
return;
}
// Try enabling and report the result
instance = selectedFeatureClass.static.EnableMe(newConfig);
if (instance == none) {
announcer.AnnounceFailedCannotEnableFeature(
selectedFeatureName,
newConfig);
} else if (wasEnabled) {
announcer.AnnounceSwappedConfig(
selectedFeatureName,
oldConfig,
newConfig);
} else {
announcer.AnnounceEnabledFeature(selectedFeatureName, newConfig);
}
_.memory.Free(newConfig);
_.memory.Free(oldConfig);
}
protected function DisableFeature() {
if (!selectedFeatureClass.static.IsEnabled()) {
announcer.AnnounceFailedAlreadyDisabled(selectedFeatureName);
return;
}
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(selectedFeatureName);
}
}
protected function ShowSelectedConfigs(bool showAllFeatures) {
local int i;
local array<Text> availableConfigs;
local MutableText configList;
local class<FeatureConfig> configClass;
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;
}
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(
selectedFeatureName,
configName);
return;
}
// Display current data
dataAsJSON = _.json.PrettyPrint(currentData);
announcer.AnnounceCurrentConfig(selectedFeatureName, 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(
selectedFeatureName,
configName);
callerConsole.Flush().WriteLine(dataAsJSON);
_.memory.Free(dataAsJSON);
}
_.memory.Free(pendingData);
_.memory.Free(currentData);
}
protected function Text PickConfigBasedOnParameter() {
local Text resolvedConfig;
local class<FeatureConfig> configClass;
configClass = selectedFeatureClass.default.configClass;
if (configClass == none) {
announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
return none;
}
// If config was specified - simply check that it exists
if (selectedConfigName != none) {
if (configClass.static.Exists(selectedConfigName)) {
return selectedConfigName.Copy();
}
announcer.AnnounceFailedConfigMissing(selectedConfigName);
return none;
}
// If it wasn't specified - try auto config instead
resolvedConfig = configClass.static.GetAutoEnabledConfig();
if (resolvedConfig == none) {
announcer.AnnounceFailedNoConfigProvided(selectedFeatureName);
}
return resolvedConfig;
}
protected function class<Feature> LoadFeatureClass(BaseText featureClassName) {
local class<Feature> featureClass;
if (featureClassName == none) {
return none;
}
featureClass = class<Feature>(_.memory.LoadClass(featureClassName));
if (featureClass == none) {
announcer.AnnounceFailedToLoadFeatureClass(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> featureClass)
{
local MutableText featureName;
local MutableText configList;
if (featureClass == none) {
return;
}
featureName = _.text
.FromClassMutable(featureClass)
.ChangeDefaultColor(_.color.TextEmphasis);
configList = PrintConfigList(featureClass);
callerConsole.Flush();
if (featureClass.static.IsEnabled()) {
callerConsole.Write(F("[ {$TextPositive enabled} ] "));
}
else {
callerConsole.Write(F("[ {$TextNegative disabled} ] "));
}
callerConsole.Write(featureName)
.Write(P(" with configs: "))
.WriteLine(configList);
_.memory.Free(featureName);
_.memory.Free(configList);
}
protected function MutableText PrintConfigList(class<Feature> featureClass) {
local int i;
local Text autoConfig, enabledConfig;
local ListBuilder configList;
local MutableText result, nextConfig;
local array<Text> availableConfigs;
local class<FeatureConfig> configClass;
if (featureClass == none) return none;
configClass = featureClass.default.configClass;
if (configClass == none) return none;
availableConfigs = configClass.static.AvailableConfigs();
enabledConfig = featureClass.static.GetCurrentConfig();
autoConfig = configClass.static.GetAutoEnabledConfig();
configList = ListBuilder(_.memory.Allocate(class'ListBuilder'));
for (i = 0; i < availableConfigs.length; i += 1) {
nextConfig = availableConfigs[i].MutableCopy();
if (enabledConfig != none && enabledConfig.Compare(nextConfig, SCASE_INSENSITIVE)) {
nextConfig.ChangeDefaultColor(_.color.TextPositive);
}
if (pendingConfigs.HasPendingConfigFor(featureClass, nextConfig)) {
nextConfig.Append(F("{$TextEmphasis *}"));
}
configList.Item(nextConfig);
_.memory.Free(nextConfig);
if (autoConfig != none && autoConfig.Compare(availableConfigs[i], SCASE_INSENSITIVE)) {
if (autoConfig.Compare(enabledConfig, SCASE_INSENSITIVE)) {
configList.Comment(F("{$TextPositive auto enabled}"));
} else {
configList.Comment(F("{$TextNegative auto enabled}"));
}
}
}
result = configList.GetMutable();
_.memory.Free3(configList, autoConfig, enabledConfig);
_.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, AcediaObject newValue, bool saveConfig) {
local MutableText printedOldValue;
local MutableText printedNewValue;
local PendingConfigsTool.PendingConfigToolResult error;
if (selectedFeatureClass == none) {
return;
}
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(
selectedFeatureName,
selectedConfigName,
pathToValue);
} else if (printedOldValue == none) {
announcer.AnnounceConfigNewValue(
selectedFeatureName,
selectedConfigName,
pathToValue,
printedNewValue);
} else {
announcer.AnnounceConfigEdited(
selectedFeatureName,
selectedConfigName,
pathToValue,
printedOldValue,
printedNewValue);
}
if (saveConfig && error == PCTE_None) {
SaveFeatureConfig();
}
_.memory.Free(printedOldValue);
_.memory.Free(printedNewValue);
_.memory.Free(pathToValue);
_.memory.Free(newValue);
}
protected function SaveFeatureConfig() {
local BaseText enabledConfigName;
local HashTable pendingData;
local class<FeatureConfig> configClass;
configClass = selectedFeatureClass.default.configClass;
if (configClass == none) {
announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
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(selectedFeatureName);
} else {
announcer.AnnouncePrivatePendingConfigSaved(selectedFeatureName, selectedConfigName);
}
_.memory.Free(enabledConfigName);
pendingData.FreeSelf();
pendingConfigs.RemoveConfig();
return;
}
protected function NewFeatureConfig() {
local BaseText enabledConfigName;
local class<FeatureConfig> configClass;
configClass = selectedFeatureClass.default.configClass;
if (configClass == none) {
announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
return;
}
if (configClass.static.Exists(selectedConfigName)) {
announcer.AnnounceFailedConfigAlreadyExists(selectedFeatureName, 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(selectedFeatureName);
}
_.memory.Free(enabledConfigName);
announcer.AnnounceConfigCreated(selectedFeatureName, selectedConfigName);
}
protected function RemoveFeatureConfig() {
local class<FeatureConfig> configClass;
configClass = selectedFeatureClass.default.configClass;
if (configClass == none) {
announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
return;
}
if (!configClass.static.Exists(selectedConfigName)) {
announcer.AnnounceFailedConfigDoesNotExist(
selectedFeatureName,
selectedConfigName);
return;
}
pendingConfigs.RemoveConfig();
configClass.static.DeleteConfig(selectedConfigName);
announcer.AnnounceConfigRemoved(selectedFeatureName, selectedConfigName);
}
protected function SetAutoFeatureConfig() {
local Text currentAutoEnabledConfig;
local class<FeatureConfig> configClass;
configClass = selectedFeatureClass.default.configClass;
if (configClass == none) {
announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
return;
}
if (selectedConfigName != none && !configClass.static.Exists(selectedConfigName)) {
announcer.AnnounceFailedConfigDoesNotExist(selectedFeatureName, selectedConfigName);
return;
}
currentAutoEnabledConfig = configClass.static.GetAutoEnabledConfig();
if (selectedConfigName == none && currentAutoEnabledConfig == none) {
announcer.AnnounceFailedAlreadyNoAutoEnabled(selectedFeatureName);
}
else if (selectedConfigName != none &&
selectedConfigName.Compare(currentAutoEnabledConfig, SCASE_INSENSITIVE)) {
announcer.AnnounceFailedAlreadySameAutoEnabled(
selectedFeatureName,
selectedConfigName);
} else {
configClass.static.SetAutoEnabledConfig(selectedConfigName);
if (selectedConfigName != none) {
announcer.AnnounceAutoEnabledConfig(
selectedFeatureName,
selectedConfigName);
} else {
announcer.AnnounceRemovedAutoEnabledConfig(selectedFeatureName);
}
}
_.memory.Free(currentAutoEnabledConfig);
}
private function HashTable GetCurrentConfigData(BaseText configName) {
local class<FeatureConfig> configClass;
if (configName == none) {
return none;
}
configClass = selectedFeatureClass.default.configClass;
if (configClass == none) {
announcer.AnnounceFailedNoConfigClass(selectedFeatureName);
return none;
}
return configClass.static.LoadData(configName);
}
private function HashTable GetCurrentSelectedConfigData() {
return GetCurrentConfigData(selectedConfigName);
}
defaultproperties {
preferredName = "feature"
}

View file

@ -0,0 +1,594 @@
/**
* Announcer for `ACommandFeature`.
* 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_Announcer extends CommandAnnouncer;
var private AnnouncementVariations enabledFeature, disabledFeature;
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, 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();
}
public final function AnnounceEnabledFeature(
BaseText featureName,
BaseText configName)
{
local int i;
local array<TextTemplate> templates;
if (!enabledFeature.initialized)
{
enabledFeature.initialized = true;
enabledFeature.toSelfReport = _.text.MakeTemplate_S(
"Feature {$TextEmphasis `%1`} {$TextPositive enabled} with config"
@ "\"%2\"");
enabledFeature.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive enabled} feature"
@ "{$TextEmphasis `%1`} with config \"%2\"");
}
templates = MakeArray(enabledFeature);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(featureName).Arg(configName);
}
MakeAnnouncement(enabledFeature);
}
public final function AnnounceDisabledFeature(BaseText featureName)
{
local int i;
local array<TextTemplate> templates;
if (!disabledFeature.initialized)
{
disabledFeature.initialized = true;
disabledFeature.toSelfReport = _.text.MakeTemplate_S(
"Feature {$TextEmphasis `%1`} {$TextNegative disabled}");
disabledFeature.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNegative disabled} feature"
@ "{$TextEmphasis `%1`}");
}
templates = MakeArray(disabledFeature);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(featureName);
}
MakeAnnouncement(disabledFeature);
}
public final function AnnounceSwappedConfig(
BaseText featureName,
BaseText oldConfig,
BaseText newConfig)
{
local int i;
local array<TextTemplate> templates;
if (!swappedConfig.initialized)
{
swappedConfig.initialized = true;
swappedConfig.toSelfReport = _.text.MakeTemplate_S(
"Config for feature {$TextEmphasis `%1`} {$TextNeutral swapped}"
@ "from \"%2\" to \"%3\"");
swappedConfig.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNeutral swapped} config for feature"
@ "{$TextEmphasis `%1`} from \"%2\" to \"%3\"");
}
templates = MakeArray(swappedConfig);
for (i = 0; i < templates.length; i += 1)
{
templates[i]
.Reset()
.Arg(featureName)
.Arg(oldConfig)
.Arg(newConfig);
}
MakeAnnouncement(swappedConfig);
}
public final function AnnouncePublicPendingConfigSaved(
BaseText featureName)
{
local int i;
local array<TextTemplate> 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().Arg(featureName);
}
MakeAnnouncement(pendingConfigSavedPublic);
}
public final function AnnouncePrivatePendingConfigSaved(BaseText featureName, BaseText configName)
{
if (!pendingConfigSavedPrivate.initialized)
{
pendingConfigSavedPrivate.initialized = true;
pendingConfigSavedPrivate.toSelfReport = _.text.MakeTemplate_S(
"Config \"%2\" for feature {$TextEmphasis `%1`} was"
@ "{$TextNeutral modified}");
}
pendingConfigSavedPrivate.toSelfReport
.Reset()
.Arg(featureName)
.Arg(configName);
MakeAnnouncement(pendingConfigSavedPrivate);
}
public final function AnnounceCurrentConfig(
BaseText featureName,
BaseText config)
{
if (!showCurrentConfig.initialized)
{
showCurrentConfig.initialized = true;
showCurrentConfig.toSelfReport = _.text.MakeTemplate_S(
"Current config \"%2\" for feature {$TextEmphasis `%1`}:");
}
showCurrentConfig.toSelfReport
.Reset()
.Arg(featureName)
.Arg(config);
MakeAnnouncement(showCurrentConfig);
}
public final function AnnouncePendingConfig(
BaseText featureName,
BaseText config)
{
if (!showPendingConfig.initialized)
{
showPendingConfig.initialized = true;
showPendingConfig.toSelfReport = _.text.MakeTemplate_S(
"Pending config \"%2\" for feature {$TextEmphasis `%1`}:");
}
showPendingConfig.toSelfReport
.Reset()
.Arg(featureName)
.Arg(config);
MakeAnnouncement(showPendingConfig);
}
public final function AnnounceConfigCreated(
BaseText featureName,
BaseText config)
{
if (!configCreated.initialized)
{
configCreated.initialized = true;
configCreated.toSelfReport = _.text.MakeTemplate_S(
"{$TextPositive Created config} \"%2\" for feature"
@ "{$TextEmphasis `%1`}");
}
configCreated.toSelfReport
.Reset()
.Arg(featureName)
.Arg(config);
MakeAnnouncement(configCreated);
}
public final function AnnounceConfigRemoved(
BaseText featureName,
BaseText config)
{
if (!configRemoved.initialized)
{
configRemoved.initialized = true;
configRemoved.toSelfReport = _.text.MakeTemplate_S(
"{$TextNegative Removed config} \"%2\" for feature"
@ "{$TextEmphasis `%1`}");
}
configRemoved.toSelfReport
.Reset()
.Arg(featureName)
.Arg(config);
MakeAnnouncement(configRemoved);
}
public final function AnnounceConfigEdited(
BaseText featureName,
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()
.Arg(featureName)
.Arg(config)
.Arg(pathToValue)
.Arg(oldValue)
.Arg(newValue);
MakeAnnouncement(configEdited);
}
public final function AnnounceConfigNewValue(
BaseText featureName,
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()
.Arg(featureName)
.Arg(config)
.Arg(pathToValue)
.Arg(newValue);
MakeAnnouncement(configEditedNew);
}
public final function AnnounceAutoEnabledConfig(
BaseText featureName,
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()
.Arg(featureName)
.Arg(config);
MakeAnnouncement(autoEnabled);
}
public final function AnnounceRemovedAutoEnabledConfig(
BaseText featureName)
{
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()
.Arg(featureName);
MakeAnnouncement(removedAutoEnabled);
}
public final function AnnounceFailedAlreadyNoAutoEnabled(
BaseText featureName)
{
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()
.Arg(featureName);
MakeAnnouncement(failedAlreadyNoAutoEnabled);
}
public final function AnnounceFailedAlreadySameAutoEnabled(
BaseText featureName,
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()
.Arg(featureName)
.Arg(config);
MakeAnnouncement(failedAlreadySameAutoEnabled);
}
public final function AnnounceFailedConfigAlreadyExists(
BaseText featureName,
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()
.Arg(featureName)
.Arg(config);
MakeAnnouncement(failedConfigAlreadyExists);
}
public final function AnnounceFailedConfigDoesNotExist(
BaseText featureName,
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()
.Arg(featureName)
.Arg(config);
MakeAnnouncement(failedConfigDoesNotExists);
}
public final function AnnounceFailedToLoadFeatureClass(BaseText failedClassName)
{
if (!failedToLoadFeatureClass.initialized)
{
failedToLoadFeatureClass.initialized = true;
failedToLoadFeatureClass.toSelfReport = _.text.MakeTemplate_S(
"{$TextFailure Failed} to load feature class {$TextEmphasis `%1`}");
}
failedToLoadFeatureClass.toSelfReport.Reset().Arg(failedClassName);
MakeAnnouncement(failedToLoadFeatureClass);
}
public final function AnnounceFailedNoConfigProvided(
BaseText featureName)
{
if (!failedNoConfigProvided.initialized)
{
failedNoConfigProvided.initialized = true;
failedNoConfigProvided.toSelfReport = _.text.MakeTemplate_S(
"{$TextFailure No config specified} and {$TextFailure no"
@ "auto-enabled config} exists for feature {$TextEmphasis `%1`}");
}
failedNoConfigProvided.toSelfReport.Reset().Arg(featureName);
MakeAnnouncement(failedNoConfigProvided);
}
public final function AnnounceFailedConfigMissing(BaseText config)
{
if (!failedConfigMissing.initialized)
{
failedConfigMissing.initialized = true;
failedConfigMissing.toSelfReport = _.text.MakeTemplate_S(
"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(
BaseText featureName,
BaseText config)
{
if (!failedCannotEnableFeature.initialized)
{
failedCannotEnableFeature.initialized = true;
failedCannotEnableFeature.toSelfReport = _.text.MakeTemplate_S(
"Something went {$TextFailure wrong}, {$TextFailure failed} to"
@ "enable feature {$TextEmphasis `%1`} with config \"%2\"");
}
failedCannotEnableFeature.toSelfReport
.Reset()
.Arg(featureName)
.Arg(config);
MakeAnnouncement(failedCannotEnableFeature);
}
public final function AnnounceFailedNoConfigClass(
BaseText featureName)
{
if (!failedNoConfigClass.initialized)
{
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"
@ "implementation");
}
failedNoConfigClass.toSelfReport.Reset().Arg(featureName);
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(
BaseText featureName)
{
if (!failedAlreadyDisabled.initialized)
{
failedAlreadyDisabled.initialized = true;
failedAlreadyDisabled.toSelfReport = _.text.MakeTemplate_S(
"{$TextFailure Cannot disable} feature {$TextEmphasis `%1`}: it is"
@ "already {$TextNegative disabled}");
}
failedAlreadyDisabled.toSelfReport.Reset().Arg(featureName);
MakeAnnouncement(failedAlreadyDisabled);
}
public final function AnnounceFailedAlreadyEnabled(
BaseText featureName,
BaseText config)
{
if (!failedAlreadyEnabled.initialized)
{
failedAlreadyEnabled.initialized = true;
failedAlreadyEnabled.toSelfReport = _.text.MakeTemplate_S(
"{$TextFailure Cannot enable} feature {$TextEmphasis `%1`}: it is"
@ "already {$TextPositive enabled} with specified config \"%2\"");
}
failedAlreadyEnabled.toSelfReport
.Reset()
.Arg(featureName)
.Arg(config);
MakeAnnouncement(failedAlreadyEnabled);
}
public final function AnnounceFailedNoDataForConfig(
BaseText featureName,
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()
.Arg(featureName)
.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(
BaseText featureName,
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()
.Arg(featureName)
.Arg(config)
.Arg(pointer);
MakeAnnouncement(failedBadPointer);
}
defaultproperties
{
}

View file

@ -0,0 +1,224 @@
/**
* Command for making player immortal.
* 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 ACommandGod extends Command;
struct GodStatus
{
// Player to whom we grant godhood
var EPlayer target;
// Is `target` only a demigod (can get damaged, but not die)?
var bool demigod;
// Should `target` be unaffected by attacks momentum?
var bool unmovable;
};
var private bool connectedToSignal;
var private array<GodStatus> godhoodList;
var private ACommandGod_Announcer announcer;
var private const int TDAMAGE, TMOMENTUM;
protected function Finalizer()
{
connectedToSignal = false;
_server.kf.health.OnDamage(self).Disconnect();
_.memory.Free(announcer);
super.Finalizer();
}
protected function BuildData(CommandDataBuilder builder)
{
builder.Group(P("gameplay"));
builder.Summary(P("Command for making player immortal."));
builder.RequireTarget();
builder.Describe(P("Gives targeted players god status, making them"
@ "invincible."));
builder.SubCommand(P("list"));
builder.Describe(P("Reports godhood status of targeted players."));
builder.SubCommand(P("strip"));
builder.Describe(P("Strips targeted players from the godhood status."));
builder.Option(P("demi"));
builder.Describe(P("This flag makes targeted players \"demigods\" instead -"
@ "they still cannot die, but they can take any non-lethal"
@ "damage."));
builder.Option(P("unmovable"));
builder.Describe(P("This flag also prevents targeted players from being"
@ "affected by the momentum trasnferred from damaging attacks."));
announcer = ACommandGod_Announcer(
_.memory.Allocate(class'ACommandGod_Announcer'));
}
protected function ExecutedFor(
EPlayer target,
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local GodStatus newGodStatus;
announcer.Setup(target, instigator, othersConsole);
if (arguments.subCommandName.IsEmpty())
{
newGodStatus.target = target;
newGodStatus.demigod = arguments.options.HasKey(P("demi"));
newGodStatus.unmovable = arguments.options.HasKey(P("unmovable"));
MakeGod(target, newGodStatus);
}
else if (arguments.subCommandName.Compare(P("list"))) {
announcer.AnnounceGodStatus(BorrowGodStatus(target));
}
else if (arguments.subCommandName.Compare(P("strip"))) {
RemoveGod(target);
}
}
private function ProtectDivines(
EPawn target,
EPawn instigator,
HashTable damageData)
{
local int damage;
local EPlayer targetedPlayer;
local GodStatus targetDivinity;
targetedPlayer = target.GetPlayer();
targetDivinity = BorrowGodStatus(targetedPlayer);
_.memory.Free(targetedPlayer);
if (targetDivinity.target == none) {
return;
}
if (targetDivinity.unmovable) {
damageData.SetVector(T(TMOMENTUM), Vect(0.0f, 0.0f, 0.0f));
}
if (targetDivinity.demiGod)
{
damage = damageData.GetInt(T(TDAMAGE));
damage = Min(damage, target.GetHealth() - 1);
damageData.SetInt(T(TDAMAGE), damage);
}
else {
damageData.SetInt(T(TDAMAGE), 0);
}
}
private final function MakeGod(
EPlayer target,
GodStatus newGodStatus)
{
local int godIndex;
local bool wasGod;
local GodStatus oldGodStatus;
if (target == none) {
return;
}
for (godIndex = 0; godIndex < godhoodList.length; godIndex += 1)
{
if (target.SameAs(godhoodList[godIndex].target))
{
wasGod = true;
oldGodStatus = godhoodList[godIndex];
break;
}
}
if (wasGod)
{
if ( newGodStatus.demiGod == oldGodStatus.demiGod
&& newGodStatus.unmovable == oldGodStatus.unmovable)
{
announcer.AnnounceSameGod(newGodStatus);
}
else
{
announcer.AnnounceChangedGod(oldGodStatus, newGodStatus);
godhoodList[godIndex].target.FreeSelf();
newGodStatus.target.NewRef();
godhoodList[godIndex] = newGodStatus;
}
}
else {
announcer.AnnounceNewGod(newGodStatus);
newGodStatus.target.NewRef();
godhoodList[godhoodList.length] = newGodStatus;
}
UpdateHealthSignalConnection();
}
private final function RemoveGod(EPlayer target)
{
local int i;
if (target == none) {
return;
}
for (i = 0; i < godhoodList.length; i += 1)
{
if (target.SameAs(godhoodList[i].target))
{
announcer.AnnounceRemoveGod(godhoodList[i]);
godhoodList[i].target.FreeSelf();
godhoodList.Remove(i, 1);
UpdateHealthSignalConnection();
return;
}
}
announcer.AnnounceWasNotGod();
}
private final function GodStatus BorrowGodStatus(EPlayer target)
{
local int i;
local GodStatus emptyStatus;
if (target == none) {
return emptyStatus;
}
for (i = 0; i < godhoodList.length; i += 1)
{
if (target.SameAs(godhoodList[i].target)) {
return godhoodList[i];
}
}
return emptyStatus;
}
private final function UpdateHealthSignalConnection()
{
if (connectedToSignal && godhoodList.length <= 0)
{
_server.kf.health.OnDamage(self).Disconnect();
connectedToSignal = false;
}
if (!connectedToSignal && godhoodList.length > 0)
{
connectedToSignal = true;
_server.kf.health.OnDamage(self).connect = ProtectDivines;
}
}
defaultproperties
{
preferredName = "god"
TDAMAGE = 0
stringConstants(0) = "damage"
TMOMENTUM = 1
stringConstants(1) = "momentum"
}

View file

@ -0,0 +1,225 @@
/**
* Announcer for `ACommandGod`.
* 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 ACommandGod_Announcer extends CommandAnnouncer
dependson(ACommandGod);
var private AnnouncementVariations godStatus, newGod, removeGod, sameGod;
var private AnnouncementVariations changedGod, wasNotGod;
protected function Finalizer()
{
FreeVariations(godStatus);
FreeVariations(newGod);
FreeVariations(removeGod);
FreeVariations(sameGod);
FreeVariations(changedGod);
FreeVariations(wasNotGod);
super.Finalizer();
}
public final function AnnounceGodStatus(ACommandGod.GodStatus status)
{
local int i;
local MutableText statusAsText;
local array<TextTemplate> templates;
if (!godStatus.initialized)
{
godStatus.initialized = true;
godStatus.toSelfReport = _.text.MakeTemplate_S(
"You're %1");
godStatus.toOtherReport = _.text.MakeTemplate_S(
"%%target%% is %1");
}
statusAsText = DisplayStatus(status);
templates = MakeArray(godStatus);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(statusAsText);
}
_.memory.Free(statusAsText);
MakeAnnouncement(godStatus);
}
public final function AnnounceNewGod(ACommandGod.GodStatus status)
{
local int i;
local MutableText statusAsText;
local array<TextTemplate> templates;
if (!newGod.initialized)
{
newGod.initialized = true;
newGod.toSelfReport = _.text.MakeTemplate_S(
"You {$TextPositive made} yourself %1");
newGod.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive made} themselves %1");
newGod.toOtherReport = _.text.MakeTemplate_S(
"You {$TextPositive made} %%target%% %1");
newGod.toOtherPrivate = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive made} you %1");
newGod.toOtherPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive made} %%target%% %1");
}
statusAsText = DisplayStatus(status);
templates = MakeArray(newGod);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(statusAsText);
}
_.memory.Free(statusAsText);
MakeAnnouncement(newGod);
}
public final function AnnounceRemoveGod(ACommandGod.GodStatus status)
{
local int i;
local MutableText statusAsText;
local array<TextTemplate> templates;
if (!removeGod.initialized)
{
removeGod.initialized = true;
removeGod.toSelfReport = _.text.MakeTemplate_S(
"You, %1, {$TextNegative became} a mere {$TextNegative mortal}");
removeGod.toSelfPublic = _.text.MakeTemplate_S(
"%1 %%instigator%% {$TextNegative made} themselves a mere"
@ "{$TextNegative mortal}");
removeGod.toOtherReport = _.text.MakeTemplate_S(
"%1 %%target%% was {$TextNegative made} a mere"
@ "{$TextNegative mortal} by you");
removeGod.toOtherPrivate = _.text.MakeTemplate_S(
"You, %1, was {$TextNegative made} a mere {$TextNegative mortal}"
@ "by %%instigator%%");
removeGod.toOtherPublic = _.text.MakeTemplate_S(
"%1 %%target%% was {$TextNegative made} a mere"
@ "{$TextNegative mortal} by %%instigator%%");
}
statusAsText = DisplayStatus(status);
templates = MakeArray(removeGod);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(statusAsText);
}
_.memory.Free(statusAsText);
MakeAnnouncement(removeGod);
}
public final function AnnounceSameGod(ACommandGod.GodStatus status)
{
local int i;
local MutableText statusAsText;
local array<TextTemplate> templates;
if (!sameGod.initialized)
{
sameGod.initialized = true;
sameGod.toSelfReport = _.text.MakeTemplate_S(
"You are already %1");
sameGod.toOtherReport = _.text.MakeTemplate_S(
"%%target%% is already %1");
}
statusAsText = DisplayStatus(status);
templates = MakeArray(sameGod);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(statusAsText);
}
_.memory.Free(statusAsText);
MakeAnnouncement(sameGod);
}
public final function AnnounceChangedGod(
ACommandGod.GodStatus oldStatus,
ACommandGod.GodStatus newStatus)
{
local int i;
local MutableText oldStatusAsText, newStatusAsText;
local array<TextTemplate> templates;
if (!changedGod.initialized)
{
changedGod.initialized = true;
changedGod.toSelfReport = _.text.MakeTemplate_S(
"You, %1, {$TextPositive made} yourself %2");
changedGod.toSelfPublic = _.text.MakeTemplate_S(
"%1 %%instigator%% {$TextPositive made} themselves %2");
changedGod.toOtherReport = _.text.MakeTemplate_S(
"You {$TextPositive made} %1 %%target%% into %1");
changedGod.toOtherPrivate = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive made} you, %1, into %2");
changedGod.toOtherPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive made} %1 %%target%% into %2");
}
oldStatusAsText = DisplayStatus(oldStatus);
newStatusAsText = DisplayStatus(newStatus);
templates = MakeArray(changedGod);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(oldStatusAsText).Arg(newStatusAsText);
}
_.memory.Free(oldStatusAsText);
_.memory.Free(newStatusAsText);
MakeAnnouncement(changedGod);
}
public final function AnnounceWasNotGod()
{
local int i;
local array<TextTemplate> templates;
if (!sameGod.initialized)
{
sameGod.initialized = true;
sameGod.toSelfReport = _.text.MakeTemplate_S(
"You are already a mere {$TextNegative mortal}");
sameGod.toOtherReport = _.text.MakeTemplate_S(
"%%target%% is already a mere {$TextNegative mortal}");
}
templates = MakeArray(sameGod);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset();
}
MakeAnnouncement(sameGod);
}
private final function MutableText DisplayStatus(ACommandGod.GodStatus status)
{
local MutableText builder;
builder = _.text.Empty();
if (status.target == none)
{
builder.Append(F("a mere {$TextNegative mortal}"));
return builder;
}
if (status.unmovable) {
builder.Append(F("an {$TextPositive unmovable}, "));
}
else {
builder.Append(F("a {$TextNeutral simple}, "));
}
if (status.demigod) {
builder.Append(F("immortal {$TextNeutral demigod}"));
}
else {
builder.Append(F("invincible {$TextPositive god}"));
}
return builder;
}
defaultproperties
{
}

View file

@ -0,0 +1,319 @@
/**
* Command for managing (displaying + adding and removing to/items from it)
* player's inventory.
* Copyright 2021 - 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 ACommandInventory extends Command;
// Load user-specified options into the boolean flags during'
// `ExecutedFor()` and use them in auxiliary methods.
// This is might be a questionable way of doing things, but it allows to
// avoid passing flags in copious amounts to auxiliary methods and
// does not overcomplicate logic
var private bool flagAll, flagForce, flagAmmo, flagKeep;
var private bool flagEquip, flagHidden, flagGroups;
var protected const int TINVENTORY, TADD, TREMOVE, TITEMS, TEQUIP, TALL, TKEEP;
var protected const int THIDDEN, TFORCE, TAMMO, TLIST, TLISTS_NAMES, TSET;
var protected const int TLISTS_SKIPPED;
protected function BuildData(CommandDataBuilder builder)
{
builder.Group(P("gameplay"));
builder.Summary(P("Manages player's inventory."));
builder.Describe(P("Command for displaying and editing players' inventories."
@ "If called without specifying subcommand - simply displays"
@ "targeted player's inventory."));
builder.RequireTarget();
builder.SubCommand(T(TADD));
builder.OptionalParams();
builder.ParamTextList(T(TITEMS));
builder.Describe(P("This command adds items (based on listed templates) to"
@ "the targeted player's inventory."
@ "Instead of templates item aliases can be specified."));
builder.SubCommand(T(TREMOVE));
builder.OptionalParams();
builder.ParamTextList(T(TITEMS));
builder.Describe(P("This command removes items (based on listed templates)"
@ "from the targeted player's inventory."
@ "Instead of templates item aliases can be specified."));
builder.SubCommand(T(TSET));
builder.OptionalParams();
builder.ParamTextList(T(TITEMS));
builder.Describe(P("This command acts like combination of two commands -"
@ "first removing all items from the player's current inventory and"
@ "then adding specified items. first clears inventory"
@ "(based on specified options) and then "));
builder.Option(T(TEQUIP));
builder.Describe(F("Affect items currently equipped by the targeted player."
@ "Releveant for a {$TextEmphasis remove} subcommand."));
builder.Option(T(TLIST));
builder.Describe(P("Include weapons from specified group into the list."));
builder.ParamTextList(T(TLISTS_NAMES));
builder.Option(T(TAMMO));
builder.Describe(P("When adding weapons - signals that their"
@ "ammo / charge / whatever has to be filled after addition."));
builder.Option(T(TKEEP));
builder.Describe(F("Removing items by default means simply destroying them."
@ "This flag makes command to try and keep them in some form."
@ "Success for all items is not guaranteed."));
builder.Option(T(THIDDEN));
builder.Describe(F("Some of the items in the inventory are"
@ "{$TextEmphasis hidden} and are not supposed to be seem by"
@ "the player. To avoid weird behavior, {$TextEmphasis inventory}"
@ "command by default ignores them when affecting groups of items"
@ "(like when removing all items) unless they're directly"
@ "specified. This flag tells it to also affect hidden items."));
builder.Option(T(TFORCE));
builder.Describe(P("Sometimes adding and removing items is impossible due to"
@ "the limitations imposed by the game. This option allows to"
@ "ignore some of those limitation."));
builder.Option(T(TALL), P("A"));
builder.Describe(F("This flag is used when removing items. If user has"
@ "specified any weapon templates - it means"
@ "\"remove all items with these tempaltes from inventory\","
@ "but if user has not specified any templated it simply means"
@ "\"remove all items from the inventory\"."));
}
protected function ExecutedFor(
EPlayer target,
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local InventoryTool tool;
local ArrayList itemsArray, specifiedLists;
LoadUserFlags(arguments.options);
tool = class'InventoryTool'.static.CreateFor(target);
if (tool == none) {
return;
}
itemsArray = arguments.parameters.GetArrayList(T(TITEMS));
specifiedLists = arguments.options.GetArrayListBy(P("/list/lists names"));
if (arguments.subCommandName.IsEmpty()) {
tool.ReportInventory(callerConsole, flagHidden);
}
else if (arguments.subCommandName.Compare(T(TADD))) {
SubCommandAdd(tool, itemsArray, specifiedLists);
}
else if (arguments.subCommandName.Compare(T(TREMOVE))) {
SubCommandRemove(tool, itemsArray, specifiedLists);
}
else if (arguments.subCommandName.Compare(T(TSET)))
{
tool.RemoveAllItems(flagKeep, flagForce, flagHidden);
SubCommandAdd(tool, itemsArray, specifiedLists);
}
tool.SetupReportInstigator(instigator);
if (!instigator.SameAs(target)) {
tool.ReportChanges(instigator, targetConsole, IRT_Target);
}
tool.ReportChanges(instigator, callerConsole, IRT_Instigator);
tool.ReportChanges(instigator, othersConsole, IRT_Others);
_.memory.Free(tool);
_.memory.Free(itemsArray);
_.memory.Free(specifiedLists);
}
protected function SubCommandAdd(
InventoryTool tool,
ArrayList itemsArray,
ArrayList specifiedLists)
{
local int i;
local int itemsAmount;
local Text nextItem;
local array<Text> itemsFromLists;
if (tool == none) {
return;
}
if (itemsArray != none) {
itemsAmount = itemsArray.GetLength();
}
// Add items user listed manually
// Use `itemsAmount` because `itemsArray` can be `none`
for (i = 0; i < itemsAmount; i += 1)
{
nextItem = itemsArray.GetText(i);
tool.AddItem(nextItem, flagForce, flagAmmo);
_.memory.Free(nextItem);
}
// Add items from specified lists
itemsFromLists = LoadAllItemsLists(specifiedLists);
for (i = 0; i < itemsFromLists.length; i += 1) {
tool.AddItem(itemsFromLists[i], flagForce, flagAmmo);
}
_.memory.FreeMany(itemsFromLists);
}
protected function SubCommandRemove(
InventoryTool tool,
ArrayList itemsArray,
ArrayList specifiedLists)
{
local int i;
local int itemsAmount;
local Text nextItem;
local array<Text> itemsFromLists;
if (tool == none) {
return;
}
if (itemsArray != none) {
itemsAmount = itemsArray.GetLength();
}
// Remove due to "--all" option
if (flagAll && itemsAmount <= 0)
{
tool.RemoveAllItems(flagKeep, flagForce, flagHidden);
return;
}
// Remove due to "--equip" option
if (flagEquip) {
tool.RemoveEquippedItems(flagKeep, flagForce, flagHidden);
}
// Remove items user listed manually
// Use `itemsAmount` because `itemsArray` can be `none`
for (i = 0; i < itemsAmount; i += 1)
{
nextItem = itemsArray.GetText(i);
tool.RemoveItem(nextItem, flagKeep, flagForce, flagAll);
_.memory.Free(nextItem);
}
// Remove items from specified lists
itemsFromLists = LoadAllItemsLists(specifiedLists);
for (i = 0; i < itemsFromLists.length; i += 1) {
tool.RemoveItem(itemsFromLists[i], flagKeep, flagForce, flagAll);
}
_.memory.FreeMany(itemsFromLists);
}
protected function LoadUserFlags(HashTable options)
{
if (options == none)
{
flagAll = false;
flagForce = false;
flagAmmo = false;
flagKeep = false;
flagEquip = false;
flagHidden = false;
flagGroups = false;
return;
}
flagAll = options.HasKey(T(TALL));
flagForce = options.HasKey(T(TFORCE));
flagAmmo = options.HasKey(T(TAMMO));
flagKeep = options.HasKey(T(TKEEP));
flagEquip = options.HasKey(T(TEQUIP));
flagHidden = options.HasKey(T(THIDDEN));
flagGroups = options.HasKey(T(TLIST));
}
protected function array<Text> LoadAllItemsLists(ArrayList specifiedLists)
{
local int i, j;
local Text nextList;
local array<Text> result;
local array<Text> nextItemBatch;
local array<Text> availableLists;
local ListBuilder badLists;
local MutableText badListsAsText;
if (specifiedLists == none) {
return result;
}
badLists = ListBuilder(_.memory.Allocate(class'ListBuilder'));
callerConsole.Write(T(TLISTS_SKIPPED));
availableLists = _server.kf.templates.GetAvailableLists();
for (i = 0; i < specifiedLists.GetLength(); i += 1)
{
nextList = specifiedLists.GetText(i);
nextItemBatch = LoadItemsList(nextList, availableLists, badLists);
_.memory.Free(nextList);
for (j = 0; j < nextItemBatch.length; j += 1) {
result[result.length] = nextItemBatch[j];
}
}
badListsAsText = badLists.IntoMutableText();
callerConsole.WriteLine(badListsAsText);
_.memory.Free(badListsAsText);
_.memory.FreeMany(availableLists);
return result;
}
protected function array<Text> LoadItemsList(
BaseText listName,
array<BaseText> availableLists,
ListBuilder badLists)
{
local int i;
local array<Text> emptyArray;
if (listName == none) {
return emptyArray;
}
// Try exact matching first
for (i = 0; i < availableLists.length; i += 1)
{
if (availableLists[i].Compare(listName, SCASE_INSENSITIVE)) {
return _server.kf.templates.GetItemList(availableLists[i]);
}
}
// Prefix matching otherwise
for (i = 0; i < availableLists.length; i += 1)
{
if (availableLists[i].StartsWith(listName, SCASE_INSENSITIVE)) {
return _server.kf.templates.GetItemList(availableLists[i]);
}
}
badLists.Item(listName);
return emptyArray;
}
defaultproperties
{
preferredName = "inventory"
TINVENTORY = 0
stringConstants(0) = "inventory"
TADD = 1
stringConstants(1) = "add"
TREMOVE = 2
stringConstants(2) = "remove"
TITEMS = 3
stringConstants(3) = "items"
TEQUIP = 4
stringConstants(4) = "equip"
TALL = 5
stringConstants(5) = "all"
TKEEP = 6
stringConstants(6) = "keep"
THIDDEN = 7
stringConstants(7) = "hidden"
TFORCE = 8
stringConstants(8) = "force"
TAMMO = 9
stringConstants(9) = "ammo"
TLIST = 10
stringConstants(10) = "list"
TLISTS_NAMES = 11
stringConstants(11) = "lists names"
TSET = 12
stringConstants(12) = "set"
TLISTS_SKIPPED = 13
stringConstants(13) = "Following lists could not have been found and will be {$TextFailure skipped}:"
}

View file

@ -0,0 +1,133 @@
/**
* Command for changing nickname of the player.
* Copyright 2021-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 ACommandNick extends Command;
var private bool foundErrors;
var private MutableText newName;
var private ACommandNick_Announcer announcer;
protected function Finalizer()
{
_.memory.Free(announcer);
_.memory.Free(newName);
newName = none;
super.Finalizer();
}
protected function BuildData(CommandDataBuilder builder)
{
builder.Group(P("gameplay"));
builder.Summary(P("Changes nickname."));
builder.RequireTarget();
builder.ParamRemainder(P("nick"));
builder.Describe(P("Changes nickname of targeted players to <nick>."));
builder.Option(P("plain"));
builder.Describe(P("Take nickname exactly as typed, without attempting to"
@ "treat it like formatted string."));
builder.Option(P("fix"), P("f"));
builder.Describe(P("In case of a nickname with erroroneous formatting or"
@ "invalid default color (specified with `--color`),"
@ "try to fix/ignore it instead of simply rejecting it."));
builder.Option(P("color"));
builder.Describe(P("Color to use for the nickname. In case nickname is already"
@ "colored, this flag will only affects uncolored parts."));
builder.ParamText(P("default_color"));
announcer = ACommandNick_Announcer(
_.memory.Allocate(class'ACommandNick_Announcer'));
}
protected function Executed(
CallData arguments,
EPlayer callerPlayer,
CommandPermissions permissions
) {
local Text givenName;
local array<FormattingErrorsReport.FormattedStringError> errors;
givenName = arguments.parameters.GetText(P("nick"));
// `newName`'s reference persists between different command calls and
// only deallocated when we need this variable for the next execution.
// "Leaking" a single `Text` like that is insignificant.
_.memory.Free(newName);
newName = _.text.Empty();
if (arguments.options.HasKey(P("plain"))) {
newName = givenName.MutableCopy();
}
else
{
errors = class'FormattingStringParser'.static
.ParseFormatted(givenName, newName, true);
}
foundErrors = false;
if (arguments.options.HasKey(P("color")))
{
foundErrors = !TryChangeDefaultColor(
arguments.options.GetTextBy(P("/color/default_color")));
}
foundErrors = foundErrors || (errors.length > 0);
class'FormattingReportTool'.static.Report(callerConsole, errors);
class'FormattingReportTool'.static.FreeErrors(errors);
}
protected function ExecutedFor(
EPlayer target,
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local Text alteredVersion;
if (!foundErrors || arguments.options.HasKey(P("fix")))
{
announcer.Setup(target, instigator, othersConsole);
target.SetName(newName);
alteredVersion = target.GetName();
if (newName.Compare(alteredVersion, SCASE_SENSITIVE, SFORM_SENSITIVE)) {
announcer.AnnounceChangedNickname(newName);
}
else {
announcer.AnnounceChangedAlteredNickname(newName, alteredVersion);
}
_.memory.Free(alteredVersion);
}
}
protected function bool TryChangeDefaultColor(BaseText specifiedColor)
{
local Color defaultColor;
if (newName == none) return false;
if (specifiedColor == none) return false;
if (_.color.Parse(specifiedColor, defaultColor))
{
newName.ChangeDefaultColor(defaultColor);
return true;
}
callerConsole
.Write(F("Specified {$TextFailure invalid} color: "))
.WriteLine(specifiedColor);
return false;
}
defaultproperties {
preferredName = "nick"
}

View file

@ -0,0 +1,96 @@
/**
* Announcer for `ACommandNick`.
* 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 ACommandNick_Announcer extends CommandAnnouncer;
var private AnnouncementVariations changedNickname, changedAlteredNickname;
protected function Finalizer()
{
FreeVariations(changedNickname);
FreeVariations(changedAlteredNickname);
super.Finalizer();
}
public final function AnnounceChangedNickname(BaseText newNickname)
{
local int i;
local array<TextTemplate> templates;
if (!changedNickname.initialized)
{
changedNickname.initialized = true;
changedNickname.toSelfReport = _.text.MakeTemplate_S(
"Your nickname {$TextNeutral changed} to \"%1\"");
changedNickname.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNeutral changed} their own nickname to"
@ "\"%1\"");
changedNickname.toOtherReport = _.text.MakeTemplate_S(
"Nickname for %%target%% {$TextNeutral changed} to"
@ "\"%1\"");
changedNickname.toOtherPrivate = _.text.MakeTemplate_S(
"%%instigator%% {$TextNeutral changed} your nickname to \"%1\"");
changedNickname.toOtherPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNeutral changed} nickname for player"
@ "%%target%% to \"%1\"");
}
templates = MakeArray(changedNickname);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(newNickname);
}
MakeAnnouncement(changedNickname);
}
public final function AnnounceChangedAlteredNickname(
BaseText newNickname,
BaseText alteredVersion)
{
local int i;
local array<TextTemplate> templates;
if (!changedAlteredNickname.initialized)
{
changedAlteredNickname.initialized = true;
changedAlteredNickname.toSelfReport = _.text.MakeTemplate_S(
"Your nickname was {$TextNeutral changed} to \"%1\", but then"
@ "got altered by the game into \"%2\"");
changedAlteredNickname.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% has {$TextNeutral changed} their own nickname to"
@ "\"%1\", but it was altered by the game into \"%2\"");
changedAlteredNickname.toOtherReport = _.text.MakeTemplate_S(
"Nickname for %%target%% was {$TextNeutral changed} to"
@ "\"%1\", but then got altered by the game into \"%2\"");
changedAlteredNickname.toOtherPrivate = _.text.MakeTemplate_S(
"%%instigator%% has {$TextNeutral changed} your nickname to \"%1\","
@ "but then it got altered by the game into \"%2\"");
changedAlteredNickname.toOtherPublic = _.text.MakeTemplate_S(
"%%instigator%% has {$TextNeutral changed} nickname for player"
@ "%%target%% to \"%1\", but then it got altered by the game"
@ "into \"%2\"");
}
templates = MakeArray(changedAlteredNickname);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(newNickname).Arg(alteredVersion);
}
MakeAnnouncement(changedAlteredNickname);
}
defaultproperties
{
}

View file

@ -0,0 +1,124 @@
/**
* Command for spawning new entities into the world.
* 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 ACommandSpawn extends Command;
// TODO: use spawned name for errors output?
var private ACommandSpawn_Announcer announcer;
protected function Finalizer()
{
_.memory.Free(announcer);
super.Finalizer();
}
protected function BuildData(CommandDataBuilder builder)
{
builder.Group(P("debug"));
builder.Summary(P("Spawns new entity on the map."));
builder.ParamText(P("template"),, P("entity"));
builder.Describe(P("Spawns new entity based on the given template at the point"
@ "player is currently looking at."));
builder.SubCommand(P("at"));
builder.ParamText(P("template"),, P("entity"));
builder.ParamNumber(P("x"));
builder.ParamNumber(P("y"));
builder.ParamNumber(P("z"));
builder.Describe(P("Spawns new entity based on the given template at"
@ "the point, given by the coordinates"));
announcer = ACommandSpawn_Announcer(
_.memory.Allocate(class'ACommandSpawn_Announcer'));
}
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local HashTable value;
local Vector spawnLocation;
announcer.Setup(none, instigator, othersConsole);
value = arguments.parameters.GetHashTable(P("template"));
if (arguments.subCommandName.IsEmpty()) {
SpawnInInstigatorSight(instigator, value);
} else if (arguments.subCommandName.Compare(P("at"), SCASE_INSENSITIVE)) {
spawnLocation.x = arguments.parameters.GetFloat(P("x"));
spawnLocation.y = arguments.parameters.GetFloat(P("y"));
spawnLocation.z = arguments.parameters.GetFloat(P("z"));
SpawnAt(instigator, value, spawnLocation);
}
_.memory.Free(value);
}
private final function SpawnAt(
EPlayer instigator,
HashTable value,
Vector spawnLocation
) {
local Text humanReadable, template;
local EPlaceable result;
humanReadable = value.GetText(P("alias"));
template = value.GetText(P("value"));
result = _server.kf.world.Spawn(template, spawnLocation);
if (result != none) {
announcer.AnnounceSpawned(humanReadable);
} else {
announcer.AnnounceSpawningFailed(humanReadable);
}
_.memory.Free2(humanReadable, result);
}
private final function SpawnInInstigatorSight(
EPlayer instigator,
HashTable value
) {
local EPlaceable result;
local Vector spawnLocation;
local TracingIterator iter;
local Text humanReadable, template;
humanReadable = value.GetText(P("alias"));
template = value.GetText(P("value"));
iter = _server.kf.world.TracePlayerSight(instigator);
iter.LeaveOnlyVisible();
if (iter.HasFinished()) {
announcer.AnnounceFailedTrace();
return;
}
spawnLocation = iter.GetHitLocation();
result = _server.kf.world.Spawn(template, spawnLocation);
// Shift position back a little and try again;
// this should fix a ton of spawning failures
if (result == none) {
spawnLocation = spawnLocation + Normal(iter.GetTracingStart() - spawnLocation) * 100;
result = _server.kf.world.Spawn(template, spawnLocation);
}
if (result != none) {
announcer.AnnounceSpawned(humanReadable);
} else {
announcer.AnnounceSpawningFailed(humanReadable);
}
_.memory.Free4(result, iter, humanReadable, result);
}
defaultproperties {
preferredName = "spawn"
}

View file

@ -0,0 +1,90 @@
/**
* Announcer for `ACommandSpawn`.
* 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 ACommandSpawn_Announcer extends CommandAnnouncer;
var private AnnouncementVariations spawned, spawningFailed, failedTrace;
protected function Finalizer()
{
FreeVariations(spawned);
FreeVariations(spawningFailed);
FreeVariations(failedTrace);
super.Finalizer();
}
public final function AnnounceSpawned(BaseText template)
{
local int i;
local array<TextTemplate> templates;
if (!spawned.initialized)
{
spawned.initialized = true;
spawned.toSelfReport = _.text.MakeTemplate_S(
"You {$TextPositive spawned} {$TextEmphasis %1}!");
spawned.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNeutral spawned} {$TextEmphasis %1}!");
}
templates = MakeArray(spawned);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(template);
}
MakeAnnouncement(spawned);
}
public final function AnnounceSpawningFailed(BaseText template)
{
local int i;
local array<TextTemplate> templates;
if (!spawningFailed.initialized)
{
spawningFailed.initialized = true;
spawningFailed.toSelfReport = _.text.MakeTemplate_S(
"{$TextFailure Couldn't spawn} {$TextEmphasis %1}!");
}
templates = MakeArray(spawningFailed);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(template);
}
MakeAnnouncement(spawningFailed);
}
public final function AnnounceFailedTrace()
{
local int i;
local array<TextTemplate> templates;
if (!failedTrace.initialized)
{
failedTrace.initialized = true;
failedTrace.toSelfReport = _.text.MakeTemplate_S(
"{$TextFailure Failed} to trace spawn point");
}
templates = MakeArray(failedTrace);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset();
}
MakeAnnouncement(failedTrace);
}
defaultproperties
{
}

View file

@ -0,0 +1,699 @@
/**
* Command for managing trader time and traders.
* Copyright 2021-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 ACommandTrader extends Command;
var private ACommandTrader_Announcer announcer;
var protected const int TLIST, TOPEN, TCLOSE, TENABLE, TDISABLE, TAUTO_OPEN;
var protected const int TTRADER, TTRADERS, TALL, TAUTO_OPEN_QUESTION, TQUOTE;
var protected const int TAUTO_OPEN_FLAG, TDISABLED_FLAG, TUNKNOWN_TRADERS;
var protected const int TLIST_TRADERS, TCOMMA_SPACE, TSELECTED_FLAG;
var protected const int TPARENTHESIS_OPEN, TPARENTHESIS_CLOSE;
var protected const int TSELECT, TIGNORE_DOORS, TBOOT, TTRADER_TIME, TTIME;
var protected const int TIGNORE_PLAYERS, TPAUSE, TUNPAUSE, TCANNOT_PARSE_PARAM;
var protected const int TCLOSEST, TSPACE;
protected function Finalizer()
{
_.memory.Free(announcer);
super.Finalizer();
}
protected function BuildData(CommandDataBuilder builder)
{
builder.Group(P("gameplay"));
builder.Summary(P("Manages trader time and available traders."));
builder.Describe(P("Enables of disables trading."));
builder.ParamBoolean(T(TENABLE));
builder.SubCommand(T(TTIME));
builder.Describe(F("Changes current trader time if numeric value is specified."
@ "You can also pause trader countdown by specifying"
@ "{$TextEmphasis pause} or turn it back on with"
@ "{$TextEmphasis unpause}."));
builder.ParamText(T(TTRADER_TIME));
builder.SubCommand(T(TLIST));
builder.Describe(P("Lists names of all available traders and"
@ "marks closest one to the caller."));
builder.SubCommand(T(TOPEN));
builder.Describe(P("Opens specified traders."));
builder.OptionalParams();
builder.ParamTextList(T(TTRADERS));
builder.SubCommand(T(TCLOSE));
builder.Describe(P("Closes specified traders."));
builder.OptionalParams();
builder.ParamTextList(T(TTRADERS));
builder.SubCommand(T(TAUTO_OPEN));
builder.Describe(P("Sets whether specified traders are open automatically."));
builder.ParamBoolean(T(TAUTO_OPEN_QUESTION));
builder.OptionalParams();
builder.ParamTextList(T(TTRADERS));
builder.SubCommand(T(TSELECT));
builder.Describe(P("Selects specified trader."));
builder.OptionalParams();
builder.ParamText(T(TTRADER));
builder.SubCommand(T(TBOOT));
builder.Describe(P("Boots all players from specified traders. If no traders"
@ "were specified - assumes that all of them should be affected."));
builder.OptionalParams();
builder.ParamTextList(T(TTRADERS));
builder.SubCommand(T(TENABLE));
builder.Describe(P("Enables specified traders."));
builder.OptionalParams();
builder.ParamTextList(T(TTRADERS));
builder.SubCommand(T(TDISABLE));
builder.Describe(P("Disables specified traders."));
builder.OptionalParams();
builder.ParamTextList(T(TTRADERS));
builder.Option(T(TALL));
builder.Describe(P("If sub-command targets shops, this flag will make it"
@ "target all the available shops."));
builder.Option(T(TCLOSEST));
builder.Describe(P("If sub-command targets shops, this flag will make it also"
@ "target closest shop to the caller."));
builder.Option(T(TIGNORE_DOORS));
builder.Describe(F("When used with {$TextEmphasis select} sub-command, it will"
@ "neither open or close doors."));
builder.Option(T(TIGNORE_PLAYERS), P("I"));
builder.Describe(P("Normally commands that close doors will automatically boot"
@ "players from inside to prevent locking them in. This flag forces"
@ "this command to leave players inside. However they can still be"
@ "booted out at the end of trading time. Also it is impossible to"
@ "disable the trader and not boot players inside it."));
announcer = ACommandTrader_Announcer(
_.memory.Allocate(class'ACommandTrader_Announcer'));
}
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local bool newTradingStatus;
announcer.Setup(none, instigator, othersConsole);
if (arguments.subCommandName.IsEmpty())
{
newTradingStatus = arguments.parameters.GetBool(T(TENABLE));
if ( arguments.parameters.GetBool(T(TENABLE))
== _server.kf.trading.IsTradingActive())
{
announcer.AnnounceTradingNoChange();
}
_server.kf.trading.SetTradingStatus(newTradingStatus);
if (newTradingStatus) {
announcer.AnnounceActivatedTrading();
}
else {
announcer.AnnounceDeactivatedTrading();
}
}
else if (arguments.subCommandName.Compare(T(TLIST))) {
ListTradersFor(instigator);
}
else if (arguments.subCommandName.Compare(T(TTIME), SCASE_INSENSITIVE)) {
HandleTraderTime(arguments);
}
else if (arguments.subCommandName.Compare(T(TOPEN), SCASE_INSENSITIVE)) {
SetTradersOpen(true, arguments, instigator);
}
else if (arguments.subCommandName.Compare(T(TCLOSE), SCASE_INSENSITIVE)) {
SetTradersOpen(false, arguments, instigator);
}
else if (arguments.subCommandName.Compare(T(TSELECT), SCASE_INSENSITIVE)) {
SelectTrader(arguments, instigator);
}
else if (arguments.subCommandName.Compare(T(TBOOT), SCASE_INSENSITIVE)) {
BootFromTraders(arguments, instigator);
}
else if (arguments.subCommandName.Compare(T(TENABLE), SCASE_INSENSITIVE)) {
SetTradersEnabled(true, arguments, instigator);
}
else if (arguments.subCommandName.Compare(T(TDISABLE), SCASE_INSENSITIVE)) {
SetTradersEnabled(false, arguments, instigator);
}
else if (arguments.subCommandName.Compare(T(TAUTO_OPEN), SCASE_INSENSITIVE))
{
SetTradersAutoOpen(arguments, instigator);
}
}
protected function ListTradersFor(EPlayer target)
{
local int i;
local ETrader closestTrader;
local array<ETrader> availableTraders;
if (target == none) {
return;
}
availableTraders = _server.kf.trading.GetTraders();
callerConsole.Flush()
.UseColorOnce(_.color.TextEmphasis).Write(T(TLIST_TRADERS));
closestTrader = FindClosestTrader(target);
for (i = 0; i < availableTraders.length; i += 1)
{
WriteTrader(availableTraders[i],
availableTraders[i].SameAs(closestTrader));
if (i != availableTraders.length - 1) {
callerConsole.Write(T(TCOMMA_SPACE));
}
}
_.memory.Free(closestTrader);
_.memory.FreeMany(availableTraders);
callerConsole.Flush();
}
protected function HandleTraderTime(CallData result)
{
local bool oldIsPaused, newIsPaused;
local int countDownValue;
local Text parameter;
local Parser parser;
parameter = result.parameters.GetText(T(TTRADER_TIME));
if (parameter.Compare(T(TPAUSE), SCASE_INSENSITIVE))
{
oldIsPaused = _server.kf.trading.IsCountDownPaused();
if (!oldIsPaused) {
_server.kf.trading.SetCountdownPause(true);
}
newIsPaused = _server.kf.trading.IsCountDownPaused();
if (oldIsPaused != newIsPaused) {
announcer.AnnouncePausedTime();
}
return;
}
else if (parameter.Compare(T(TUNPAUSE), SCASE_INSENSITIVE))
{
oldIsPaused = _server.kf.trading.IsCountDownPaused();
if (oldIsPaused) {
_server.kf.trading.SetCountdownPause(false);
}
newIsPaused = _server.kf.trading.IsCountDownPaused();
if (oldIsPaused != newIsPaused) {
announcer.AnnounceUnpausedTime();
}
return;
}
parser = _.text.Parse(parameter);
if (parser.MInteger(countDownValue).Ok())
{
_server.kf.trading.SetCountdown(countDownValue);
announcer.AnnounceChangedCountdown(_server.kf.trading.GetCountdown());
}
else
{
callerConsole
.UseColor(_.color.TextFailure)
.Write(T(TCANNOT_PARSE_PARAM))
.WriteLine(parameter)
.ResetColor();
}
parser.FreeSelf();
}
protected function SetTradersOpen(
bool doOpen,
CallData result,
EPlayer callerPlayer)
{
local int i;
local bool needToBootPlayers;
local array<ETrader> selectedTraders;
local Text nextTraderName;
local ListBuilder affectedTraders;
affectedTraders = ListBuilder(_.memory.Allocate(class'ListBuilder'));
selectedTraders = GetTradersArray(result, callerPlayer);
needToBootPlayers = !doOpen
&& !result.options.HasKey(T(TIGNORE_PLAYERS));
for (i = 0; i < selectedTraders.length; i += 1)
{
if (selectedTraders[i].IsOpen() != doOpen)
{
nextTraderName = selectedTraders[i].GetName();
affectedTraders.Item(nextTraderName);
_.memory.Free(nextTraderName);
}
selectedTraders[i].SetOpen(doOpen);
if (needToBootPlayers) {
selectedTraders[i].BootPlayers();
}
}
if (doOpen) {
announcer.AnnounceTradersOpened(affectedTraders);
}
else {
announcer.AnnounceTradersClosed(affectedTraders);
}
_.memory.FreeMany(selectedTraders);
_.memory.Free(affectedTraders);
}
protected function bool AreTradersSame(ETrader trader1, ETrader trader2)
{
if (trader1 == none && trader2 == none) return true;
if (trader1 == none && trader2 != none) return false;
if (trader1 != none && trader2 == none) return false;
return trader1.SameAs(trader2);
}
protected function SelectTrader(CallData result, EPlayer callerPlayer)
{
local Text specifiedTraderName;
local ETrader previouslySelectedTrader, newlySelectedTrader;
previouslySelectedTrader = _server.kf.trading.GetSelectedTrader();
specifiedTraderName = result.parameters.GetText(T(TTRADER));
// Try to get trader user want to select:
// first try closes (if option is specified), next trader's name
if (callerPlayer != none && result.options.HasKey(T(TCLOSEST))) {
newlySelectedTrader = FindClosestTrader(callerPlayer);
}
if (newlySelectedTrader == none) {
newlySelectedTrader = _server.kf.trading.GetTrader(specifiedTraderName);
}
// If nothing is found, but name was specified - there is an error
if (newlySelectedTrader == none && specifiedTraderName != none)
{
callerConsole.Flush()
.UseColorOnce(_.color.TextFailure).Write(T(TUNKNOWN_TRADERS))
.WriteLine(specifiedTraderName);
_.memory.Free(previouslySelectedTrader);
return;
}
// Select proper trader
HandleTraderSwap(result, previouslySelectedTrader, newlySelectedTrader);
_server.kf.trading.SelectTrader(newlySelectedTrader);
// Report change
if (AreTradersSame(previouslySelectedTrader, newlySelectedTrader)) {
announcer.AnnounceSelectedSameTrader();
}
else if (newlySelectedTrader == none) {
announcer.AnnounceSelectedNoTrader();
}
else {
announcer.AnnounceSelectedTrader(newlySelectedTrader);
}
_.memory.Free(previouslySelectedTrader);
_.memory.Free(newlySelectedTrader);
}
// Boot players from the old trader iff
// 1. It is different from the new one (otherwise swapping means nothing);
// 2. Option "ignore-players" was not specified.
// 3. New trader was actually closed.
protected function HandleTraderSwap(
CallData result,
ETrader oldTrader,
ETrader newTrader)
{
local bool closeOldTrader, openNewTrader;
if (oldTrader == none) return;
if (oldTrader.SameAs(newTrader)) return;
closeOldTrader = newTrader == none || !newTrader.IsOpen();
openNewTrader = oldTrader.IsOpen();
if (closeOldTrader)
{
if (!result.options.HasKey(T(TIGNORE_DOORS))) {
oldTrader.Close();
}
if (!result.options.HasKey(T(TIGNORE_PLAYERS))) {
oldTrader.BootPlayers();
}
}
if (openNewTrader && newTrader != none) {
newTrader.Open();
}
}
protected function BootFromTraders(CallData result, EPlayer callerPlayer)
{
local int i;
local array<ETrader> selectedTraders;
local Text nextTraderName;
local ListBuilder affectedTraderList;
affectedTraderList = ListBuilder(_.memory.Allocate(class'ListBuilder'));
selectedTraders = GetTradersArray(result, callerPlayer);
if (selectedTraders.length <= 0) {
selectedTraders = _server.kf.trading.GetTraders();
}
for (i = 0; i < selectedTraders.length; i += 1)
{
nextTraderName = selectedTraders[i].GetName();
affectedTraderList.Item(nextTraderName);
selectedTraders[i].BootPlayers();
_.memory.Free(nextTraderName);
}
announcer.AnnounceBootedPlayers(affectedTraderList);
_.memory.FreeMany(selectedTraders);
_.memory.Free(affectedTraderList);
}
protected function SetTradersEnabled(
bool doEnable,
CallData result,
EPlayer callerPlayer)
{
local int i;
local array<ETrader> selectedTraders;
local Text nextTraderName;
local ListBuilder affectedTraderList;
affectedTraderList = ListBuilder(_.memory.Allocate(class'ListBuilder'));
selectedTraders = GetTradersArray(result, callerPlayer);
for (i = 0; i < selectedTraders.length; i += 1)
{
if (doEnable != selectedTraders[i].IsEnabled())
{
nextTraderName = selectedTraders[i].GetName();
affectedTraderList.Item(nextTraderName);
_.memory.Free(nextTraderName);
}
selectedTraders[i].SetEnabled(doEnable);
}
if (doEnable) {
announcer.AnnounceEnabledTraders(affectedTraderList);
}
else {
announcer.AnnounceDisabledTraders(affectedTraderList);
}
_.memory.FreeMany(selectedTraders);
_.memory.Free(affectedTraderList);
}
protected function SetTradersAutoOpen(CallData result, EPlayer callerPlayer)
{
local int i;
local bool doAutoOpen;
local array<ETrader> selectedTraders;
local Text nextTraderName;
local ListBuilder affectedTraderList;
affectedTraderList = ListBuilder(_.memory.Allocate(class'ListBuilder'));
doAutoOpen = result.parameters.GetBool(T(TAUTO_OPEN_QUESTION));
selectedTraders = GetTradersArray(result, callerPlayer);
for (i = 0; i < selectedTraders.length; i += 1)
{
if (doAutoOpen != selectedTraders[i].IsAutoOpen())
{
nextTraderName = selectedTraders[i].GetName();
affectedTraderList.Item(nextTraderName);
_.memory.Free(nextTraderName);
}
selectedTraders[i].SetAutoOpen(doAutoOpen);
}
if (doAutoOpen) {
announcer.AnnounceAutoOpenTraders(affectedTraderList);
}
else {
announcer.AnnounceDoNotAutoOpenTraders(affectedTraderList);
}
_.memory.FreeMany(selectedTraders);
_.memory.Free(affectedTraderList);
}
// Reads traders specified for the command (if any).
// Assumes `result != none`.
protected function array<ETrader> GetTradersArray(
CallData result,
EPlayer callerPlayer)
{
local int i, j;
local Text nextTraderName, nextSpecifiedTrader;
local ArrayList specifiedTrades;
local array<ETrader> resultTraders;
local array<ETrader> availableTraders;
// Boundary cases: all traders and no traders at all
availableTraders = _server.kf.trading.GetTraders();
if (result.options.HasKey(T(TALL))) {
return availableTraders;
}
// Add closest one, if flag tells us to
if (result.options.HasKey(T(TCLOSEST)))
{
resultTraders =
InsertTrader(resultTraders, FindClosestTrader(callerPlayer));
}
specifiedTrades = result.parameters.GetArrayList(T(TTRADERS));
if (specifiedTrades == none) {
return resultTraders;
}
// We iterate over `availableTraders` in the outer loop because:
// 1. Each `ETrader` from `availableTraders` will be matched only once,
// ensuring that result will not contain duplicate instances;
// 2. `availableTraders.GetName()` creates a new `Text` copy and
// `specifiedTrades.GetText()` does not.
for (i = 0; i < availableTraders.length; i += 1)
{
nextTraderName = availableTraders[i].GetName();
for (j = 0; j < specifiedTrades.GetLength(); j += 1)
{
nextSpecifiedTrader = specifiedTrades.GetText(j);
if (nextTraderName.Compare(nextSpecifiedTrader))
{
resultTraders =
InsertTrader(resultTraders, availableTraders[i]);
availableTraders[i] = none;
specifiedTrades.Remove(j, 1);
_.memory.Free(nextSpecifiedTrader);
break;
}
_.memory.Free(nextSpecifiedTrader);
}
nextTraderName.FreeSelf();
if (specifiedTrades.GetLength() <= 0) {
break;
}
}
// Some of the remaining trader names inside `specifiedTrades` do not
// match any actual traders. Report it.
if (callerPlayer != none && specifiedTrades.GetLength() > 0) {
ReportUnknowTraders(specifiedTrades);
}
_.memory.Free(specifiedTrades);
_.memory.FreeMany(availableTraders);
return resultTraders;
}
// Auxiliary method that adds `newTrader` into existing array of traders
// if it is still missing.
protected function array<ETrader> InsertTrader(
/* take */ array<ETrader> traders,
/* take */ ETrader newTrader)
{
local int i;
if (newTrader == none) {
return traders;
}
for (i = 0; i < traders.length; i += 1)
{
if (traders[i].SameAs(newTrader))
{
_.memory.Free(newTrader);
return traders;
}
}
traders[traders.length] = newTrader;
return traders;
}
protected function ReportUnknowTraders(ArrayList specifiedTrades)
{
local int i;
local Text nextTraderName;
if (specifiedTrades == none) {
return;
}
callerConsole.Flush()
.UseColorOnce(_.color.TextNegative).Write(T(TUNKNOWN_TRADERS));
for (i = 0; i < specifiedTrades.GetLength(); i += 1)
{
nextTraderName = specifiedTrades.GetText(i);
callerConsole.Write(nextTraderName);
_.memory.Free(nextTraderName);
if (i != specifiedTrades.GetLength() - 1) {
callerConsole.Write(T(TCOMMA_SPACE));
}
}
callerConsole.Flush();
}
// Find closest trader to the `target` player
protected function ETrader FindClosestTrader(EPlayer target)
{
local int i;
local float newDistance, bestDistance;
local ETrader bestTrader;
local array<ETrader> availableTraders;
local Vector targetLocation;
if (target == none) {
return none;
}
targetLocation = target.GetLocation();
availableTraders = _server.kf.trading.GetTraders();
for (i = 0; i < availableTraders.length; i += 1)
{
newDistance =
VSizeSquared(availableTraders[i].GetLocation() - targetLocation);
if (bestTrader == none || newDistance < bestDistance)
{
bestDistance = newDistance;
_.memory.Free(bestTrader);
bestTrader = availableTraders[i];
availableTraders[i] = none;
}
}
_.memory.FreeMany(availableTraders);
return bestTrader;
}
// Writes a trader name along with information on whether it's
// disabled / auto-open
protected function WriteTrader(
ETrader traderToWrite,
bool isClosestTrader)
{
local Text traderName;
if (traderToWrite == none) {
return;
}
callerConsole.Write(T(TQUOTE));
if (traderToWrite.IsOpen()) {
callerConsole.UseColor(_.color.TextPositive);
}
else {
callerConsole.UseColor(_.color.TextNegative);
}
traderName = traderToWrite.GetName();
callerConsole.Write(traderName)
.ResetColor()
.Write(T(TQUOTE));
traderName.FreeSelf();
WriteTraderTags(traderToWrite, isClosestTrader);
}
protected function WriteTraderTags(ETrader traderToWrite, bool isClosest)
{
local bool hasTagsInFront;
local bool isAutoOpen, isSelected;
if (traderToWrite == none) {
return;
}
if (!traderToWrite.IsEnabled())
{
callerConsole.Write(T(TDISABLED_FLAG));
return;
}
isAutoOpen = traderToWrite.IsAutoOpen();
isSelected = traderToWrite.IsSelected();
if (!isAutoOpen && !isSelected && !isClosest) {
return;
}
callerConsole.Write(T(TSPACE)).Write(T(TPARENTHESIS_OPEN));
if (isClosest)
{
callerConsole.Write(T(TCLOSEST));
hasTagsInFront = true;
}
if (isAutoOpen)
{
if (hasTagsInFront) {
callerConsole.Write(T(TCOMMA_SPACE));
}
callerConsole.Write(T(TAUTO_OPEN_FLAG));
hasTagsInFront = true;
}
if (isSelected)
{
if (hasTagsInFront) {
callerConsole.Write(T(TCOMMA_SPACE));
}
callerConsole.Write(T(TSELECTED_FLAG));
}
callerConsole.Write(T(TPARENTHESIS_CLOSE));
}
defaultproperties
{
preferredName = "trader"
TLIST = 0
stringConstants(0) = "list"
TOPEN = 1
stringConstants(1) = "open"
TCLOSE = 2
stringConstants(2) = "close"
TENABLE = 3
stringConstants(3) = "enable"
TDISABLE = 4
stringConstants(4) = "disable"
TAUTO_OPEN = 5
stringConstants(5) = "autoopen"
TTRADER = 6
stringConstants(6) = "trader"
TTRADERS = 7
stringConstants(7) = "traders"
TALL = 8
stringConstants(8) = "all"
TAUTO_OPEN_QUESTION = 9
stringConstants(9) = "autoOpen?"
TQUOTE = 10
stringConstants(10) = "\""
TAUTO_OPEN_FLAG = 11
stringConstants(11) = "auto-open"
TDISABLED_FLAG = 12
stringConstants(12) = " (disabled)"
TUNKNOWN_TRADERS = 13
stringConstants(13) = "Could not find some of the traders: "
TLIST_TRADERS = 14
stringConstants(14) = "List of available traders: "
TCOMMA_SPACE = 15
stringConstants(15) = ", "
TPARENTHESIS_OPEN = 16
stringConstants(16) = "("
TPARENTHESIS_CLOSE = 17
stringConstants(17) = ")"
TSELECTED_FLAG = 18
stringConstants(18) = "selected"
TSELECT = 19
stringConstants(19) = "select"
TIGNORE_DOORS = 20
stringConstants(20) = "ignore-doors"
TBOOT = 21
stringConstants(21) = "boot"
TTIME = 22
stringConstants(22) = "time"
TTRADER_TIME = 23
stringConstants(23) = "traderTime"
TIGNORE_PLAYERS = 24
stringConstants(24) = "ignore-players"
TPAUSE = 25
stringConstants(25) = "pause"
TUNPAUSE = 26
stringConstants(26) = "unpause"
TCANNOT_PARSE_PARAM = 27
stringConstants(27) = "Cannot parse parameter: "
TCLOSEST = 28
stringConstants(28) = "closest"
TSPACE = 29
stringConstants(29) = " "
}

View file

@ -0,0 +1,420 @@
/**
* Announcer for `ACommandTrader`.
* 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 ACommandTrader_Announcer extends CommandAnnouncer;
var private AnnouncementVariations tradingNoChange;
var private AnnouncementVariations activatedTrading, deactivatedTrading;
var private AnnouncementVariations pausedTime, unpausedTime, changedCountdown;
var private AnnouncementVariations tradersOpened, tradersClosed;
var private AnnouncementVariations selectedNoTrader, selectedSameTrader;
var private AnnouncementVariations selectedTrader, bootedPlayers;
var private AnnouncementVariations enabledTraders, disabledTraders;
var private AnnouncementVariations autoOpenTraders, doNotAutoOpenTraders;
protected function Finalizer()
{
FreeVariations(tradingNoChange);
FreeVariations(activatedTrading);
FreeVariations(deactivatedTrading);
FreeVariations(pausedTime);
FreeVariations(unpausedTime);
FreeVariations(changedCountdown);
FreeVariations(tradersOpened);
FreeVariations(tradersClosed);
FreeVariations(selectedNoTrader);
FreeVariations(selectedSameTrader);
FreeVariations(selectedTrader);
FreeVariations(bootedPlayers);
FreeVariations(enabledTraders);
FreeVariations(disabledTraders);
FreeVariations(autoOpenTraders);
FreeVariations(doNotAutoOpenTraders);
super.Finalizer();
}
public final function AnnounceTradingNoChange()
{
local int i;
local array<TextTemplate> templates;
if (!tradingNoChange.initialized)
{
tradingNoChange.initialized = true;
tradingNoChange.toSelfReport = _.text.MakeTemplate_S(
"There was {$TextNegative no change} in trading time status");
}
templates = MakeArray(tradingNoChange);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset();
}
MakeAnnouncement(tradingNoChange);
}
public final function AnnounceActivatedTrading()
{
local int i;
local array<TextTemplate> templates;
if (!activatedTrading.initialized)
{
activatedTrading.initialized = true;
activatedTrading.toSelfReport = _.text.MakeTemplate_S(
"Trader time {$TextPositive started}");
activatedTrading.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive started} trader time");
}
templates = MakeArray(activatedTrading);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset();
}
MakeAnnouncement(activatedTrading);
}
public final function AnnounceDeactivatedTrading()
{
local int i;
local array<TextTemplate> templates;
if (!deactivatedTrading.initialized)
{
deactivatedTrading.initialized = true;
deactivatedTrading.toSelfReport = _.text.MakeTemplate_S(
"Trader time {$TextNegative ended}");
deactivatedTrading.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNegative ended} trader time");
}
templates = MakeArray(pausedTime);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset();
}
MakeAnnouncement(deactivatedTrading);
}
public final function AnnouncePausedTime()
{
local int i;
local array<TextTemplate> templates;
if (!pausedTime.initialized)
{
pausedTime.initialized = true;
pausedTime.toSelfReport = _.text.MakeTemplate_S(
"Trader time {$TextNeutral paused}");
pausedTime.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNeutral paused} trader time");
}
templates = MakeArray(pausedTime);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset();
}
MakeAnnouncement(pausedTime);
}
public final function AnnounceUnpausedTime()
{
local int i;
local array<TextTemplate> templates;
if (!unpausedTime.initialized)
{
unpausedTime.initialized = true;
unpausedTime.toSelfReport = _.text.MakeTemplate_S(
"Trader time {$TextNeutral unpaused}");
unpausedTime.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNeutral unpaused} trader time");
}
templates = MakeArray(unpausedTime);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset();
}
MakeAnnouncement(unpausedTime);
}
public final function AnnounceChangedCountdown(int userTimerValue)
{
local int i;
local array<TextTemplate> templates;
if (!changedCountdown.initialized)
{
changedCountdown.initialized = true;
changedCountdown.toSelfReport = _.text.MakeTemplate_S(
"Trader time {$TextNeutral changed} to %1");
changedCountdown.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNeutral changed} trader time to %1");
}
templates = MakeArray(changedCountdown);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().ArgInt(userTimerValue);
}
MakeAnnouncement(changedCountdown);
}
public final function AnnounceTradersOpened(ListBuilder traderList)
{
local int i;
local MutableText traderListAsText;
local array<TextTemplate> templates;
if (!tradersOpened.initialized)
{
tradersOpened.initialized = true;
tradersOpened.toSelfReport = _.text.MakeTemplate_S(
"{$TextPositive Opened} following traders: %1");
tradersOpened.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive opened} following traders: %1");
}
if (traderList.IsEmpty()) {
return;
}
traderListAsText = traderList.GetMutable();
templates = MakeArray(tradersOpened);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(traderListAsText);
}
MakeAnnouncement(tradersOpened);
_.memory.Free(traderListAsText);
}
public final function AnnounceTradersClosed(ListBuilder traderList)
{
local int i;
local MutableText traderListAsText;
local array<TextTemplate> templates;
if (!tradersClosed.initialized)
{
tradersClosed.initialized = true;
tradersClosed.toSelfReport = _.text.MakeTemplate_S(
"{$TextNegative Closed} following traders: %1");
tradersClosed.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNegative closed} following traders: %1");
}
if (traderList.IsEmpty()) {
return;
}
traderListAsText = traderList.GetMutable();
templates = MakeArray(tradersClosed);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(traderListAsText);
}
MakeAnnouncement(tradersClosed);
_.memory.Free(traderListAsText);
}
public final function AnnounceSelectedNoTrader()
{
local int i;
local array<TextTemplate> templates;
if (!selectedNoTrader.initialized)
{
selectedNoTrader.initialized = true;
selectedNoTrader.toSelfReport = _.text.MakeTemplate_S(
"All traders were {$TextNegative deselected}");
selectedNoTrader.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNegative deselected} all traders");
}
templates = MakeArray(selectedNoTrader);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset();
}
MakeAnnouncement(selectedNoTrader);
}
public final function AnnounceSelectedSameTrader()
{
local int i;
local array<TextTemplate> templates;
if (!selectedSameTrader.initialized)
{
selectedSameTrader.initialized = true;
selectedSameTrader.toSelfReport = _.text.MakeTemplate_S(
"{$TestNeutral No changes} made as a result of"
@ "{$TextEmphasis select} command");
}
templates = MakeArray(selectedSameTrader);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset();
}
MakeAnnouncement(selectedSameTrader);
}
public final function AnnounceSelectedTrader(ETrader trader)
{
local int i;
local Text traderName;
local array<TextTemplate> templates;
if (!selectedTrader.initialized)
{
selectedTrader.initialized = true;
selectedTrader.toSelfReport = _.text.MakeTemplate_S(
"{$TextNeutral Selected} trader \"%1\"");
selectedTrader.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNeutral selected} trader \"%1\"");
}
traderName = trader.GetName();
templates = MakeArray(selectedTrader);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(traderName);
}
MakeAnnouncement(selectedTrader);
_.memory.Free(traderName);
}
public final function AnnounceBootedPlayers(ListBuilder traderList)
{
local int i;
local MutableText traderListAsText;
local array<TextTemplate> templates;
if (!bootedPlayers.initialized)
{
bootedPlayers.initialized = true;
bootedPlayers.toSelfReport = _.text.MakeTemplate_S(
"{$TextNegative Booted} players from following traders: %1");
bootedPlayers.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNegative booted} players from following"
@ "traders: %1");
}
if (traderList.IsEmpty()) {
return;
}
traderListAsText = traderList.GetMutable();
templates = MakeArray(bootedPlayers);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(traderListAsText);
}
MakeAnnouncement(bootedPlayers);
_.memory.Free(traderListAsText);
}
public final function AnnounceEnabledTraders(ListBuilder traderList)
{
local int i;
local MutableText traderListAsText;
local array<TextTemplate> templates;
if (!enabledTraders.initialized)
{
enabledTraders.initialized = true;
enabledTraders.toSelfReport = _.text.MakeTemplate_S(
"{$TextPositive Enabled} following traders: %1");
enabledTraders.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextPositive enabled} following traders: %1");
}
if (traderList.IsEmpty()) {
return;
}
traderListAsText = traderList.GetMutable();
templates = MakeArray(enabledTraders);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(traderListAsText);
}
MakeAnnouncement(enabledTraders);
_.memory.Free(traderListAsText);
}
public final function AnnounceDisabledTraders(ListBuilder traderList)
{
local int i;
local MutableText traderListAsText;
local array<TextTemplate> templates;
if (!disabledTraders.initialized)
{
disabledTraders.initialized = true;
disabledTraders.toSelfReport = _.text.MakeTemplate_S(
"{$TextNegative Disabled} following traders: %1");
disabledTraders.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% {$TextNegative disabled} following traders: %1");
}
if (traderList.IsEmpty()) {
return;
}
traderListAsText = traderList.GetMutable();
templates = MakeArray(disabledTraders);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(traderListAsText);
}
MakeAnnouncement(disabledTraders);
_.memory.Free(traderListAsText);
}
public final function AnnounceAutoOpenTraders(ListBuilder traderList)
{
local int i;
local MutableText traderListAsText;
local array<TextTemplate> templates;
if (!autoOpenTraders.initialized)
{
autoOpenTraders.initialized = true;
autoOpenTraders.toSelfReport = _.text.MakeTemplate_S(
"Following traders will be {$TextPositive auto-opened}: %1");
autoOpenTraders.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% made following traders {$TextPositive automatically"
@ "openable}: %1");
}
if (traderList.IsEmpty()) {
return;
}
traderListAsText = traderList.GetMutable();
templates = MakeArray(autoOpenTraders);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(traderListAsText);
}
MakeAnnouncement(autoOpenTraders);
_.memory.Free(traderListAsText);
}
public final function AnnounceDoNotAutoOpenTraders(ListBuilder traderList)
{
local int i;
local MutableText traderListAsText;
local array<TextTemplate> templates;
if (!doNotAutoOpenTraders.initialized)
{
doNotAutoOpenTraders.initialized = true;
doNotAutoOpenTraders.toSelfReport = _.text.MakeTemplate_S(
"Following traders will {$TextNegative no longer} be auto-opened:"
@ "%1");
doNotAutoOpenTraders.toSelfPublic = _.text.MakeTemplate_S(
"%%instigator%% made following traders {$TextNegative no longer}"
@ "automatically openable: %1");
}
if (traderList.IsEmpty()) {
return;
}
traderListAsText = traderList.GetMutable();
templates = MakeArray(doNotAutoOpenTraders);
for (i = 0; i < templates.length; i += 1) {
templates[i].Reset().Arg(traderListAsText);
}
MakeAnnouncement(doNotAutoOpenTraders);
_.memory.Free(traderListAsText);
}
defaultproperties
{
}

View file

@ -0,0 +1,129 @@
/**
* Command for changing amount of money players have.
* Copyright 2022-2023 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 ACommandUserData extends Command;
var private array<EPlayer> playerQueue;
protected function BuildData(CommandDataBuilder builder)
{
builder.Group(P("admin"));
builder.Summary(P("Allows to read and write custom user data for players."));
builder.RequireTarget();
builder.ParamText(P("groupName"));
builder.OptionalParams();
builder.ParamText(P("dataName"));
builder.Describe(F("Reads user data stored for targeted user under group"
@ "{$TextEmphasis `groupName`} and name"
@ "{$TextEmphasis `dataName`}. If {$TextEmphasis `dataName`} is"
@ "omitted, the data inside the whole group will be read."));
builder.SubCommand(P("write"));
builder.ParamText(P("groupName"));
builder.ParamText(P("dataName"));
builder.ParamJSON(P("newData"));
builder.Describe(F("Stores new user data {$TextEmphasis `newData`} for"
@ "targeted user under group {$TextEmphasis `groupName`} and name"
@ "{$TextEmphasis `dataName`}."));
}
protected function ExecutedFor(
EPlayer target,
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local AcediaObject userData;
local Text groupName, dataName;
groupName = arguments.parameters.GetText(P("groupName"));
dataName = arguments.parameters.GetText(P("dataName"));
userData = arguments.parameters.GetItem(P("newData"));
if (arguments.subCommandName.IsEmpty()) {
ReadUserData(target, groupName, dataName);
}
else {
WriteUserData(target, groupName, dataName, userData);
}
_.memory.Free(dataName);
_.memory.Free(groupName);
}
private final function ReadUserData(
EPlayer targetPlayer,
BaseText groupName,
BaseText dataName)
{
local User identity;
local AcediaObject rawData;
local MutableText dataAsJSON;
local Text targetPlayerName;
identity = targetPlayer.GetIdentity();
if (identity == none) {
return;
}
targetPlayerName = targetPlayer.GetName();
rawData = identity.GetPersistentData(groupName, dataName);
dataAsJSON = _.json.PrettyPrint(rawData);
targetPlayer.BorrowConsole()
.Write(F("User data for player "))
.Write(targetPlayerName)
.Write(P(": "))
.WriteLine(dataAsJSON);
_.memory.Free(dataAsJSON);
_.memory.Free(rawData);
_.memory.Free(targetPlayerName);
_.memory.Free(identity);
}
private final function WriteUserData(
EPlayer targetPlayer,
BaseText groupName,
BaseText dataName,
AcediaObject rawData)
{
local User identity;
local Text targetPlayerName;
identity = targetPlayer.GetIdentity();
if (identity == none) {
return;
}
targetPlayerName = targetPlayer.GetName();
if (identity.SetPersistentData(groupName, dataName, rawData))
{
targetPlayer.BorrowConsole()
.Write(P("User data for player "))
.Write(targetPlayerName)
.WriteLine(F(" was {$TextPositive successfully} changed!"));
}
else
{
targetPlayer.BorrowConsole()
.Write(P("User data for player "))
.Write(targetPlayerName)
.WriteLine(F(" has {$TextPositive failed} to change!"));
}
_.memory.Free(targetPlayerName);
_.memory.Free(identity);
}
defaultproperties {
preferredName = "userdata"
}

View file

@ -0,0 +1,282 @@
/**
* Simple class to simplify announcements of changes made by Futility's
* commands. Allows to announnce different messages to self, target and others;
* changing them up when self coincides with target.
* 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 CommandAnnouncer extends AcediaObject;
/**
* # `CommandAnnouncer`
*
* Simple class to simplify announcements of changes made by Futility's
* commands. Allows to announnce different messages to self, target and others;
* changing them up when self coincides with target.
* Technically only for reporting successes, since failures are only
* reported to the instigator and are, therefore, simple. But for the sake of
* consistency, every report can be placed here.
*
* ## Usage
*
* There is supposed to be a separate announcer class for every command class,
* so there's two steps to setting one up: creating new class and putting it
* to proper use.
*
* ### Creating new `CommandAnnouncer` class
*
* Step by step the process is:
* 1. Declare a new class, extending `CommandAnnouncer`;
* 2. Declare iinside `AnnouncementVariations` field variables for every
* announcement;
* 3. Declare a `Finalizer()` and place a `FreeVariations(...);` line for
* each announcement variable inside. Don't forget to later also make
* a `super.Finalizer();` call;
* 4. For every announcement make a separate method that accepts values to
* be included in that announcement as arguments.
* 5. Inside that method check whether corresponding announcement variable
* was already initialized (`initialized` field in side
* `AnnouncementVariations` struct) and otherwise initialize all
* contained `TextTemplate`s.
* 6. Fill every template with passed arguments ("instigator" and "target"
* arguments are auto-filled later). For that you can use auxiliary
* method `MakeArray()`, e.g.
* ```unrealscript
* local int i;
* local array<TextTemplate> templates;
* // ...
* templates = MakeArray(gainedDosh);
* for (i = 0; i < templates.length; i += 1) {
* templates[i].Reset().ArgInt(doshAmount);
* }
* ```
* 7. Make a `MakeAnnouncement()`, passing it `AnnouncementVariations` that
* you've just (initialized and) filled with arguments.
*
* ### Using created `CommandAnnouncer` class
*
* Simply allocate variable of that class, make a `Setup()` call inside
* `Executed()` and `ExecutedFor()` (only necessary to do in the one you are
* using).
* Since it is way more efficient to only allocate such variable once
* (avoiding creating templates every time), it is recommended that you create
* it inside `BuildData()` call and remember that instance in a field variable.
* You then simply need to declare command's finalizer to deallocate it.
* Just don't forget to call `super.Finalizer()` as well.
*/
var private EPlayer instigator, target;
var private int instigatorLifeVersion, targetLifeVersion;
var private ConsoleWriter publicConsole;
var private int publicConsoleLifeVersion;
var private MutableText instigatorName, targetName;
struct AnnouncementVariations
{
var public bool initialized;
// `toSelf...` == command's instigator is targeting himself/herself;
// `toOther...` == command's instigator is targeting somebody else;
// `...report` == message is for a report to command's instigator;
// `...private` == message is for a report to command's target;
// `...public` == message is for a report to eveyone who isn't
// an instigator or a target.
var public TextTemplate toSelfReport;
var public TextTemplate toSelfPublic;
var public TextTemplate toOtherReport;
var public TextTemplate toOtherPrivate;
var public TextTemplate toOtherPublic;
};
protected function Finalizer()
{
instigator = none;
target = none;
publicConsole = none;
_.memory.Free(instigatorName);
_.memory.Free(targetName);
instigatorName = none;
targetName = none;
}
/**
* Prepares caller `CommandAnnouncer` to make announcements about
* `newInstigator` player affecting `newTarget` player.
*
* @param newTarget Player that is targeted by the command.
* @param newInstigator Player that is calling the command, can be
* the same as `newTarget`.
* @param newPublicConsole Console instance to announce command's action to
* other (directly unaffected) players.
*/
public final function Setup(
EPlayer newTarget,
EPlayer newInstigator,
ConsoleWriter newPublicConsole)
{
target = none;
_.memory.Free(targetName);
targetName = none;
if (newTarget != none && newTarget.IsAllocated())
{
target = newTarget;
targetLifeVersion = newTarget.GetLifeVersion();
targetName = target
.GetName()
.IntoMutableText()
.ChangeDefaultColor(_.color.LightGray);
}
instigator = none;
_.memory.Free(instigatorName);
instigatorName = none;
if (newInstigator != none && newInstigator.IsAllocated())
{
instigator = newInstigator;
instigatorLifeVersion = newInstigator.GetLifeVersion();
instigatorName = instigator
.GetName()
.IntoMutableText()
.ChangeDefaultColor(_.color.LightGray);
}
publicConsole = none;
if (newPublicConsole != none && newPublicConsole.IsAllocated())
{
publicConsole = newPublicConsole;
publicConsoleLifeVersion = newPublicConsole.GetLifeVersion();
}
}
/**
* Makes appropriate announcements from `variations` to appropriate targets.
*
* @param variations Struct with announcement templates to make.
*/
protected final function MakeAnnouncement(AnnouncementVariations variations)
{
local ConsoleWriter instigatorConsole, targetConsole;
if (!variations.initialized) return;
if (!ValidateClasses()) return;
instigatorConsole = _.console.For(instigator);
targetConsole = _.console.For(target);
if (target == none || instigator.SameAs(target))
{
// If instigator is targeting himself, then there is no need for
// a separate announcement to target
AnnounceTemplate(instigatorConsole, variations.toSelfReport);
AnnounceTemplate(publicConsole, variations.toSelfPublic);
}
else
{
// Otherwise report to three different targets
AnnounceTemplate(instigatorConsole, variations.toOtherReport);
AnnounceTemplate(targetConsole, variations.toOtherPrivate);
AnnounceTemplate(publicConsole, variations.toOtherPublic);
}
}
/**
* Auxiliary method to free all objects inside given `AnnouncementVariations`
* struct.
*
* @param variations Struct, whos contained objects methodf should free.
*/
protected final function FreeVariations(out AnnouncementVariations variations)
{
_.memory.Free(variations.toSelfReport);
_.memory.Free(variations.toSelfPublic);
_.memory.Free(variations.toOtherReport);
_.memory.Free(variations.toOtherPrivate);
_.memory.Free(variations.toOtherPublic);
variations.toSelfReport = none;
variations.toSelfPublic = none;
variations.toOtherReport = none;
variations.toOtherPrivate = none;
variations.toOtherPublic = none;
variations.initialized = false;
}
/**
* Auxiliary method to put all `TextTemplate`s inside `variations` into
* an array that can then be easily iterated over.
*/
protected final function array<TextTemplate> MakeArray(
AnnouncementVariations variations)
{
local array<TextTemplate> result;
if (variations.toSelfReport != none) {
result[result.length] = variations.toSelfReport;
}
if (variations.toSelfPublic != none) {
result[result.length] = variations.toSelfPublic;
}
if (variations.toOtherReport != none) {
result[result.length] = variations.toOtherReport;
}
if (variations.toOtherPrivate != none) {
result[result.length] = variations.toOtherPrivate;
}
if (variations.toOtherPublic != none) {
result[result.length] = variations.toOtherPublic;
}
return result;
}
private final function bool ValidateClasses()
{
if (instigator == none) return false;
if (publicConsole == none) return false;
if (instigator.GetLifeVersion() != instigatorLifeVersion) return false;
if (!instigator.IsExistent()) return false;
if (target != none)
{
if ( target.GetLifeVersion() != targetLifeVersion
|| !target.IsExistent())
{
target = none;
}
}
if (publicConsole.GetLifeVersion() != publicConsoleLifeVersion) {
return false;
}
return true;
}
private final function AnnounceTemplate(
ConsoleWriter writer,
TextTemplate template)
{
local MutableText result;
if (writer == none) return;
if (template == none) return;
if (!template.IsInitialized()) return;
template
.TextArg(P("instigator"), instigatorName)
.TextArg(P("target"), targetName);
result = template.CollectFormattedMutable();
writer.Write(result).Flush();
_.memory.Free(result);
}
defaultproperties
{
}

View file

@ -0,0 +1,158 @@
/**
* Interface class for providing static methods for working with errors,
* that can arise from parsing formatted strings.
* 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 FormattingReportTool extends AcediaObject
abstract;
var private const int TCASES, TREPORT_HEADER, TUNMATCHED_SINGLE;
var private const int TUNMATCHED_MULTIPLE, TEMPTY_TAG_SINGLE;
var private const int TEMPTY_TAG_MULTIPLE, TBAD_COLOR, TBAD_GRADIENT_POINT;
var private const int TBADSHORT_TAG;
/**
* Outputs report about formatting errors given by `errors` array.
* Reports will be made only if at least one error exists.
*
* @param writer `ConsoleWriter` to output report into.
* @param errors Formatting errors to report.
*/
public final static function Report(
ConsoleWriter writer,
array<FormattingErrorsReport.FormattedStringError> errors)
{
local int i;
local ListBuilder builder;
local MutableText itemList;
builder = ListBuilder(__().memory.Allocate(class'ListBuilder'));
writer.Write(T(default.TREPORT_HEADER));
for (i = 0; i < errors.length; i += 1)
{
if (errors[i].type == FSE_UnmatchedClosingBrackets)
{
ReportCount(
errors[i],
builder,
default.TUNMATCHED_SINGLE,
default.TUNMATCHED_MULTIPLE);
}
else if (errors[i].type == FSE_EmptyColorTag)
{
ReportCount(
errors[i],
builder,
default.TEMPTY_TAG_SINGLE,
default.TEMPTY_TAG_MULTIPLE);
}
else if (errors[i].type == FSE_BadColor) {
builder.Item(T(default.TBAD_COLOR)).Comment(errors[i].cause);
}
else if (errors[i].type == FSE_BadShortColorTag) {
builder.Item(T(default.TBADSHORT_TAG)).Comment(errors[i].cause);
}
else if (errors[i].type == FSE_BadGradientPoint)
{
builder
.Item(T(default.TBAD_GRADIENT_POINT))
.Comment(errors[i].cause);
}
}
itemList = builder.IntoMutableText();
writer.WriteLine(itemList);
__().memory.Free(itemList);
}
/**
* `FormattedStringError` is a struct that can contain a `Text` object that
* needs to be deallocated. This is convenience method that does that.
*
* @param errors Errors, whos `cause` filds must deallocated.
*/
public final static function FreeErrors(
array<FormattingErrorsReport.FormattedStringError> errors)
{
local int i;
for (i = 0; i < errors.length; i += 1) {
__().memory.Free(errors[i].cause);
}
}
private final static function ReportCause(
FormattingErrorsReport.FormattedStringError error,
ListBuilder builder,
int sentence)
{
local MutableText causeBuilder;
if (error.cause == none) {
return;
}
builder.Item(T(sentence));
causeBuilder = __().text.FromIntMutable(error.count).Append(T(default.TCASES));
builder.Comment(causeBuilder);
__().memory.Free(causeBuilder);
}
// In the methods below, do not double check the error type in the following
// errors or whether `builder != none`
private final static function ReportCount(
FormattingErrorsReport.FormattedStringError error,
ListBuilder builder,
int singleSentence,
int multipleSentence)
{
local MutableText commentBuilder;
if (error.count < 1) {
return;
}
if (error.count == 1)
{
builder.Item(T(singleSentence));
return;
}
builder.Item(T(multipleSentence));
commentBuilder = __().text.FromIntMutable(error.count).Append(T(default.TCASES));
builder.Comment(commentBuilder);
__().memory.Free(commentBuilder);
}
defaultproperties
{
TCASES = 0
stringConstants(0) = " cases"
TREPORT_HEADER = 1
stringConstants(1) = "{$TextFailure Following formatting errors were found}:"
TUNMATCHED_SINGLE = 2
stringConstants(2) = "unmatched closing curly bracket '&}'"
TUNMATCHED_MULTIPLE = 3
stringConstants(3) = "several unmatched closing curly brackets '&}'"
TEMPTY_TAG_SINGLE = 4
stringConstants(4) = "empty formatting tag"
TEMPTY_TAG_MULTIPLE = 5
stringConstants(5) = "several empty formatting tag"
TBAD_COLOR = 6
stringConstants(6) = "bad color"
TBAD_GRADIENT_POINT = 7
stringConstants(7) = "bad gradient point"
TBADSHORT_TAG = 8
stringConstants(8) = "bad short tag"
}

View file

@ -0,0 +1,36 @@
/**
* Config object for `Futility_Feature`.
* Copyright 2021-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Futility.
*
* Futility 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.
*
* Futility 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 Futility. If not, see <https://www.gnu.org/licenses/>.
*/
class Futility extends FeatureConfig
perobjectconfig
config(Futility);
protected function HashTable ToData() {
return _.collections.EmptyHashTable();
}
protected function FromData(HashTable source) {
}
protected function DefaultIt() {
}
defaultproperties {
configName = "Futility"
}

View file

@ -0,0 +1,141 @@
/**
* Config class for storing map lists.
* Copyright 2022-2023 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 FutilityChat extends FeatureConfig
perobjectconfig
config(FutilityChat);
enum ChatColorSetting {
CCS_DoNothing,
CCS_TeamColorForced,
CCS_ConfigColorForced,
CCS_TeamColorCustom,
CCS_ConfigColorCustom
};
/// How to color text chat messages?
///
/// 1. `CCS_DoNothing` - do not change color in any way;
/// 2. `CCS_TeamColorForced` - force players' team colors for
/// their messages;
/// 3. `CCS_ConfigColorForced` - force `configuredColor` value for
/// players' messages;
/// 4. `CCS_TeamColorCustom` - use players' team colors for
/// their messages by default, but allow to change color with formatted tags
/// (e.g. "Stop right there, {$crimson criminal} scum!");
/// 5. `CCS_ConfigColorCustom` - use `configuredColor` value for
/// messages by default, but allow to change color with formatted
/// tags (e.g. "Stop right there, {$crimson criminal} scum!");
///
/// Default is `CCS_DoNothing`, corresponding to vanilla behaviour.
var public config ChatColorSetting colorSetting;
/// Color that will be used if either of `CCS_ConfigColorForced` or
/// `CCS_ConfigColorCustom` options were used in `colorSetting`.
/// Default value is white: (R=255,G=255,B=255,A=255), has no vanilla
/// equivalent.
var public config Color configuredColor;
/// Allows to modify team color's value for the chat messages
/// (if either of `CCS_TeamColorForced` or `CCS_TeamColorCustom` options
/// were used) to be lighter or darker.
/// This value is clamped between -1 and 1:
///
/// * `0` means using the same color;
/// * range (0; 1) - gives you lighter colors (`1` being white);
/// * range (-1; 0) - gives you darker colors (`-1` being black);
///
/// Default value is `0.6`, has no vanilla equivalent.
var public config float teamColorModifier;
protected function HashTable ToData() {
local HashTable data;
local Text colorAsText;
data = __().collections.EmptyHashTable();
data.SetString(P("colorSetting"), StringFromColorSetting(colorSetting));
colorAsText = _.color.ToText(configuredColor);
data.SetItem(P("configuredColor"), colorAsText);
_.memory.Free(colorAsText);
data.SetFloat(P("teamColorModifier"), teamColorModifier);
return data;
}
protected function FromData(HashTable source) {
local Text storedText;
if (source != none) {
storedText = source.GetText(P("colorSetting"));
colorSetting = ColorSettingFromText(storedText);
_.memory.Free(storedText);
storedText = source.GetText(P("configuredColor"));
_.color.Parse(storedText, configuredColor);
_.memory.Free(storedText);
teamColorModifier = source.GetFloat(P("teamColorModifier"), 0.5);
}
}
private function ChatColorSetting ColorSettingFromText(BaseText permissions) {
if (permissions == none) {
return CCS_DoNothing;
}
if (permissions.EndsWith(P("TeamColorForced"), SCASE_INSENSITIVE)) {
return CCS_TeamColorForced;
}
if (permissions.EndsWith(P("ConfigColorForced"), SCASE_INSENSITIVE)) {
return CCS_ConfigColorForced;
}
if (permissions.EndsWith(P("TeamColorCustom"), SCASE_INSENSITIVE)) {
return CCS_TeamColorCustom;
}
if (permissions.EndsWith(P("ConfigColorCustom"), SCASE_INSENSITIVE)) {
return CCS_ConfigColorCustom;
}
return CCS_DoNothing;
}
private function string StringFromColorSetting(ChatColorSetting permissions) {
if (permissions == CCS_DoNothing) {
return "DoNothing";
}
if (permissions == CCS_TeamColorForced) {
return "TeamColorForced";
}
if (permissions == CCS_ConfigColorForced) {
return "ConfigColorForced";
}
if (permissions == CCS_TeamColorCustom) {
return "TeamColorCustom";
}
if (permissions == CCS_ConfigColorCustom) {
return "ConfigColorCustom";
}
return "DoNothing";
}
protected function DefaultIt() {
colorSetting = CCS_DoNothing;
configuredColor = _.color.RGB(255, 255, 255);
teamColorModifier = 0.6;
}
defaultproperties {
configName = "FutilityChat"
colorSetting = CCS_DoNothing
configuredColor = (R=255,G=255,B=255,A=255)
teamColorModifier = 0.6
}

View file

@ -0,0 +1,111 @@
/**
* Config class for storing map lists.
* Copyright 2022-2023 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 FutilityChat_Feature extends Feature
dependson(FutilityChat);
var private /*config*/ FutilityChat.ChatColorSetting colorSetting;
var private /*config*/ Color configuredColor;
var private /*config*/ float teamColorModifier;
/// Keep track of whether we connected to necessary signals, so that we can
/// connect to them or disconnect from them once setting get updated
var private bool connectedToSignal;
protected function OnEnabled() {
if (colorSetting != CCS_DoNothing) {
_.chat.OnMessage(self).connect = ReformatChatMessage;
}
}
protected function OnDisabled() {
if (colorSetting != CCS_DoNothing) {
_.chat.OnMessage(self).Disconnect();
}
}
protected function SwapConfig(FeatureConfig config)
{
local bool configRequiresSignal;
local FutilityChat newConfig;
newConfig = FutilityChat(config);
if (newConfig == none) {
return;
}
colorSetting = newConfig.colorSetting;
configuredColor = newConfig.configuredColor;
teamColorModifier = newConfig.teamColorModifier;
configRequiresSignal = (colorSetting != CCS_DoNothing);
}
private function bool ReformatChatMessage(
EPlayer sender,
MutableText message,
bool teamMessage
) {
local Text messageCopy;
local BaseText.Formatting defaultFormatting;
if (sender == none) return true;
if (message == none) return true;
if (colorSetting == CCS_DoNothing) return true;
defaultFormatting.isColored = true;
if (colorSetting == CCS_TeamColorForced || colorSetting == CCS_TeamColorCustom) {
defaultFormatting.color = ModColor(sender.GetTeamColor());
} else {
defaultFormatting.color = configuredColor;
}
if (message.StartsWith(P("|"))) {
message.Remove(0, 1);
} else if (colorSetting != CCS_TeamColorForced && colorSetting != CCS_ConfigColorForced) {
messageCopy = message.Copy();
class'FormattingStringParser'.static.ParseFormatted(messageCopy, message.Clear());
_.memory.Free(messageCopy);
}
message.ChangeDefaultFormatting(defaultFormatting);
return true;
}
private function Color ModColor(Color inputColor) {
local Color mixColor;
local Color outputColor;
local float clampedModifier;
if (Abs(teamColorModifier) < 0.001) {
return inputColor;
}
clampedModifier = FClamp(teamColorModifier, -1.0, 1.0);
if (clampedModifier > 0) {
mixColor = _.color.White;
} else {
mixColor = _.color.Black;
clampedModifier *= -1.0;
}
outputColor.R = Lerp(clampedModifier, inputColor.R, mixColor.R);
outputColor.G = Lerp(clampedModifier, inputColor.G, mixColor.G);
outputColor.B = Lerp(clampedModifier, inputColor.B, mixColor.B);
outputColor.A = inputColor.A;
return outputColor;
}
defaultproperties {
configClass = class'FutilityChat'
}

View file

@ -0,0 +1,233 @@
/**
* Config class for storing map lists.
* Copyright 2022-2023 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 FutilityNicknames extends FeatureConfig
perobjectconfig
config(FutilityNicknames);
enum NicknameSpacesAction {
NSA_DoNothing,
NSA_Trim,
NSA_Simplify
};
enum NicknameColorPermissions {
NCP_ForbidColor,
NCP_ForceTeamColor,
NCP_ForceSingleColor,
NCP_AllowAnyColor
};
/// How to treat whitespace characters inside players' nicknames:
///
/// * `NSA_DoNothing` - does nothing, leaving whitespaces as they are;
/// * `NSA_Trim` - removes leading and trailing whitespaces for nicknames;
/// * `NSA_Simplify` - removes leading and trailing whitespaces
/// for nicknames, also reducing a sequence of whitespaces inside
/// nickname to a single space, e.g. "my nick" becomes "my nick".
///
/// Default is `NSA_DoNothing`, same as on vanilla.
var public config NicknameSpacesAction spacesAction;
/// How to treat colored nicknames:
///
/// * `NCP_ForbidColor` - completely strips down any color from nicknames;
/// * `NCP_ForceTeamColor` - forces all nicknames to have player's current
/// team's color;
/// * `NCP_ForceSingleColor` - allows nickname to be painted with a single
/// color (sets nickname's color to that of the first character);
/// * `NCP_AllowAnyColor` - allows nickname to be colored in any way player
/// wants.
/// Default is `NCP_ForbidColor`, same as on vanilla.
var public config NicknameColorPermissions colorPermissions;
/// Set this to `true` if you wish to replace all whitespace characters with
/// underscores and `false` to leave them as is.
/// Default is `true`, same as on vanilla. However there is one difference:
/// Futility replaces all whitespace characters (including tabulations,
/// non-breaking spaces, etc.) instead of only ' '.
var public config bool replaceSpacesWithUnderscores;
/// Set this to `true` to remove single 'quotation marks' and `false` to
/// leave them. Default is `false`, same as on vanilla.
var public config bool removeSingleQuotationMarks;
/// Set this to `true` to remove dobule 'quotation marks' and `false` to
/// leave them. Default is `true`, same as on vanilla.
var public config bool removeDoubleQuotationMarks;
/// Should we replace empty player nicknames with a random fallback nickname
/// (defined in `fallbackNickname` array)?
var public config bool correctEmptyNicknames;
/// Max allowed nickname length. Negative values disable any length limits.
///
/// NOTE #1: `0` resets all nicknames to be empty and,
/// if `correctEmptyNicknames` is set to `true`, they will be replaced with
/// one of the fallback nicknames (see `correctEmptyNicknames` and
/// `fallbackNickname`).
///
/// NOTE #2: Because of how color swapping in vanilla Killing Floor works,
/// every color swap makes text count as being about 4 characters longer.
/// So if one uses too many colors in the nickname, for drawing functions
/// it will appear to be longer than it actually is and it *will* mess up
/// UI. Unless you are using custom HUD it is recommended to keep this value
/// at default `20` and forbid colored nicknames (by setting
/// `colorPermissions=NCP_ForbidColor`). Or to allow only one color (by setting
/// `colorPermissions=NCP_ForceSingleColor` or
/// `colorPermissions=NCP_ForceTeamColor`) and reducing `maxNicknameLength` to
/// `16` (20 characters - 4 for color swap).
/// If you want to increase the limit above that, you can also do your own
/// research by testing nicknames of various length on screen resolutions you
/// care about.
var public config int maxNicknameLength;
/// Array of fallback nicknames that will be used to replace any empty nicknames
/// if `correctEmptyNicknames` is set to `true`.
var public config array<string> fallbackNickname;
protected function HashTable ToData() {
local int i;
local ArrayList fallbackNicknamesData;
local HashTable data;
data = __().collections.EmptyHashTable();
data.SetString(P("spacesAction"), string(spacesAction));
data.SetString(P("colorPermissions"), string(colorPermissions));
data.SetBool(P("replaceSpacesWithUnderscores"), replaceSpacesWithUnderscores);
data.SetBool(P("removeSingleQuotationMarks"), removeSingleQuotationMarks);
data.SetBool(P("removeDoubleQuotationMarks"), removeDoubleQuotationMarks);
data.SetBool(P("correctEmptyNicknames"), correctEmptyNicknames);
data.SetInt(P("maxNicknameLength"), maxNicknameLength);
fallbackNicknamesData = __().collections.EmptyArrayList();
for (i = 0; i < fallbackNickname.length; i += 1) {
fallbackNicknamesData.AddItem(__().text.FromFormattedString(fallbackNickname[i]));
}
data.SetItem(P("fallbackNickname"), fallbackNicknamesData);
_.memory.Free(fallbackNicknamesData);
return data;
}
protected function FromData(HashTable source) {
local int i;
local Text nextNickName, storedText;
local ArrayList fallbackNicknamesData;
if (source == none) {
return;
}
storedText = source.GetText(P("spacesAction"));
spacesAction = SpaceActionFromText(storedText);
_.memory.Free(storedText);
storedText = source.GetText(P("colorPermissions"));
colorPermissions = ColorPermissionsFromText(storedText);
_.memory.Free(storedText);
replaceSpacesWithUnderscores = source.GetBool(P("replaceSpacesWithUnderscores"), true);
removeSingleQuotationMarks = source.GetBool(P("removeSingleQuotationMarks"), true);
removeDoubleQuotationMarks = source.GetBool(P("removeDoubleQuotationMarks"), true);
correctEmptyNicknames = source.GetBool(P("correctEmptyNicknames"), true);
maxNicknameLength = source.GetInt(P("correctEmptyNicknames"), 20);
fallbackNicknamesData = source.GetArrayList(P("fallbackNickname"));
if (fallbackNickname.length > 0) {
fallbackNickname.length = 0;
}
for (i = 0; i < fallbackNicknamesData.GetLength(); i += 1) {
nextNickName = fallbackNicknamesData.GetText(i);
if (nextNickName != none) {
fallbackNickname[i] = nextNickName.ToFormattedString();
} else {
fallbackNickname[i] = "";
}
_.memory.Free(nextNickName);
}
_.memory.Free(fallbackNicknamesData);
}
private function NicknameSpacesAction SpaceActionFromText(BaseText action) {
if (action == none) {
return NSA_DoNothing;
}
if (action.EndsWith(P("DoNothing"), SCASE_INSENSITIVE)) {
return NSA_DoNothing;
}
if (action.EndsWith(P("Trim"), SCASE_INSENSITIVE)) {
return NSA_Trim;
}
if (action.EndsWith(P("Simplify"), SCASE_INSENSITIVE)) {
return NSA_Simplify;
}
return NSA_DoNothing;
}
private function NicknameColorPermissions ColorPermissionsFromText(BaseText permissions) {
if (permissions == none) {
return NCP_ForbidColor;
}
if (permissions.EndsWith(P("ForbidColor"), SCASE_INSENSITIVE)) {
return NCP_ForbidColor;
}
if (permissions.EndsWith(P("TeamColor"), SCASE_INSENSITIVE)) {
return NCP_ForceTeamColor;
}
if (permissions.EndsWith(P("SingleColor"), SCASE_INSENSITIVE)) {
return NCP_ForceSingleColor;
}
if (permissions.EndsWith(P("AllowAnyColor"), SCASE_INSENSITIVE)) {
return NCP_AllowAnyColor;
}
return NCP_ForbidColor;
}
protected function DefaultIt() {
spacesAction = NSA_DoNothing;
colorPermissions = NCP_ForbidColor;
replaceSpacesWithUnderscores = true;
removeSingleQuotationMarks = false;
removeDoubleQuotationMarks = true;
correctEmptyNicknames = true;
maxNicknameLength = 20;
if (fallbackNickname.length > 0) {
fallbackNickname.length = 0;
}
fallbackNickname[0] = "Fresh Meat";
fallbackNickname[1] = "Rotten Meat";
fallbackNickname[2] = "Troll Meat";
fallbackNickname[3] = "Rat Meat";
fallbackNickname[4] = "Dog Meat";
fallbackNickname[5] = "Elk Meat";
fallbackNickname[6] = "Crab Meat";
fallbackNickname[7] = "Boar Meat";
fallbackNickname[8] = "Horker Meat";
fallbackNickname[9] = "Bug Meat";
}
defaultproperties {
configName = "FutilityNicknames"
spacesAction = NSA_DoNothing
colorPermissions = NCP_ForbidColor
replaceSpacesWithUnderscores = true
removeSingleQuotationMarks = false
removeDoubleQuotationMarks = true
correctEmptyNicknames = true
maxNicknameLength = 20
fallbackNickname(0) = "Fresh Meat"
fallbackNickname(1) = "Rotten Meat"
fallbackNickname(2) = "Troll Meat"
fallbackNickname(3) = "Rat Meat"
fallbackNickname(4) = "Dog Meat"
fallbackNickname(5) = "Elk Meat"
fallbackNickname(6) = "Crab Meat"
fallbackNickname(7) = "Boar Meat"
fallbackNickname(8) = "Horker Meat"
fallbackNickname(9) = "Bug Meat"
}

View file

@ -0,0 +1,267 @@
/**
* Config class for storing map lists.
* Copyright 2022-2023 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 FutilityNicknames_Feature extends Feature
dependson(FutilityNicknames);
//! This feature's functionality is rather simple, but we will still break up
//! what its various components are.
//!
//! Fallback nicknames are picked at random from
//! the `fallbackNicknames` array. This is done by copying that array into
//! `unusedNicknames` and then picking and removing its random elements each
//! time we need a fallback. Once `unusedNicknames` is empty - it is copied from
//! `fallbackNicknames` once again, letting already used nicknames to be reused.
//! `unusedNicknames` contains same references as `fallbackNicknames`,
//! so they need to be separately deallocated and should also be forgotten once
//! `fallbackNicknames` are deallocated`.
//! This is implemented inside `PickNextFallback()` method.
//!
//! Nickname changes are applied inside `CensorNickname()` method that uses
//! several auxiliary methods to perform different stages of "censoring".
//! Censoring is performed:
//! 1. On any player's name change
//! (using `OnPlayerNameChanging()` signal, connected to
//! `HandleNicknameChange()`);
//! 2. When new player logins (using `OnNewPlayer()` signal,
//! conneted to `CensorOriginalNickname()`) to enforce our own
//! handling of player's original nickname.
var private /*config*/ FutilityNicknames.NicknameSpacesAction spacesAction;
var private /*config*/ FutilityNicknames.NicknameColorPermissions colorPermissions;
var private /*config*/ bool replaceSpacesWithUnderscores;
var private /*config*/ bool removeSingleQuotationMarks;
var private /*config*/ bool removeDoubleQuotationMarks;
var private /*config*/ int maxNicknameLength;
var private /*config*/ bool correctEmptyNicknames;
var private /*config*/ array<Text> fallbackNickname;
/// Guaranteed order of applying changes (only chosen ones) is as following:
///
/// 1. Trim/simplify spaces;
/// 2. Remove single and double quotation marks;
/// 3. Enforce max limit of nickname's length;
/// 4. Replace empty nickname with fallback nickname (no further changes
/// will be applied to fallback nickname in that case);
/// 5. Enforce color limitation;
/// 6. Replace remaining whitespaces with underscores.
///
/// NOTE #1: as follows from the instruction described above, no changes will
/// ever be applied to fallback nicknames (unless player's nickname coincides
/// with one by pure accident).
///
/// NOTE #2: whitespaces inside steam nicknames are converted into underscores
/// before they are passed into the game and this is a change Futility
/// cannot currently abort. Therefore all changes relevant to whitespaces inside
/// nicknames will only be applied to in-game changes.
/// Nicknames from `fallbackNickname` that can still be picked in the current
/// rotation.
var private array<Text> unusedNicknames;
var private const int CODEPOINT_UNDERSCORE;
protected function OnEnabled() {
if (IsAnyCensoringEnabled()) {
// Do this before adding event handler to avoid censoring nicknames
// second time (censoring nickname will trigger `OnPlayerNameChanging()`
// signal)
CensorCurrentPlayersNicknames();
_.players.OnPlayerNameChanging(self).connect = HandleNicknameChange;
_.players.OnNewPlayer(self).connect = CensorOriginalNickname;
}
}
protected function OnDisabled() {
_.memory.FreeMany(fallbackNickname);
_.memory.FreeMany(unusedNicknames);
fallbackNickname.length = 0;
unusedNicknames.length = 0;
if (IsAnyCensoringEnabled()) {
_.players.OnPlayerNameChanging(self).Disconnect();
_.players.OnNewPlayer(self).Disconnect();
}
}
protected function SwapConfig(FeatureConfig config) {
local FutilityNicknames newConfig;
newConfig = FutilityNicknames(config);
if (newConfig == none) {
return;
}
replaceSpacesWithUnderscores = newConfig.replaceSpacesWithUnderscores;
removeSingleQuotationMarks = newConfig.removeSingleQuotationMarks;
removeDoubleQuotationMarks = newConfig.removeDoubleQuotationMarks;
correctEmptyNicknames = newConfig.correctEmptyNicknames;
spacesAction = newConfig.spacesAction;
colorPermissions = newConfig.colorPermissions;
maxNicknameLength = newConfig.maxNicknameLength;
SwapFallbackNicknames(newConfig);
}
private function SwapFallbackNicknames(FutilityNicknames newConfig) {
local int i;
_.memory.FreeMany(fallbackNickname);
fallbackNickname.length = 0;
for (i = 0; i < newConfig.fallbackNickname.length; i += 1) {
fallbackNickname[i] = _.text.FromFormattedString(newConfig.fallbackNickname[i]);
}
unusedNicknames = fallbackNickname;
}
private function Text PickNextFallback() {
local int pickedIndex;
local Text result;
if (fallbackNickname.length <= 0) {
// Just in case this feature is really misconfigured
return P("Fresh Meat").Copy();
}
if (unusedNicknames.length <= 0) {
unusedNicknames = fallbackNickname;
}
// Pick one nickname at random.
// `pickedIndex` will belong to [0; unusedNicknames.length - 1] segment.
pickedIndex = Rand(unusedNicknames.length);
result = unusedNicknames[pickedIndex].Copy();
unusedNicknames.Remove(pickedIndex, 1);
return result;
}
private function bool IsAnyCensoringEnabled() {
return ( replaceSpacesWithUnderscores
|| removeSingleQuotationMarks
|| removeDoubleQuotationMarks
|| correctEmptyNicknames
|| maxNicknameLength >= 0
|| colorPermissions != NCP_AllowAnyColor
|| spacesAction != NSA_DoNothing);
}
// For nickname changes mid-game.
private function HandleNicknameChange(
EPlayer affectedPlayer,
BaseText oldName,
MutableText newName
) {
CensorNickname(newName, affectedPlayer);
}
// For handling of player's original nicknames.
private function CensorOriginalNickname(EPlayer affectedPlayer) {
local Text originalNickname;
if (affectedPlayer == none) {
return;
}
originalNickname = affectedPlayer.GetOriginalName();
// This will automatically trigger `OnPlayerNameChanging()` signal and
// our `HandleNicknameChange()` handler.
affectedPlayer.SetName(originalNickname);
_.memory.Free(originalNickname);
}
// For handling nicknames of players after censoring is re-activated by
// config change.
private function CensorCurrentPlayersNicknames() {
local int i;
local Text nextNickname;
local MutableText alteredNickname;
local array<EPlayer> currentPlayers;
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
nextNickname = currentPlayers[i].GetName();
alteredNickname = nextNickname.MutableCopy();
CensorNickname(alteredNickname, currentPlayers[i]);
if (!alteredNickname.Compare(nextNickname)) {
currentPlayers[i].SetName(alteredNickname);
}
_.memory.Free(alteredNickname);
_.memory.Free(nextNickname);
}
}
private function CensorNickname(MutableText nickname, EPlayer affectedPlayer) {
local Text fallback;
local BaseText.Formatting newFormatting;
if (nickname == none) return;
if (affectedPlayer == none) return;
if (spacesAction != NSA_DoNothing) {
nickname.Simplify(spacesAction == NSA_Simplify);
}
if (removeSingleQuotationMarks) {
nickname.Replace(P("'"), P(""));
}
if (removeDoubleQuotationMarks) {
nickname.Replace(P("\""), P(""));
}
if (maxNicknameLength >= 0) {
nickname.Remove(maxNicknameLength);
}
if (correctEmptyNicknames && nickname.IsEmpty()) {
fallback = PickNextFallback();
nickname.Append(fallback);
_.memory.Free(fallback);
return;
}
if (colorPermissions != NCP_AllowAnyColor) {
if (colorPermissions == NCP_ForceSingleColor) {
newFormatting = nickname.GetCharacter(0).formatting;
} else if (colorPermissions == NCP_ForceTeamColor) {
newFormatting.isColored = true;
newFormatting.color = affectedPlayer.GetTeamColor();
}
// `colorPermissions == NCP_ForbidColor`
// `newFormatting` is colorless by default
nickname.ChangeFormatting(newFormatting);
}
if (replaceSpacesWithUnderscores) {
ReplaceSpaces(nickname);
}
}
// Asusmes `nickname != none`.
private function ReplaceSpaces(MutableText nickname) {
local int i;
local MutableText nicknameCopy;
local BaseText.Character nextCharacter, underscoreCharacter;
nicknameCopy = nickname.MutableCopy();
nickname.Clear();
underscoreCharacter = _.text.CharacterFromCodePoint(CODEPOINT_UNDERSCORE);
for (i = 0; i < nicknameCopy.GetLength(); i += 1) {
nextCharacter = nicknameCopy.GetCharacter(i);
if (_.text.IsWhitespace(nextCharacter)) {
// Replace character with underscore, leaving the formatting
underscoreCharacter.formatting = nextCharacter.formatting;
nextCharacter = underscoreCharacter;
}
nickname.AppendCharacter(nextCharacter);
}
_.memory.Free(nicknameCopy);
}
defaultproperties {
configClass = class'FutilityNicknames'
CODEPOINT_UNDERSCORE = 95 // '_'
}

View file

@ -0,0 +1,85 @@
/**
* This is the Futility feature, whose main purpose is to register commands
* from its package.
* Copyright 2021-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Futility.
*
* Futility 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.
*
* Futility 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 Futility. If not, see <https://www.gnu.org/licenses/>.
*/
class Futility_Feature extends Feature;
var private array< class<Command> > allCommandClasses;
var private LoggerAPI.Definition errNoCommandsFeature;
protected function OnEnabled()
{
local int i;
local Commands_Feature commandsFeature;
commandsFeature =
Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
if (commandsFeature == none)
{
_.logger.Auto(errNoCommandsFeature);
return;
}
for (i = 0; i < allCommandClasses.length; i += 1) {
//commandsFeature.RegisterCommand(allCommandClasses[i]);
}
_.environment.OnFeatureEnabled(self).connect = RegisterAllCommandClasses;
}
protected function OnDisabled()
{
local int i;
local Commands_Feature commandsFeature;
_.environment.OnFeatureEnabled(self).Disconnect();
commandsFeature =
Commands_Feature(class'Commands_Feature'.static.GetEnabledInstance());
if (commandsFeature == none) {
return;
}
for (i = 0; i < allCommandClasses.length; i += 1) {
//ommandsFeature.RegisterCommand(allCommandClasses[i]);
}
}
private final function RegisterAllCommandClasses(Feature enabledFeature)
{
local int i;
local Commands_Feature commandsFeature;
commandsFeature = Commands_Feature(enabledFeature);
if (commandsFeature == none) {
return;
}
for (i = 0; i < allCommandClasses.length; i += 1) {
//commandsFeature.RegisterCommand(allCommandClasses[i]);
}
}
defaultproperties
{
configClass = class'Futility'
allCommandClasses(0) = class'ACommandDosh'
allCommandClasses(1) = class'ACommandNick'
allCommandClasses(2) = class'ACommandTrader'
allCommandClasses(3) = class'ACommandDB'
allCommandClasses(4) = class'ACommandInventory'
allCommandClasses(5) = class'ACommandFeature'
allCommandClasses(6) = class'ACommandGod'
allCommandClasses(7) = class'ACommandSpawn'
allCommandClasses(8) = class'ACommandUserData'
errNoCommandsFeature = (l=LOG_Error,m="`Commands_Feature` is not detected, \"Futility\" will not be able to provide its functionality.")
}

View file

@ -0,0 +1,799 @@
/**
* Auxiliary object for working with player's inventory and making reports
* about it. Simplifies code for inventory commands themselves by
* taking care of actual item addition/removal and reporting about successes,
* failures and inventory status.
* This tool is supposed to be created for one player and provides wrapper
* methods for his usual inventory methods that take care of information
* collection about outcome of operations and then reporting on them.
* Copyright 2022-2023 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 InventoryTool extends AcediaObject;
enum InventoryReportTarget
{
IRT_Instigator,
IRT_Target,
IRT_Others
};
/**
* Every instance of this class is created for a particular player and that
* player cannot be changed. It allows:
* 1. This object allows for editing player's inventory in a way that allows
* it to produce a human-readable report about the changes.
* Call `AddItem()`, `RemoveItem()` or `RemoveAllItems()` and then call
* `ReportChanges()` to write changes made into the `ConsoleWriter`.
* 2. `ReportInventory()` summarizes player's inventory.
*/
// References to player (for whom this tool was created)...
var private EPlayer targetPlayer;
// ...and his inventory (for easy access)
var private EInventory targetInventory;
/**
* `ListBuilder`s for 6 different cases:
* ~ two of "...Verbose" and "...Failed" ones make reports about
* successes and failures of adding and removals to the instigator of
* these changes;
* ~ two other ones (`itemsAdded` and `itemsRemoved`) make reports about
* successful changes to everybody else present on the server.
* Supposed to be created via `CreateFor()` method.
*/
var public ListBuilder itemsAdded;
var public ListBuilder itemsRemoved;
var public ListBuilder itemsAddedPrivate;
var public ListBuilder itemsRemovedPrivate;
var public ListBuilder itemsAdditionFailed;
var public ListBuilder itemsRemovalFailed;
var private TextTemplate templateItemsAdded, templateItemsRemoved;
var private TextTemplate templateItemsAddedVerbose, templateItemsRemovedVerbose;
var private TextTemplate templateAdditionFailed, templateRemovalFailed;
var const int TINSTIGATOR, TTARGET, TRESOLVED_INTO, TTILDE_QUOTE;
var const int TITEM_MISSING, TITEM_NOT_REMOVABLE, TUNKNOWN, TVISIBLE;
var const int TDISPLAYING_INVENTORY, THEADER_COLON, TDOT_SPACE, TCOLON_SPACE;
var const int TCOMMA_SPACE, TSPACE, TOUT_OF, THIDDEN_ITEMS, TDOLLAR, TYOU;
var const int TTHEMSELVES, TFAULTY_INVENTORY_IMPLEMENTATION;
public static function StaticConstructor()
{
if (StaticConstructorGuard()) {
return;
}
default.templateItemsAdded = __().text.MakeTemplate_S(
"%%instigator%% {$TextPositive added} following weapons to"
@ "%%target%%: ");
default.templateItemsRemoved = __().text.MakeTemplate_S(
"%%instigator%% {$TextNegative removed} following weapons from"
@ "%%target%%: ");
default.templateItemsAddedVerbose = __().text.MakeTemplate_S(
"Weapons {$TextPositive added} to %%target%%: ");
default.templateItemsRemovedVerbose = __().text.MakeTemplate_S(
"Weapons {$TextNegative removed} from %%target%%: ");
default.templateAdditionFailed = __().text.MakeTemplate_S(
"Weapons we've {$TextFailure failed} to add to %%target%%: ");
default.templateRemovalFailed = __().text.MakeTemplate_S(
"Weapons we've {$TextFailure failed} to remove from %%target%%: " );
}
protected function Constructor()
{
itemsAdded = ListBuilder(_.memory.Allocate(class'ListBuilder'));
itemsRemoved = ListBuilder(_.memory.Allocate(class'ListBuilder'));
itemsAddedPrivate = ListBuilder(_.memory.Allocate(class'ListBuilder'));
itemsRemovedPrivate = ListBuilder(_.memory.Allocate(class'ListBuilder'));
itemsAdditionFailed = ListBuilder(_.memory.Allocate(class'ListBuilder'));
itemsRemovalFailed = ListBuilder(_.memory.Allocate(class'ListBuilder'));
}
protected function Finalizer()
{
// Deallocate report tools
_.memory.Free(itemsAdded);
_.memory.Free(itemsRemoved);
_.memory.Free(itemsAddedPrivate);
_.memory.Free(itemsRemovedPrivate);
_.memory.Free(itemsAdditionFailed);
_.memory.Free(itemsRemovalFailed);
itemsAdded = none;
itemsRemoved = none;
itemsAddedPrivate = none;
itemsRemovedPrivate = none;
itemsAdditionFailed = none;
itemsRemovalFailed = none;
// Deallocate player references
_.memory.Free(targetPlayer);
_.memory.Free(targetInventory);
targetPlayer = none;
targetInventory = none;
}
/**
* Creates new `InventoryTool` instance for a given player `target`.
*
* @param target Player for which to create new `InventoryTool`.
* @return `InventoryTool` created for the given player - not a copy of any
* preexisting instance. `none` iff `target == none` or refers to
* a non-existent player.
*/
public static final function InventoryTool CreateFor(EPlayer target)
{
local InventoryTool newInventoryTool;
if (target == none) return none;
if (!target.IsExistent()) return none;
newInventoryTool =
InventoryTool(__().memory.Allocate(class'InventoryTool'));
if (target != none) {
newInventoryTool.targetPlayer = target;
target.NewRef();
}
newInventoryTool.targetInventory = target.GetInventory();
return newInventoryTool;
}
// Checks whether reference to the `EPlayer` that caller `InventoryTool` was
// created for is still valid.
private final function bool TargetPlayerIsInvalid()
{
if (targetPlayer == none) return true;
if (!targetPlayer.IsExistent()) return true;
return false;
}
/**
* Resets `InventoryTool`, forgetting about changes made with it so far.
*/
public final function Reset()
{
itemsAddedPrivate.Reset();
itemsRemovedPrivate.Reset();
itemsAdded.Reset();
itemsRemoved.Reset();
itemsAdditionFailed.Reset();
itemsRemovalFailed.Reset();
}
// Makes "`resolvedWhat` resolved into `intoWhat`" line
// In case `resolvedWhat == intoWhat` just returns copy of
// original `resolvedWhat`
private function MutableText MakeResolvedIntoLine(
BaseText resolvedWhat,
BaseText intoWhat)
{
if (resolvedWhat == none) {
return none;
}
if (_.text.IsEmpty(intoWhat) || resolvedWhat.Compare(intoWhat)) {
return resolvedWhat.MutableCopy();
}
return _.text.Empty()
.Append(T(TTILDE_QUOTE))
.Append(resolvedWhat)
.Append(T(TRESOLVED_INTO))
.Append(intoWhat)
.Append(T(TTILDE_QUOTE));
}
// Tries to fill ammo for the `item` in case it is a weapon
private function TryFillAmmo(EItem item)
{
local EWeapon itemAsWeapon;
if (item == none) {
return;
}
itemAsWeapon = EWeapon(item.As(class'EWeapon'));
if (itemAsWeapon != none)
{
itemAsWeapon.FillAmmo();
_.memory.Free(itemAsWeapon);
}
}
/**
* Adds a new item, based on user provided name `userProvidedName`.
*
* @param userProvidedName Name of the inventory, provided by the user.
* If it is started with "$", then tool tried to treat it as
* an alias first. If it either does not start with "$" or does not
* correspond to a valid alias - it is treated as a template.
* @param doForce Set to `true` if we must try to add an item
* even if it normally cannot be added.
* @param doFillAmmo Set to `true` if we must also fill ammo reserves
* of weapons we have added to the full.
*/
public function AddItem(
BaseText userProvidedName,
bool doForce,
bool doFillAmmo)
{
local EItem addedItem;
local MutableText resolvedLine;
local Text realItemName, itemTemplate, failureReason;
if (TargetPlayerIsInvalid()) return;
if (userProvidedName == none) return;
// Get template in case alias was specified
// (`itemTemplate` cannot be `none`, since `userProvidedName != none`)
if (userProvidedName.StartsWith(T(TDOLLAR))) {
itemTemplate = _.alias.ResolveWeapon(userProvidedName, true);
}
else {
itemTemplate = userProvidedName.Copy();
}
// The only way we can fail in a valid way is when API says we will
// via `CanAddTemplateExplain()`
failureReason = targetInventory
.CanAddTemplateExplain(itemTemplate, doForce);
if (failureReason != none)
{
itemsAdditionFailed.Item(userProvidedName).Comment(failureReason);
_.memory.Free(failureReason);
_.memory.Free(itemTemplate);
return;
}
// Actually try to add specified item
addedItem = targetInventory.AddTemplate(itemTemplate, doForce);
if (addedItem != none)
{
if (doFillAmmo) {
TryFillAmmo(addedItem);
}
realItemName = addedItem.GetName();
resolvedLine = MakeResolvedIntoLine(userProvidedName, itemTemplate);
itemsAdded.Item(realItemName);
itemsAddedPrivate.Item(realItemName).Comment(resolvedLine);
_.memory.Free(realItemName);
_.memory.Free(resolvedLine);
_.memory.Free(addedItem);
}
else
{
// `CanAddTemplateExplain()` told us that we should not have failed,
// so complain about bad API
itemsAdditionFailed.Item(userProvidedName)
.Comment(T(TFAULTY_INVENTORY_IMPLEMENTATION));
}
_.memory.Free(itemTemplate);
}
/**
* Removes a specified item, based on user provided name `userProvidedName`.
*
* @param userProvidedName Name of inventory, provided by the user.
* If it is started with "$", then tool tried to treat it as
* an alias first. If it either does not start with "$" or does not
* correspond to a valid alias - it is treated as a template.
* @param doKeep Set to `true` if item should be preserved
* (or, at least, attempted to be preserved) and not simply destroyed.
* @param doForce Set to `true` if we must try to remove an item
* even if it normally cannot be removed.
* @param doRemoveAll Set to `true` to remove all instances of given
* template and `false` to only remove one.
*/
public function RemoveItem(
BaseText userProvidedName,
bool doKeep,
bool doForce,
bool doRemoveAll)
{
local bool itemWasMissing;
local Text realItemName, itemTemplate;
local MutableText resolvedLine;
local EItem storedItem;
if (TargetPlayerIsInvalid()) return;
if (userProvidedName == none) return;
// Get template in case alias was specified
// (`itemTemplate` cannot be `none`, since `userProvidedName != none`)
if (userProvidedName.StartsWith(T(TDOLLAR))) {
itemTemplate = _.alias.ResolveWeapon(userProvidedName, true);
}
else {
itemTemplate = userProvidedName.Copy();
}
// Check if item is even in the inventory
storedItem = targetInventory.GetTemplateItem(itemTemplate);
if (storedItem == none)
{
// If not, we still need to attempt to remove it, as it can be
// "merged" into another item
itemWasMissing = true;
realItemName = P("").Copy();
}
else {
// Need to remember the name before removing the item
realItemName = storedItem.GetName();
}
if (targetInventory
.RemoveTemplate(itemTemplate, doKeep, doForce, doRemoveAll))
{
resolvedLine = MakeResolvedIntoLine(userProvidedName, itemTemplate);
itemsRemoved.Item(realItemName);
itemsRemovedPrivate.Item(realItemName).Comment(resolvedLine);
_.memory.Free(resolvedLine);
}
// Try to guess why operation failed
// (no special explanation method is present in the API)
else if (itemWasMissing) { // likely because it was missing
itemsRemovalFailed.Item(userProvidedName).Comment(T(TITEM_MISSING));
}
else if (!doForce && !storedItem.IsRemovable()) // simply was not removable
{
itemsRemovalFailed.Item(userProvidedName)
.Comment(T(TITEM_NOT_REMOVABLE));
}
else { // no idea about the reason
itemsRemovalFailed.Item(userProvidedName).Comment(T(TUNKNOWN));
}
_.memory.Free(storedItem);
_.memory.Free(realItemName);
_.memory.Free(itemTemplate);
}
// Auxiliary method for detecting and reporting about removed items by
/// comparing lists of `EItem` interfeaces created beofer and after removal
private function DetectAndReportRemovedItems(
out array<EItem> itemsAfterRemoval,
array<EItem> itemsBeforeRemoval,
array<BaseText> itemNames,
bool doForce)
{
local int i, j;
local bool itemWasRemoved;
for (i = 0; i < itemsBeforeRemoval.length; i += 1)
{
itemWasRemoved = true;
// If item was not destroyed - double check whether it got removed
if (itemsBeforeRemoval[i].IsExistent())
{
for (j = 0; j < itemsAfterRemoval.length; j += 1)
{
if (itemsBeforeRemoval[i].SameAs(itemsAfterRemoval[j]))
{
_.memory.Free(itemsAfterRemoval[j]);
itemsAfterRemoval.Remove(j, 1);
itemWasRemoved = false;
break;
}
}
}
if (itemWasRemoved)
{
itemsRemoved.Item(itemNames[i]);
itemsRemovedPrivate.Item(itemNames[i]);
}
else if (doForce || itemsBeforeRemoval[i].IsRemovable()) {
itemsRemovalFailed.Item(itemNames[i]).Comment(T(TUNKNOWN));
}
}
}
/**
* Removes all items from the player's inventory.
*
* @param doKeep Set to `true` if items should be preserved
* (or, at least, attempted to be preserved) and not simply destroyed.
* @param doForce Set to `true` if we must try to remove an item
* even if it normally cannot be removed.
* @param includeHidden Set to `true` if "hidden" items should also be
* targeted by this method. These are items player cannot directly see in
* their inventory, usually serving some sort of technical role.
*/
public function RemoveAllItems(bool doKeep, bool doForce, bool includeHidden)
{
local int i;
local array<Text> itemNames;
local array<EItem> itemsBeforeRemoval, itemsAfterRemoval;
if (TargetPlayerIsInvalid()) {
return;
}
// Remove all items!
// Remember what items we have had before to output them and
// what items we have after removal to detect what we have actually
// removed.
// This is necessary, since (to an extent depending on flags)
// some items might not be removable.
if (includeHidden) {
itemsBeforeRemoval = targetInventory.GetAllItems();
}
else {
itemsBeforeRemoval = targetInventory.GetTagItems(T(TVISIBLE));
}
for (i = 0; i < itemsBeforeRemoval.length; i += 1) {
itemNames[i] = itemsBeforeRemoval[i].GetName();
}
targetInventory.RemoveAll(doKeep, doForce, includeHidden);
itemsAfterRemoval = targetInventory.GetAllItems();
// Figure out what items are actually gone and report about them
DetectAndReportRemovedItems( itemsAfterRemoval,
itemsBeforeRemoval, itemNames,
doForce);
_.memory.FreeMany(itemNames);
_.memory.FreeMany(itemsBeforeRemoval);
_.memory.FreeMany(itemsAfterRemoval);
}
/**
* Removes all equipped items from the player's inventory.
*
* @param doKeep Set to `true` if items should be preserved
* (or, at least, attempted to be preserved) and not simply destroyed.
* @param doForce Set to `true` if we must try to remove an item
* even if it normally cannot be removed.
* @param includeHidden Set to `true` if "hidden" items should also be
* targeted by this method. These are items player cannot directly see in
* their inventory, usually serving some sort of technical role.
*/
public function RemoveEquippedItems(
bool doKeep,
bool doForce,
bool includeHidden)
{
local int i;
local EItem nextItem;
local Text nextItemName;
local array<EItem> equippedItems;
if (TargetPlayerIsInvalid()) {
return;
}
equippedItems = targetInventory.GetEquippedItems();
for (i = 0; i < equippedItems.length; i += 1)
{
nextItem = equippedItems[i];
if (!nextItem.IsExistent()) continue;
if (!includeHidden && !nextItem.HasTag(T(TVISIBLE))) continue;
nextItemName = nextItem.GetName();
// Try to guess the reason we cannot remove the item
if (!doForce && !nextItem.IsRemovable())
{
itemsRemovalFailed
.Item(nextItemName)
.Comment(T(TITEM_NOT_REMOVABLE));
}
else if (!targetInventory.Remove(nextItem, doKeep, doForce))
{
itemsRemovalFailed
.Item(nextItemName)
.Comment(T(TUNKNOWN));
}
_.memory.Free(nextItemName);
nextItemName = none;
}
_.memory.FreeMany(equippedItems);
}
/**
* Tells `InventoryTool` which player is responsible for the changes it is
* reporting. This information is used to choose the phrasing of the reported
* messages.
*
* @param instigator Player that supposedly requested all the changes done by
* the calller `InventoryTool`.
*/
public final function SetupReportInstigator(EPlayer instigator)
{
local MutableText instigatorName, targetName;
if (TargetPlayerIsInvalid()) return;
if (instigator == none) return;
instigatorName = ColorNickname(instigator.GetName());
if (!targetPlayer.SameAs(instigator)) {
targetName = ColorNickname(targetPlayer.GetName());
}
else {
targetName = T(TYOU).MutableCopy();
}
// For instigator
default.templateItemsAdded.Reset().TextArg(T(TINSTIGATOR), instigatorName);
default.templateItemsRemoved
.Reset()
.TextArg(T(TINSTIGATOR), instigatorName);
// For everybody else
default.templateAdditionFailed.Reset().TextArg(T(TTARGET), targetName);
default.templateRemovalFailed.Reset().TextArg(T(TTARGET), targetName);
default.templateItemsAddedVerbose.Reset().TextArg(T(TTARGET), targetName);
default.templateItemsRemovedVerbose.Reset().TextArg(T(TTARGET), targetName);
_.memory.Free(instigatorName);
_.memory.Free(targetName);
}
private final function MutableText ColorNickname(/* take */ BaseText nickname)
{
if (nickname == none) {
return none;
}
return nickname
.IntoMutableText()
.ChangeDefaultColor(_.color.LightGray);
}
/**
* Reports changes made to the player's inventory so far.
*
* Ability to provide this reports is pretty much the main reason for
* using `InventoryTool`
* @param blamedPlayer Player that should be listed as the one who caused
* the changes.
* @param writer `ConsoleWriter` that will be used to output report.
* Method does nothing if given `writer` is `none`.
* @param reportTarget For who is this report meant to? For general public
* and target only actually occured changes are reported (with different
* phrasing), but for instigator changes that tool failed to do will also
* be reported.
*/
public final function ReportChanges(
EPlayer instigator,
ConsoleWriter writer,
InventoryReportTarget reportTarget)
{
if (TargetPlayerIsInvalid()) {
return;
}
if (reportTarget != IRT_Instigator)
{
SwapTargetNameInTemplates(instigator, reportTarget);
ReportWeaponList(writer, default.templateItemsRemoved, itemsRemoved);
ReportWeaponList(writer, default.templateItemsAdded, itemsAdded);
return;
}
ReportWeaponList(
writer,
default.templateItemsRemovedVerbose,
itemsRemovedPrivate);
ReportWeaponList(
writer,
default.templateRemovalFailed,
itemsRemovalFailed);
ReportWeaponList(
writer,
default.templateItemsAddedVerbose,
itemsAddedPrivate);
ReportWeaponList(
writer,
default.templateAdditionFailed,
itemsAdditionFailed);
}
private final function SwapTargetNameInTemplates(
EPlayer instigator,
InventoryReportTarget reportTarget)
{
local MutableText targetName;
if (TargetPlayerIsInvalid()) {
return;
}
if (!targetPlayer.SameAs(instigator)) {
targetName = ColorNickname(targetPlayer.GetName());
}
else if (reportTarget == IRT_Target) {
targetName = T(TYOU).MutableCopy();
}
else {
targetName = T(TTHEMSELVES).MutableCopy();
}
default.templateItemsAdded.TextArg(T(TTARGET), targetName);
default.templateItemsRemoved.TextArg(T(TTARGET), targetName);
_.memory.Free(targetName);
}
private final function ReportWeaponList(
ConsoleWriter writer,
TextTemplate header,
ListBuilder builder)
{
local MutableText output;
if (writer == none) return;
if (builder == none) return;
if (builder.IsEmpty()) return;
if (header != none)
{
output = header.CollectFormattedMutable();
writer.Write(output);
_.memory.Free(output);
output = none;
}
output = builder.GetMutable();
writer.WriteLine(output);
_.memory.Free(output);
}
// TODO: Use `ListBuilder` for the below method?
/**
* Command that outputs summary of the player's inventory.
*
* @param writer `ConsoleWriter` into which to output information.
* Method does nothing if given `writer` is `none`.
* @param includeHidden Set to `true` if "hidden" items should also be
* targeted by this method. These are items player cannot directly see in
* their inventory, usually serving some sort of technical role.
*/
public final function ReportInventory(ConsoleWriter writer, bool includeHidden)
{
local int i;
local int lineCounter;
local array<EItem> availableItems;
local Text playerName;
if (writer == none) return;
if (TargetPlayerIsInvalid()) return;
playerName = targetPlayer.GetName();
writer.Flush()
.Write(T(TDISPLAYING_INVENTORY))
.UseColorOnce(_.color.White).Write(playerName)
.Write(T(THEADER_COLON)).Flush();
lineCounter = 1;
availableItems = targetInventory.GetAllItems();
// First show visible items
for (i = 0; i < availableItems.length; i += 1)
{
if (availableItems[i].HasTag(T(TVISIBLE)))
{
AppendItemInfo(writer, availableItems[i], lineCounter);
lineCounter += 1;
}
}
// Once more pass for non-visible items, to display them at the end
if (includeHidden)
{
writer.Write(T(THIDDEN_ITEMS)).Flush();
for (i = 0; i < availableItems.length; i += 1)
{
if (!availableItems[i].HasTag(T(TVISIBLE)))
{
AppendItemInfo(writer, availableItems[i], lineCounter);
lineCounter += 1;
}
}
}
_.memory.Free(playerName);
_.memory.FreeMany(availableItems);
}
private final function AppendItemInfo(
ConsoleWriter writer,
EItem item,
int lineNumber)
{
local Text itemName;
local Text lineNumberAsText;
local EWeapon itemAsWeapon;
local Mutabletext allAmmoInfo;
if (writer == none) return;
if (item == none) return;
itemName = item.GetName();
lineNumberAsText = _.text.FromInt(lineNumber);
writer.Write(lineNumberAsText)
.Write(T(TDOT_SPACE))
.UseColorOnce(_.color.TextEmphasis).Write(itemName);
// Try to display additional ammo info if this is a weapon
itemAsWeapon = EWeapon(item.As(class'EWeapon'));
if (itemAsWeapon != none)
{
allAmmoInfo = DisplayAllAmmoInfo(itemAsWeapon);
if (allAmmoInfo != none) {
writer.Write(T(TCOLON_SPACE)).Write(allAmmoInfo);
}
_.memory.Free(itemAsWeapon);
_.memory.Free(allAmmoInfo);
}
writer.Flush();
_.memory.Free(itemName);
_.memory.Free(lineNumberAsText);
}
private final function MutableText DisplayAllAmmoInfo(EWeapon weapon)
{
local int i;
local array<EAmmo> allAmmo;
local MutableText builder;
allAmmo = weapon.GetAvailableAmmo();
if (allAmmo.length == 0) {
return none;
}
builder = _.text.Empty();
for (i = 0; i < allAmmo.length; i += 1)
{
if (i > 0) {
builder.Append(T(TCOMMA_SPACE));
}
AppendAmmoInstanceInfo(builder, allAmmo[i]);
}
_.memory.FreeMany(allAmmo);
return builder;
}
private final function AppendAmmoInstanceInfo(MutableText builder, EAmmo ammo)
{
local Text ammoName;
if (ammo == none) {
return;
}
ammoName = ammo.GetName();
builder.AppendString( string(ammo.GetTotalAmount()),
_.text.FormattingFromColor(_.color.TypeNumber))
.Append(T(TSPACE)).Append(ammoName).Append(T(TOUT_OF))
.AppendString( string(ammo.GetMaxTotalAmount()),
_.text.FormattingFromColor(_.color.TypeNumber));
_.memory.Free(ammoName);
}
defaultproperties
{
TINSTIGATOR = 0
stringConstants(0) = "instigator"
TTARGET = 1
stringConstants(1) = "target"
TRESOLVED_INTO = 2
stringConstants(2) = "` resolved into `"
TTILDE_QUOTE = 3
stringConstants(3) = "`"
TFAULTY_INVENTORY_IMPLEMENTATION = 4
stringConstants(4) = "faulty inventory implementation"
TITEM_MISSING = 5
stringConstants(5) = "item missing"
TITEM_NOT_REMOVABLE = 6
stringConstants(6) = "item not removable"
TUNKNOWN = 7
stringConstants(7) = "unknown"
TVISIBLE = 8
stringConstants(8) = "visible"
TDISPLAYING_INVENTORY = 9
stringConstants(9) = "{$TextHeader Displaying inventory for player }"
THEADER_COLON = 10
stringConstants(10) = "{$TextHeader :}"
TDOT_SPACE = 11
stringConstants(11) = ". "
TCOLON_SPACE = 12
stringConstants(12) = ": "
TCOMMA_SPACE = 13
stringConstants(13) = ", "
TSPACE = 14
stringConstants(14) = " "
TOUT_OF = 15
stringConstants(15) = " out of "
THIDDEN_ITEMS = 16
stringConstants(16) = "{$TextSubHeader Hidden items:}"
TDOLLAR = 17
stringConstants(17) = "$"
TYOU = 18
stringConstants(18) = "you"
TTHEMSELVES = 19
stringConstants(19) = "themselves"
}

View file

@ -0,0 +1,251 @@
/**
* Many different Futility's commands need to output lists of items
* separated by commas, each item possibly containing comments in
* the parentheses (also separated by commas). This class provides necessary
* functionality.
* 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 ListBuilder extends AcediaObject;
/**
* # `ListBuilder`
*
* Many different Futility's commands need to output lists of items
* separated by commas, each item possibly containing comments in
* the parentheses (also separated by commas). This class provides necessary
* functionality.
* Example of such list:
* "item1, item2 (comment1, comment2), item3 (just_comment), item4, item5".
*
* ## Usage
*
* 1. Use `Item()` method to add new items (they will be listed after
* list header + whitespace, separated by commas and whitespaces ", ");
* 2. Use `Comment()` method to specify comments for the item (they will
* be listed between the paranthesisasd after the corresponding item).
* Comments will be added to the last item, added via `Item()` call.
* If no items were added, specified comment will be discarded.
* 3. Use `Get()` / `GetMutable()` methods to return list built so far.
* 4. Use `Reset()` to forget all the items and comments
* (but not list header), allowing to start forming a new report.
*/
// Represents one item + all of its comments
struct FutilityListItem
{
var Text itemTitle;
var array<Text> comments;
};
// All items recorded reported thus far
var private array<FutilityListItem> collectedItems;
var const int TCAUSE, TTARGET, TCOMMA, TSPACE, TCOMMA_SPACE;
var const int TSPACE_OPEN_PARANSIS, TCLOSE_PARANSIS;
protected function Finalizer()
{
Reset();
}
/**
* Checks if caller `ListBuilder` already has any items added.
*
* @return `true` if caller `ListBuilder` doesn't have any items added and
* `false` if it has at least one.
*/
public final function bool IsEmpty()
{
return (collectedItems.length <= 0);
}
/**
* Adds new `item` to the current report.
*
* @param item Text to be included into the report as an item.
* One should avoid using commas or parantheses inside an `item`, but
* this limitation is not checked or prevented by `Item()` method.
* Does nothing if `item == none` (`Comment()` will continue adding
* comments to the previously added item).
* @return Reference to the caller `ListBuilder` to allow for method chaining.
*/
public final function ListBuilder Item(BaseText item)
{
local FutilityListItem newItem;
if (item == none) {
return self;
}
newItem.itemTitle = item.Copy();
collectedItems[collectedItems.length] = newItem;
return self;
}
/**
* Adds new `comment` to the last added `item` in the current report.
*
* @param comment Text to be included into the report as a comment to
* the last added item. One should avoid using commas or parantheses inside
* a `comment`, but this limitation is not checked or prevented by
* `Comment()` method.
* Does nothing if `comment == none` or no items were added thuis far.
* @return Reference to the caller `ListBuilder` to allow for method chaining.
*/
public final function ListBuilder Comment(BaseText comment)
{
local array<Text> itemComments;
if (comment == none) return self;
if (collectedItems.length == 0) return self;
itemComments = collectedItems[collectedItems.length - 1].comments;
itemComments[itemComments.length] = comment.Copy();
collectedItems[collectedItems.length - 1].comments = itemComments;
return self;
}
/**
* Returns list, assembled from items and their comment specified so far as
* `Text`.
*
* @see `GetMutable()`, `IntoText()`, `IntoMutableText()`
*
* @return Assembled list of specified items with specified comments.
*/
public final function Text Get()
{
local MutableText mutableResult;
mutableResult = GetMutable();
if (mutableResult != none) {
return mutableResult.IntoText();
}
return none;
}
/**
* Returns list, assembled from items and their comment specified so far as
* `MutableText`.
*
* @see `Get()`, `IntoText()`, `IntoMutableText()`
*
* @return Assembled list of specified items with specified comments.
*/
public final function MutableText GetMutable()
{
local int i, j;
local MutableText result;
local array<Text> itemComments;
if (collectedItems.length == 0) {
return _.text.Empty();
}
result = _.text.Empty();
for (i = 0; i < collectedItems.length; i += 1)
{
if (i > 0) {
result.Append(T(TCOMMA_SPACE));
}
result.Append(collectedItems[i].itemTitle);
itemComments = collectedItems[i].comments;
if (itemComments.length > 0) {
result.Append(T(TSPACE_OPEN_PARANSIS));
}
for (j = 0; j < itemComments.length; j += 1)
{
if (j > 0) {
result.Append(T(TCOMMA_SPACE));
}
result.Append(itemComments[j]);
}
if (itemComments.length > 0) {
result.Append(T(TCLOSE_PARANSIS));
}
}
return result;
}
/**
* Converts caller `Listbuilder` into list, assembled from items and their
* comment specified so far as `Text`.
* Caller `ListBuilder()` is atuomatically deallocated.
*
* @see `Get()`, `GetMutable()`, `IntoMutableText()`
*
* @return Assembled list of specified items with specified comments.
*/
public final function Text IntoText()
{
local Text result;
result = Get();
FreeSelf();
return result;
}
/**
* Converts caller `Listbuilder` into list, assembled from items and their
* comment specified so far as `MutableText`.
* Caller `ListBuilder()` is atuomatically deallocated.
*
* @see `Get()`, `GetMutable()`, `IntoText()`
*
* @return Assembled list of specified items with specified comments.
*/
public final function MutableText IntoMutableText()
{
local MutableText result;
result = GetMutable();
FreeSelf();
return result;
}
/**
* Forgets all items or comments specified for the caller `ListBuilder` so far,
* allowing to start forming a new report.
*
* @return Reference to the caller `ListBuilder` to allow for method chaining.
*/
public final function ListBuilder Reset()
{
local int i;
for (i = 0; i < collectedItems.length; i += 1)
{
_.memory.Free(collectedItems[i].itemTitle);
_.memory.FreeMany(collectedItems[i].comments);
}
collectedItems.length = 0;
return self;
}
defaultproperties
{
TCAUSE = 0
stringConstants(0) = "%%instigator%%"
TTARGET = 1
stringConstants(1) = "%%target%%"
TCOMMA = 2
stringConstants(2) = ","
TCOMMA_SPACE = 3
stringConstants(3) = ", "
TSPACE_OPEN_PARANSIS = 4
stringConstants(4) = " ("
TCLOSE_PARANSIS = 5
stringConstants(5) = ")"
}

View file

@ -0,0 +1,28 @@
/**
* Manifest is meant to describe contents of the Acedia's package.
* Copyright 2021 - 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 Manifest extends _manifest
abstract;
defaultproperties
{
features(0) = class'Futility_Feature'
features(1) = class'FutilityNicknames_Feature'
features(2) = class'FutilityChat_Feature'
}

View file

@ -0,0 +1,345 @@
/**
* 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 <https://www.gnu.org/licenses/>.
*/
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<Feature> 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<Feature> featureClass;
var HashTable pendingSaves;
};
var private array<PendingConfigs> 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<Feature> 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<Feature> 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,
AcediaObject newValue)
{
local HashTable pendingData;
local MutableJsonPointer pointer;
local PendingConfigToolResult result;
if (pathToValue == none) {
return PCTE_BadPointer;
}
pendingData = GetPendingConfigData(true);
if (pendingData == none) {
return PCTE_ConfigMissing;
}
// Set new data
pointer = _.json.MutablePointer(pathToValue);
result = SetItemByJSON(pendingData, pointer, newValue);
pointer.FreeSelf();
pendingData.FreeSelf();
return result;
}
private function PendingConfigToolResult SetItemByJSON(
HashTable data,
MutableJsonPointer 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;
}
/*EditFeatureConfig #1: true
SetItemByJSON: true
SetContainerItemByText: true
EditFeatureConfig #2: true */
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
{
}

View file

@ -0,0 +1,33 @@
<html>
<head><title>Index of /kf_sources/Futility/Classes/</title></head>
<body>
<h1>Index of /kf_sources/Futility/Classes/</h1><hr><pre><a href="../">../</a>
<a href="ACommandDB.uc">ACommandDB.uc</a> 09-Sep-2023 21:02 17533
<a href="ACommandDosh.uc">ACommandDosh.uc</a> 21-Aug-2023 20:11 3467
<a href="ACommandDosh_Announcer.uc">ACommandDosh_Announcer.uc</a> 29-Jun-2022 14:01 3465
<a href="ACommandFeature.uc">ACommandFeature.uc</a> 07-May-2025 13:39 21862
<a href="ACommandFeature_Announcer.uc">ACommandFeature_Announcer.uc</a> 21-Aug-2023 20:11 20633
<a href="ACommandGod.uc">ACommandGod.uc</a> 21-Aug-2023 20:11 6803
<a href="ACommandGod_Announcer.uc">ACommandGod_Announcer.uc</a> 05-Jul-2022 21:22 7924
<a href="ACommandInventory.uc">ACommandInventory.uc</a> 21-Aug-2023 20:11 12226
<a href="ACommandNick.uc">ACommandNick.uc</a> 21-Aug-2023 20:11 4716
<a href="ACommandNick_Announcer.uc">ACommandNick_Announcer.uc</a> 12-Aug-2022 20:51 4006
<a href="ACommandSpawn.uc">ACommandSpawn.uc</a> 19-Nov-2023 18:54 4367
<a href="ACommandSpawn_Announcer.uc">ACommandSpawn_Announcer.uc</a> 09-Jul-2022 19:27 2888
<a href="ACommandTrader.uc">ACommandTrader.uc</a> 21-Aug-2023 20:11 25036
<a href="ACommandTrader_Announcer.uc">ACommandTrader_Announcer.uc</a> 02-Jul-2022 17:48 14635
<a href="ACommandUserData.uc">ACommandUserData.uc</a> 21-Aug-2023 20:11 4440
<a href="CommandAnnouncer.uc">CommandAnnouncer.uc</a> 07-May-2025 13:40 10686
<a href="FormattingReportTool.uc">FormattingReportTool.uc</a> 07-May-2025 12:14 5618
<a href="Futility.uc">Futility.uc</a> 21-Aug-2023 20:11 1181
<a href="FutilityChat.uc">FutilityChat.uc</a> 21-Aug-2023 20:11 5168
<a href="FutilityChat_Feature.uc">FutilityChat_Feature.uc</a> 21-Aug-2023 20:11 3832
<a href="FutilityNicknames.uc">FutilityNicknames.uc</a> 21-Aug-2023 20:11 9733
<a href="FutilityNicknames_Feature.uc">FutilityNicknames_Feature.uc</a> 21-Aug-2023 20:11 10477
<a href="Futility_Feature.uc">Futility_Feature.uc</a> 21-Aug-2023 20:11 3135
<a href="InventoryTool.uc">InventoryTool.uc</a> 07-May-2025 13:40 29064
<a href="ListBuilder.uc">ListBuilder.uc</a> 30-Jun-2022 10:02 8094
<a href="Manifest.uc">Manifest.uc</a> 13-Jan-2022 20:31 1100
<a href="PendingConfigsTool.uc">PendingConfigsTool.uc</a> 21-Aug-2023 20:11 11274
</pre><hr></body>
</html>