diff --git a/sources/CommandAnnouncer.uc b/sources/CommandAnnouncer.uc
new file mode 100644
index 0000000..8fceb1b
--- /dev/null
+++ b/sources/CommandAnnouncer.uc
@@ -0,0 +1,265 @@
+/**
+ * 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 .
+ */
+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 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;
+ if (newTarget != none && newTarget.IsAllocated())
+ {
+ target = newTarget;
+ targetLifeVersion = newTarget.GetLifeVersion();
+ _.memory.Free(targetName);
+ targetName = target
+ .GetName()
+ .IntoMutableText()
+ .ChangeDefaultColor(_.color.Gray);
+ }
+ instigator = none;
+ if (newInstigator != none && newInstigator.IsAllocated())
+ {
+ instigator = newInstigator;
+ instigatorLifeVersion = newInstigator.GetLifeVersion();
+ _.memory.Free(instigatorName);
+ instigatorName = instigator
+ .GetName()
+ .IntoMutableText()
+ .ChangeDefaultColor(_.color.Gray);
+ }
+ 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 (!AreClassesValid()) return;
+
+ instigatorConsole = _.console.For(instigator);
+ targetConsole = _.console.For(target);
+ if (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 MakeArray(
+ AnnouncementVariations variations)
+{
+ local array result;
+
+ result[result.length] = variations.toSelfReport;
+ result[result.length] = variations.toSelfPublic;
+ result[result.length] = variations.toOtherReport;
+ result[result.length] = variations.toOtherPrivate;
+ result[result.length] = variations.toOtherPublic;
+ return result;
+}
+
+private final function bool AreClassesValid()
+{
+ if (instigator == none) return false;
+ if (target == none) return false;
+ if (publicConsole == none) return false;
+ if (instigator.GetLifeVersion() != instigatorLifeVersion) return false;
+ if (target.GetLifeVersion() != targetLifeVersion) return false;
+ if (!instigator.IsExistent()) return false;
+ if (!target.IsExistent()) return false;
+
+ 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("intigator"), instigatorName)
+ .TextArg(P("target"), targetName);
+ result = template.CollectFormattedM();
+ writer.Say(result);
+ _.memory.Free(result);
+}
+
+defaultproperties
+{
+}
\ No newline at end of file