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