diff --git a/sources/Commands/ACommandInventory.uc b/sources/Commands/ACommandInventory.uc
new file mode 100644
index 0000000..295c023
--- /dev/null
+++ b/sources/Commands/ACommandInventory.uc
@@ -0,0 +1,220 @@
+/**
+ * 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 .
+ */
+class ACommandInventory extends Command;
+
+var protected const int TINVENTORY, TADD, TREMOVE, TITEMS, TEQUIP, TALL, TKEEP;
+var protected const int THIDDEN, TFORCE, TAMMO, TALL_WEAPONS;
+
+protected function BuildData(CommandDataBuilder builder)
+{
+ builder.Name(T(TINVENTORY))
+ .Summary(P("Manages player's inventory."))
+ .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))
+ .OptionalParams()
+ .ParamTextList(T(TITEMS))
+ .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))
+ .OptionalParams()
+ .ParamTextList(T(TITEMS))
+ .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.Option(T(TEQUIP))
+ .Describe(F("Affect items currently equipped by the targeted player."
+ @ "Releveant for a {$TextEmphasis remove} subcommand."));
+ builder.Option(T(TALL))
+ .Describe(F("This flag tells editing commands to affect all items."
+ @ "When adding items it means \"all available weapons in the game\""
+ @ "and when removing it means \"all weapons in"
+ @ "the player's inventory\"."));
+ builder.Option(T(TKEEP))
+ .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))
+ .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))
+ .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(TAMMO), P("A"))
+ .Describe(P("When adding weapons - signals that their"
+ @ "ammo / charge / whatever has to be filled after addition."));
+}
+
+protected function ExecutedFor(
+ EPlayer player,
+ CallData result,
+ EPlayer callerPlayer)
+{
+ local ConsoleWriter publicWriter;
+ local InventoryTool tool;
+ tool = class'InventoryTool'.static.CreateFor(player);
+ if (tool == none) {
+ return;
+ }
+ if (result.subCommandName.IsEmpty())
+ {
+ tool.ReportInventory( callerPlayer.BorrowConsole(),
+ result.options.HasKey(T(THIDDEN)));
+ }
+ else if (result.subCommandName.Compare(T(TADD)))
+ {
+ SubCommandAdd( tool, result.parameters.GetDynamicArray(T(TITEMS)),
+ result.options.HasKey(T(TALL)),
+ result.options.HasKey(T(TFORCE)),
+ result.options.HasKey(T(TAMMO)));
+ }
+ else if (result.subCommandName.Compare(T(TREMOVE)))
+ {
+ SubCommandRemove( tool,
+ result.parameters.GetDynamicArray(T(TITEMS)),
+ result.options.HasKey(T(TALL)),
+ result.options.HasKey(T(TFORCE)),
+ result.options.HasKey(T(TKEEP)),
+ result.options.HasKey(T(TEQUIP)),
+ result.options.HasKey(T(THIDDEN)));
+ }
+ tool.ReportChanges(callerPlayer, player.BorrowConsole(), false);
+ publicWriter = _.console.ForAll().ButPlayer(callerPlayer);
+ tool.ReportChanges(callerPlayer, publicWriter, true);
+ _.memory.Free(tool);
+ _.memory.Free(publicWriter);
+}
+
+protected function SubCommandAdd(
+ InventoryTool tool,
+ DynamicArray templateList,
+ bool flagAll,
+ bool doForce,
+ bool doFillAmmo)
+{
+ if (flagAll) {
+ AddAllItems(tool, doForce, doFillAmmo);
+ }
+ else {
+ AddGivenTemplates(tool, templateList, doForce, doFillAmmo);
+ }
+}
+
+protected function SubCommandRemove(
+ InventoryTool tool,
+ DynamicArray templateList,
+ bool flagAll,
+ bool doForce,
+ bool doKeep,
+ bool flagEquip,
+ bool flagHidden)
+{
+ if (flagAll)
+ {
+ tool.RemoveAllItems(doKeep, doForce, flagHidden);
+ return;
+ }
+ if (flagEquip) {
+ tool.RemoveEquippedItems(doKeep, doForce, flagHidden);
+ }
+ RemoveGivenTemplates(tool, templateList, doForce, doKeep);
+}
+
+protected function AddAllItems(
+ InventoryTool tool,
+ bool doForce,
+ bool doFillAmmo)
+{
+ local int i;
+ local array allTempaltes;
+ if (tool == none) {
+ return;
+ }
+ allTempaltes = _.kf.templates.GetItemList(T(TALL_WEAPONS));
+ for (i = 0; i < allTempaltes.length; i += 1) {
+ tool.AddItem(allTempaltes[i], doForce, doFillAmmo);
+ }
+ _.memory.FreeMany(allTempaltes);
+}
+
+protected function AddGivenTemplates(
+ InventoryTool tool,
+ DynamicArray templateList,
+ bool doForce,
+ bool doFillAmmo)
+{
+ local int i;
+ if (tool == none) return;
+ if (templateList == none) return;
+
+ for (i = 0; i < templateList.GetLength(); i += 1) {
+ tool.AddItem(templateList.GetText(i), doForce, doFillAmmo);
+ }
+}
+
+protected function RemoveGivenTemplates(
+ InventoryTool tool,
+ DynamicArray templateList,
+ bool doForce,
+ bool doKeep)
+{
+ local int i;
+ if (tool == none) return;
+ if (templateList == none) return;
+
+ for (i = 0; i < templateList.GetLength(); i += 1) {
+ tool.RemoveItem(templateList.GetText(i), doKeep, doForce);
+ }
+}
+
+defaultproperties
+{
+ 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"
+ TALL_WEAPONS = 10
+ stringConstants(10) = "all weapons"
+}
\ No newline at end of file
diff --git a/sources/Tools/InventoryTool.uc b/sources/Tools/InventoryTool.uc
new file mode 100644
index 0000000..9bb7076
--- /dev/null
+++ b/sources/Tools/InventoryTool.uc
@@ -0,0 +1,657 @@
+/**
+ * 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 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 InventoryTool extends AcediaObject;
+
+/**
+ * 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;
+
+/**
+ * `ReportTool`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 private ReportTool itemsAdded;
+var private ReportTool itemsRemoved;
+var private ReportTool itemsAddedPrivate;
+var private ReportTool itemsRemovedPrivate;
+var private ReportTool itemsAdditionFailed;
+var private ReportTool itemsRemovalFailed;
+
+var const int TITEMS_ADDED_MESSAGE, TITEMS_ADDED_VEROBSE_MESSAGE;
+var const int TITEMS_REMOVED_MESSAGE, TITEMS_REMOVED_VERBOSE_MESSAGE;
+var const int TITEMS_ADDITION_FAILED_MESSAGE, TITEMS_REMOVAL_FAILED_MESSAGE;
+var const int TRESOLVED_INTO, TTILDE_QUOTE, TFAULTY_INVENTORY_IMPLEMENTATION;
+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;
+
+protected function Constructor()
+{
+ itemsAdded = ReportTool(_.memory.Allocate(class'ReportTool'));
+ itemsRemoved = ReportTool(_.memory.Allocate(class'ReportTool'));
+ itemsAddedPrivate = ReportTool(_.memory.Allocate(class'ReportTool'));
+ itemsRemovedPrivate = ReportTool(_.memory.Allocate(class'ReportTool'));
+ itemsAdditionFailed = ReportTool(_.memory.Allocate(class'ReportTool'));
+ itemsRemovalFailed = ReportTool(_.memory.Allocate(class'ReportTool'));
+ itemsAdded.Initialize(T(TITEMS_ADDED_MESSAGE));
+ itemsRemoved.Initialize(T(TITEMS_REMOVED_MESSAGE));
+ itemsAddedPrivate.Initialize(T(TITEMS_ADDED_VEROBSE_MESSAGE));
+ itemsRemovedPrivate.Initialize(T(TITEMS_REMOVED_VERBOSE_MESSAGE));
+ itemsAdditionFailed.Initialize(T(TITEMS_ADDITION_FAILED_MESSAGE));
+ itemsRemovalFailed.Initialize(T(TITEMS_REMOVAL_FAILED_MESSAGE));
+}
+
+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'));
+ newInventoryTool.targetPlayer = EPlayer(target.Copy());
+ 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(
+ Text resolvedWhat,
+ Text 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(Text 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).Detail(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).Detail(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)
+ .Detail(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.
+ */
+public function RemoveItem(Text userProvidedName, bool doKeep, bool doForce)
+{
+ 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))
+ {
+ resolvedLine = MakeResolvedIntoLine(userProvidedName, itemTemplate);
+ itemsRemoved.Item(realItemName);
+ itemsRemovedPrivate.Item(realItemName).Detail(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).Detail(T(TITEM_MISSING));
+ }
+ else if (!doForce && !storedItem.IsRemovable()) // simply was not removable
+ {
+ itemsRemovalFailed.Item(userProvidedName)
+ .Detail(T(TITEM_NOT_REMOVABLE));
+ }
+ else { // no idea about the reason
+ itemsRemovalFailed.Item(userProvidedName).Detail(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 itemsAfterRemoval,
+ array itemsBeforeRemoval,
+ array 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]).Detail(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 itemNames;
+ local array 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 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)
+ .Detail(T(TITEM_NOT_REMOVABLE));
+ }
+ else if (!targetInventory.Remove(nextItem, doKeep, doForce))
+ {
+ itemsRemovalFailed
+ .Item(nextItemName)
+ .Detail(T(TUNKNOWN));
+ }
+ _.memory.Free(nextItemName);
+ nextItemName = none;
+ }
+ _.memory.FreeMany(equippedItems);
+}
+
+/**
+ * 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 publicReport Is this report meant for the public or for
+ * the player that caused the changes? Former only (briefly) lists
+ * successful changes, while latter also provides report about failed
+ * changes.
+ */
+public final function ReportChanges(
+ EPlayer blamedPlayer,
+ ConsoleWriter writer,
+ bool publicReport)
+{
+ local Text blamedName, targetName;
+ if (TargetPlayerIsInvalid()) {
+ return;
+ }
+ targetName = targetPlayer.GetName();
+ if (blamedPlayer != none) {
+ blamedName = blamedPlayer.GetName();
+ }
+ if (publicReport)
+ {
+ itemsAdded.Report(writer, blamedName, targetName);
+ itemsRemoved.Report(writer, blamedName, targetName);
+ }
+ else
+ {
+ itemsAddedPrivate.Report(writer, blamedName, targetName);
+ itemsRemovedPrivate.Report(writer, blamedName, targetName);
+ itemsAdditionFailed.Report(writer, blamedName, targetName);
+ itemsRemovalFailed.Report(writer, blamedName, targetName);
+ }
+ _.memory.Free(blamedName);
+ _.memory.Free(targetName);
+}
+
+/**
+ * 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 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 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
+{
+ TITEMS_ADDED_MESSAGE = 0
+ stringConstants(0) = "%cause% has {$TextPositive added} following weapons to %target%:"
+ TITEMS_ADDED_VEROBSE_MESSAGE = 1
+ stringConstants(1) = "Weapons {$TextPositive added} to %target%:"
+ TITEMS_REMOVED_MESSAGE = 2
+ stringConstants(2) = "%cause% has {$TextNegative removed} following weapons from %target%:"
+ TITEMS_REMOVED_VERBOSE_MESSAGE = 3
+ stringConstants(3) = "Weapons {$TextNegative removed} from %target%:"
+ TITEMS_ADDITION_FAILED_MESSAGE = 4
+ stringConstants(4) = "Weapons we've {$TextFailure failed} to add to %target%:"
+ TITEMS_REMOVAL_FAILED_MESSAGE = 5
+ stringConstants(5) = "Weapons we've {$TextFailure failed} to remove from %target%:"
+ TRESOLVED_INTO = 6
+ stringConstants(6) = "` resolved into `"
+ TTILDE_QUOTE = 7
+ stringConstants(7) = "`"
+ TFAULTY_INVENTORY_IMPLEMENTATION = 8
+ stringConstants(8) = "faulty inventory implementation"
+ TITEM_MISSING = 9
+ stringConstants(9) = "item missing"
+ TITEM_NOT_REMOVABLE = 10
+ stringConstants(10) = "item not removable"
+ TUNKNOWN = 11
+ stringConstants(11) = "unknown"
+ TVISIBLE = 12
+ stringConstants(12) = "visible"
+ TDISPLAYING_INVENTORY = 13
+ stringConstants(13) = "{$TextHeader Displaying inventory for player }"
+ THEADER_COLON = 14
+ stringConstants(14) = "{$TextHeader :}"
+ TDOT_SPACE = 15
+ stringConstants(15) = ". "
+ TCOLON_SPACE = 16
+ stringConstants(16) = ": "
+ TCOMMA_SPACE = 17
+ stringConstants(17) = ", "
+ TSPACE = 18
+ stringConstants(18) = " "
+ TOUT_OF = 19
+ stringConstants(19) = " out of "
+ THIDDEN_ITEMS = 20
+ stringConstants(20) = "{$TextSubHeader Hidden items:}"
+ TDOLLAR = 21
+ stringConstants(21) = "$"
+}
\ No newline at end of file
diff --git a/sources/Tools/ReportTool.uc b/sources/Tools/ReportTool.uc
new file mode 100644
index 0000000..c4499f3
--- /dev/null
+++ b/sources/Tools/ReportTool.uc
@@ -0,0 +1,224 @@
+/**
+ * Auxiliary object for outputting lists of values (with optional comments)
+ * for Futility's commands. Some of the commands need to report that one of
+ * the player did something to affect the other and then list the changes.
+ * This tool is made to simplify forming such reports.
+ * Produced reports have a form of ":
+ * item1 (detail), item2, item3 (detail1, detail2)".
+ * 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 ReportTool extends AcediaObject;
+
+/**
+ * How to use:
+ * 1. Specify "list header" via `Initialize()` method right after creating
+ * a new instance of `ReportTool`. It can contain "%cause%" and
+ * "%target%" substrings, that will be replaces with approprtiate
+ * parameters of `Report()` method when it is invoked;
+ * 2. Use `Item()` method to add new items (they will be listed after
+ * list header + whitespace, separated by commas and whitespaces ", ");
+ * 3. Use `Detail()` method to specify details for the item (they will be
+ * listed between the paranthesisasd after the corresponding item).
+ * Details will be added to the last item, added via `Item()` call.
+ * If no items were added, specified details will be discarded.
+ * 4. Use `Report()` method to feed the `ConsoleWriter` with report that
+ * has been assebled so far.
+ * 5. Use `Reset()` to forget all the items and details
+ * (but not list header), allowing to start forming a new report.
+ */
+
+// Header template (with possible "%cause%" and "%target%" placeholders)
+// for the lists this `ReportTool` will generate.
+// Doubles as a way to remember whether `ReportTool` was already
+// initialized (iff `headerTemplate != none`).
+var private Text headerTemplate;
+
+// Represents one item + all of its details.
+struct ReportItem
+{
+ var Text itemTitle;
+ var array details;
+};
+// All items recorded reported thus far
+var private array itemsToReport;
+
+var const int TCAUSE, TTARGET, TCOMMA, TSPACE, TSPACE_OPEN_PARANSIS;
+var const int TCLOSE_PARANSIS;
+
+protected function Finalizer()
+{
+ Reset();
+ _.memory.Free(headerTemplate);
+ headerTemplate = none;
+}
+
+/**
+ * Initialized a new `ReportTool` with appropriate template to serve as
+ * a header.
+ *
+ * Template (`template`) is allowed to contain "%cause%" and "%target%"
+ * placeholder substrings that will be replaced with corresponding names of the
+ * player that caused a change we are reporting and player affefcted by
+ * that change.
+ *
+ * @param template Template for the header of the reports made by
+ * the caller `ReportTool`.
+ * Method does nothing (initialization fails) iff `template == none`.
+ */
+public final function Initialize(Text template)
+{
+ if (template == none) {
+ return;
+ }
+ headerTemplate = template.Copy();
+}
+
+/**
+ * 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` (`Detail()` will continue adding details
+ * to the previously added item).
+ * @return Reference to the caller `ReportTool` to allow for method chaining.
+ */
+public final function ReportTool Item(Text item)
+{
+ local ReportItem newItem;
+ if (headerTemplate == none) return self;
+ if (item == none) return self;
+
+ newItem.itemTitle = item.Copy();
+ itemsToReport[itemsToReport.length] = newItem;
+ return self;
+}
+
+/**
+ * Adds new `detail` to the last added `item` in the current report.
+ *
+ * @param detail Text to be included into the report as a detail to
+ * the last added item. One should avoid using commas or parantheses inside
+ * a `detail`, but this limitation is not checked or prevented by
+ * `Detail()` method.
+ * Does nothing if `detail == none` or no items were added thuis far.
+ * @return Reference to the caller `ReportTool` to allow for method chaining.
+ */
+public final function ReportTool Detail(Text detail)
+{
+ local array detailToReport;
+ if (headerTemplate == none) return self;
+ if (detail == none) return self;
+ if (itemsToReport.length == 0) return self;
+
+ detailToReport = itemsToReport[itemsToReport.length - 1].details;
+ detailToReport[detailToReport.length] = detail.Copy();
+ itemsToReport[itemsToReport.length - 1].details = detailToReport;
+ return self;
+}
+
+/**
+ * Outputs report assembled thus far into the provided `ConsoleWriter`.
+ *
+ * @param writer `ConsoleWriter` to output report into.
+ * @param cause Player that caused the change this report is about.
+ * Their name will replace "%cause%" substring in the header template
+ * (if it is contained there).
+ * @param target Player that was affected by the change this report is about.
+ * Their name will replace "%target%" substring in the header template
+ * (if it is contained there).
+ * @return Reference to the caller `ReportTool` to allow for method chaining.
+ */
+public final function ReportTool Report(
+ ConsoleWriter writer,
+ optional Text cause,
+ optional Text target)
+{
+ local int i, j;
+ local MutableText intro;
+ local array detailToReport;
+ if (headerTemplate == none) return self;
+ if (itemsToReport.length == 0) return self;
+ if (writer == none) return self;
+
+ intro = headerTemplate.MutableCopy()
+ .Replace(T(TCAUSE), cause)
+ .Replace(T(TTARGET), target);
+ writer.Flush().Write(intro);
+ _.memory.Free(intro);
+ for (i = 0; i < itemsToReport.length; i += 1)
+ {
+ if (i > 0) {
+ writer.Write(T(TCOMMA));
+ }
+ writer.Write(T(TSPACE)).Write(itemsToReport[i].itemTitle);
+ detailToReport = itemsToReport[i].details;
+ if (detailToReport.length > 0) {
+ writer.Write(T(TSPACE_OPEN_PARANSIS));
+ }
+ for (j = 0; j < detailToReport.length; j += 1)
+ {
+ if (j > 0) {
+ writer.Write(P(", "));
+ }
+ writer.Write(detailToReport[j]);
+ }
+ if (detailToReport.length > 0) {
+ writer.Write(T(TCLOSE_PARANSIS));
+ }
+ }
+ writer.Flush();
+ return self;
+}
+
+/**
+ * Forgets all items or details specified for the caller `ReportTool` so far,
+ * allowing to start forming a new report. Does not reset template header,
+ * specified in the `Initialize()` method.
+ *
+ * @return Reference to the caller `ReportTool` to allow for method chaining.
+ */
+public final function ReportTool Reset()
+{
+ local int i;
+ for (i = 0; i < itemsToReport.length; i += 1)
+ {
+ _.memory.Free(itemsToReport[i].itemTitle);
+ _.memory.FreeMany(itemsToReport[i].details);
+ }
+ if (itemsToReport.length > 0) {
+ itemsToReport.length = 0;
+ }
+ return self;
+}
+
+defaultproperties
+{
+ TCAUSE = 0
+ stringConstants(0) = "%cause%"
+ TTARGET = 1
+ stringConstants(1) = "%target%"
+ TCOMMA = 2
+ stringConstants(2) = ","
+ TSPACE = 3
+ stringConstants(3) = " "
+ TSPACE_OPEN_PARANSIS = 4
+ stringConstants(4) = " ("
+ TCLOSE_PARANSIS = 5
+ stringConstants(5) = ")"
+}
\ No newline at end of file