MU Soapbox

    • Register
    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Muxify
    • Mustard

    Altering @help to display live values

    MU Code
    evennia
    4
    8
    648
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • Selerik
      Selerik last edited by Selerik

      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.

      1 Reply Last reply Reply Quote 0
      • Selerik
        Selerik last edited by

        Reviewed the xp.py code after realizing there was a live reference already in the 'help train' file.

        In game output looks like this and runs in the file.

        You can train 5 people per week.
        Your current cost to train another character is 0 AP.
        

        Code that generates it is after the main helpfile but before the helpfile footer.

            def get_help(self, caller, cmdset):
                if caller.char_ob:
                    caller = caller.char_ob
                trained = ", ".join(ob.key for ob in self.currently_training(caller))
                if trained:
                    trained = "You have trained %s this week. " % trained
                msg = self.__doc__ + """
        
            You can train {w%s{n people per week.
            %sYour current cost to train another character is {w%s{n AP.
            """ % (self.max_trainees(caller), trained, self.action_point_cost(caller))
                return msg
        

        Taking this a step further, it seems possible to put the entirety of the help text into def get_help, and since it pulls from code further down, it doesn't look like order is important as I thought it was.

        Yes, this is me teaching myself how this all works. Nobody else was going to do it.

        Tracked down another example of the def get_help in the overrides.py file, where it describes itself as a tool for custom helpfiles and permissions. Cool, so that is how they did custom permission locks on the theology/occult files.

        Nothing I was thinking about doing is actually new, I just need to understand how to properly insert this stuff.

        So! Back to the example I started with, the donate command. Code might look like this.

            def get_help(self, caller, cmdset):
                msg = self.__doc__ + """
        
           +donate/score lists donation amounts. Costs {w%s{n AP.
        
            """ % (action_point_cost)
                return msg
        
        

        We would remove the related string from the main helpfile and insert this def into the class CmdDonate(ArxCommand) to get a reference to the action_point_cost string baked into the helpfile.

        Hopefully I got that all right.

        1 Reply Last reply Reply Quote 0
        • G
          Groth last edited by Groth

          You can look in the Clue command for how Tehom did it for another command:
          https://github.com/Arx-Game/arxcode/blob/6c60b99a0843f4f27ad6dd102afa1bc55846d6bb/web/character/investigation.py#L1339

              def get_help(self, caller, cmdset):
                  """Custom helpfile that lists clue sharing costs"""
                  caller = caller.player_ob
                  doc = self.__doc__
                  doc += "\n\nYour cost of sharing clues is %s." % caller.clue_cost
                  return doc
          

          I think your example will get a syntax error since there's no action_point_cost defined inside that function.

          What is obvious to you may not be obvious to me and vice versa.

          Selerik 1 Reply Last reply Reply Quote 1
          • Selerik
            Selerik @Groth last edited by

            @Groth action_point_cost is defined, same as caller.clue_cost is in the clue example, but I did fail to define caller = caller.player_ob. I didn't realize it had to have that, I'll fix it.

            G 1 Reply Last reply Reply Quote 0
            • G
              Groth @Selerik last edited by Groth

              @Selerik
              It's because the way Arx is set up, it distinguishes between Account and Character and the caller for commands is the Account. There's also some confusing subclasses that I no longer remember how they're laid out because I havn't touched that code in 6 months.

              What is obvious to you may not be obvious to me and vice versa.

              1 Reply Last reply Reply Quote 1
              • Ghost
                Ghost last edited by Ghost

                Why not just store the instruction string as a parameter in the command's code and then have the help command query the code for the parameter and print it out?

                Edit. Derp. I read back. Looks like that was covered.

                Delete the Hog Pit. It'll be fun.
                I really don't understand He-Man

                1 Reply Last reply Reply Quote 1
                • Selerik
                  Selerik last edited by

                  Screwed this up with the below attempt. The internal reference needed self.action_point_cost, @Groth was right and it wasn't defined the right way. I bow to the wisdom I denied.

                  1 Reply Last reply Reply Quote 0
                  • Griatch
                    Griatch last edited by

                    @Selerik This is a bit of a thread-necro, but I just noticed this topic and wondered if you ever continued this exploration or what the conclusion was?

                    1 Reply Last reply Reply Quote 0
                    • 1 / 1
                    • First post
                      Last post