Dec 11, 2019, 5:33 PM

I'm considering the value and feasibility of making an extension to the help code Evennia has, for viewing the code associated with a command.

This is conceptual. I'm learning Python code because it is valuable both to me as a member to the community and to my day job, seeing if this is worth pursuing.

The Theory
Help files are typically static, but the code for a command may change over time. By pulling some or all of the values in the code into a help file's output, you might make that help file more evergreen and therefore more accurate and useful.

The Problem
Best way to do this? Do we signpost entire code blocks and offer a secondary @help command, or do we selectively insert details into the helpfiles?
Will this alteration around a command's code slow it down in a meaningful way?

Reference Code
I'm going to use the Donate command from the Arx github as my learning example. The raw code below is a reference to the public Git, and only posted here for learning purposes. It helps me to point at the changes that might be made in a specific example, instead of theoretical changes.

Edit: tried to spoiler the code so it would take less space and failed. Anyone know the syntax to put code in a spoiler?

class CmdDonate(ArxCommand):
    """
    Donates money to some group

    Usage:
        +donate <group name>=<amount>
        +donate/hype <player>,<group>=<amount>
        +donate/score [<group>]

    Donates money to some group of npcs in exchange for prestige.
    +donate/score lists donation amounts. Costs 1 AP.
    """
    key = "+donate"
    locks = "cmd:all()"
    help_category = "Social"
    action_point_cost = 1

    @property
    def donations(self):
        """Queryset of donations by caller"""
        return self.caller.player.Dominion.assets.donations.all().order_by('amount')

    def func(self):
        """Execute command."""
        caller = self.caller
        try:
            if "score" in self.switches:
                return self.display_score()
            if not self.lhs:
                self.list_donations(caller)
                return
            group = self.get_donation_target()
            if not group:
                return
            try:
                val = int(self.rhs)
                if val > caller.db.currency:
                    raise CommandError("Not enough money.")
                if val <= 0:
                    raise ValueError
                if not caller.player.pay_action_points(self.action_point_cost):
                    raise CommandError("Not enough AP.")
                caller.pay_money(val)
                group.donate(val, self.caller)
            except (TypeError, ValueError):
                raise CommandError("Must give a positive number.")
        except CommandError as err:
            caller.msg(err)

    def list_donations(self, caller):
        """Lists donations to the caller"""
        msg = "{wDonations:{n\n"
        table = PrettyTable(["{wGroup{n", "{wTotal{n"])
        for donation in self.donations:
            table.add_row([str(donation.receiver), donation.amount])
        msg += str(table)
        caller.msg(msg)

    def get_donation_target(self):
        """Get donation object"""
        org, npc = self.get_org_or_npc_from_args()
        if not org and not npc:
            return
        if "hype" in self.switches:
            player = self.caller.player.search(self.lhslist[0])
            if not player:
                return
            donations = player.Dominion.assets.donations
        else:
            donations = self.caller.player.Dominion.assets.donations
        if org:
            return donations.get_or_create(organization=org)[0]
        return donations.get_or_create(npc_group=npc)[0]

    def get_org_or_npc_from_args(self):
        """Get a tuple of org, npc used for getting the donation object"""
        org, npc = None, None
        if "hype" in self.switches:
            if len(self.lhslist) < 2:
                raise CommandError("Usage: <player>,<group>=<amount>")
            name = self.lhslist[1]
        else:
            name = self.lhs
        try:
            org = Organization.objects.get(name__iexact=name)
            if org.secret and not self.caller.check_permstring("builders"):
                if not org.active_members.filter(player__player=self.caller.player):
                    org = None
                    raise Organization.DoesNotExist
        except Organization.DoesNotExist:
            try:
                npc = InfluenceCategory.objects.get(name__iexact=name)
            except InfluenceCategory.DoesNotExist:
                raise CommandError("Could not find an organization or npc group by the name %s." % name)
        return org, npc

    def display_score(self):
        """Displays score for donations"""
        if self.args:
            return self.display_score_for_group()
        return self.display_top_donor_for_each_group()

    def display_score_for_group(self):
        """Displays a list of the top 10 donors for a given group"""
        org, npc = self.get_org_or_npc_from_args()
        if org and org.secret:
            raise CommandError("Cannot display donations for secret orgs.")
        group = org or npc
        if not group:
            return
        msg = "Top donors for %s\n" % group
        table = PrettyTable(["Donor", "Amount"])
        for donation in group.donations.filter(amount__gt=0).distinct().order_by('-amount'):
            table.add_row([str(donation.giver), str(donation.amount)])
        msg += str(table)
        self.msg(msg)

    def display_top_donor_for_each_group(self):
        """Displays the highest donor for each group"""
        orgs = Organization.objects.filter(donations__isnull=False)
        if not self.caller.check_permstring("builders"):
            orgs = orgs.exclude(secret=True)
        orgs = list(orgs.distinct())
        npcs = list(InfluenceCategory.objects.filter(donations__isnull=False).distinct())
        groups = orgs + npcs
        table = PrettyTable(["Group", "Top Donor", "Donor's Total Donations"])
        top_donations = []
        for group in groups:
            donation = group.donations.filter(amount__gt=0).order_by('-amount').distinct().first()
            if donation:
                top_donations.append(donation)
        top_donations.sort(key=lambda x: x.amount, reverse=True)
        for donation in top_donations:
            table.add_row([str(donation.receiver), str(donation.giver), str(donation.amount)])
        self.msg(str(table))

Method One - Full Code Block
Acknowledging this might be possible, but not recommending it.

This would just be a full verbatim output of a class file in game, such as the CmdDonate class above.

Bulky and if they want that much detail, why not just go to the github?

Method Two - Selective Inserts
Help files are mostly static and remain functionally similar, but numerical values and examples of rolled stats are pulled from named fields in the associated class.

Advantage: Basic tweaks automatically show up, users won't be confused when a value is altered and the helpfile gets missed. Helpfiles are Evergreen, unless there is a large change.

Drawback: Changes the structure of the class. Helpfile goes at the bottom as it has to explicitly pull in values already defined. Will possibly impact command efficiency.

Using the Example: The AP cost for Donate would be automatically taken from action_point_cost and integrated into the help text.

Markup Style (Optional): To ensure users know which values are static and which are pulled from the code, code values might be given a different default color than the standard @help output. This could be a Green or White highlight, for example.