754 lines
22 KiB
Python
Executable file
754 lines
22 KiB
Python
Executable file
#!/usr/bin/env python
|
|
|
|
from random import random
|
|
from re import split, sub, MULTILINE
|
|
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from evennia.commands.default.general import CmdGet, NumberedTargetCommand
|
|
from evennia.commands.default.muxcommand import MuxCommand
|
|
from evennia.contrib.rpg.rpsystem import send_emote
|
|
from evennia.utils import iter_to_str, logger
|
|
from evennia.utils.evmore import EvMore
|
|
|
|
from commands.command import Command
|
|
from typeclasses.characters import Character
|
|
from typeclasses.tutorial import TutorBird, TutorialState
|
|
from utils.word_list import routput, paragraph, choices
|
|
|
|
|
|
def speech_effect(speech, verb, target, effects):
|
|
"""
|
|
Return speech after applying 'effects'.
|
|
|
|
Return a tuple for what a user thinks he said, and what
|
|
others actually here. If no effect, just return speech twice.
|
|
|
|
Effects:
|
|
- mute ... can't talk
|
|
- donkied ... target and others hear the value of effect
|
|
- sloshed ... slurred speech
|
|
|
|
To administer:
|
|
|
|
@set woman/donkied:effect = "Heehaw! ;; Heehaw, heehaw!"
|
|
"""
|
|
for effect in effects:
|
|
if effect:
|
|
if effect.db_key == 'donkied':
|
|
msg = choices(effect.value)
|
|
return (msg, msg, "bray")
|
|
|
|
return (speech, speech, verb)
|
|
|
|
|
|
class CmdUse(MuxCommand):
|
|
"""
|
|
Use an item.
|
|
|
|
The item, probably something in your inventory, could be something
|
|
in the local area.
|
|
|
|
Usage:
|
|
|
|
use <item> [= object]
|
|
|
|
This standard from the text adventurer tome is a catch-all command
|
|
allowing you to use or combine an object with another object.
|
|
"""
|
|
key = "use"
|
|
aliases = ["apply"]
|
|
rhs_split = ("=", " with ", " on ", " to ")
|
|
|
|
def func(self):
|
|
"""Call the 'do_use' method."""
|
|
if not self.args:
|
|
self.caller.msg("Use what?")
|
|
return
|
|
|
|
item = self.caller.search(self.lhs)
|
|
if item:
|
|
if self.rhs:
|
|
obj = self.caller.search(self.rhs)
|
|
else:
|
|
obj = None
|
|
|
|
if item.has_method('do_use'):
|
|
item.do_use(self.caller, obj)
|
|
else:
|
|
self.caller.msg(item.db.use_msg or f"You can't use {item.name}.")
|
|
|
|
|
|
class CmdPush(Command):
|
|
"""
|
|
Push an item in the area.
|
|
|
|
Usage:
|
|
|
|
push <item>
|
|
"""
|
|
key = "push"
|
|
|
|
def func(self):
|
|
"""Call the 'do_push' method."""
|
|
pusher = self.caller
|
|
to_push = self.args.strip()
|
|
|
|
if to_push == "":
|
|
pusher.msg("What do you want to push?")
|
|
return
|
|
|
|
item = pusher.search(self.args.strip())
|
|
if item:
|
|
if item.has_method('do_push'):
|
|
item.do_push(pusher)
|
|
else:
|
|
pusher.msg(item.db.push_msg or f"You can't push {item.name}.")
|
|
|
|
|
|
class CmdPull(Command):
|
|
"""
|
|
Pull on something.
|
|
|
|
Usage:
|
|
|
|
pull <item>
|
|
"""
|
|
key = "pull"
|
|
aliases = ["yank"]
|
|
|
|
def func(self):
|
|
"""Call the 'do_pull' method."""
|
|
puller = self.caller
|
|
to_pull = self.args.strip()
|
|
|
|
if to_pull == "":
|
|
puller.msg("What do you want to pull?")
|
|
return
|
|
|
|
item = puller.search(to_pull)
|
|
if item:
|
|
if item.has_method('do_pull'):
|
|
item.do_pull(puller)
|
|
else:
|
|
puller.msg(item.db.pull_msg or f"You can't pull {item.name}.")
|
|
|
|
|
|
class CmdOpen(Command):
|
|
"""
|
|
Open something.
|
|
|
|
Usage:
|
|
|
|
open <item>
|
|
"""
|
|
key = "open"
|
|
|
|
def func(self):
|
|
"""
|
|
Call an item's 'do_open' method.
|
|
|
|
Note that if the room the caller is in has a 'do_open', we
|
|
call that first to see if the room can open something (like an
|
|
exit).
|
|
"""
|
|
opener = self.caller
|
|
room = opener.location
|
|
to_open = self.args.strip()
|
|
|
|
if to_open == "":
|
|
opener.msg("What would you like to open?")
|
|
return
|
|
|
|
if room.has_method('do_open'):
|
|
if room.do_open(opener, to_open):
|
|
return
|
|
|
|
item = opener.search(to_open)
|
|
if item:
|
|
if item.has_method('do_open'):
|
|
item.do_open(opener)
|
|
else:
|
|
opener.msg(item.db.open_msg or f"You can't open {item.name}.")
|
|
|
|
|
|
class CmdClose(Command):
|
|
"""
|
|
Close something.
|
|
|
|
Usage:
|
|
|
|
close <item>
|
|
"""
|
|
key = "close"
|
|
|
|
def func(self):
|
|
"""Call the 'do_close' method.
|
|
|
|
Note that if the room the caller is in has a 'do_close', we
|
|
call that first to see if the room can close something (like
|
|
an exit).
|
|
"""
|
|
closer = self.caller
|
|
room = closer.location
|
|
to_close = self.args.strip()
|
|
|
|
if to_close == "":
|
|
closer.msg("What would you like to close?")
|
|
return
|
|
|
|
if room.has_method('do_close'):
|
|
if room.do_close(closer, to_close):
|
|
return
|
|
|
|
item = closer.search(to_close)
|
|
if item:
|
|
if item.has_method('do_close'):
|
|
item.do_close(closer)
|
|
else:
|
|
closer.msg(item.db.close_msg or f"You can't close {item.name}.")
|
|
|
|
|
|
class CmdWhisper(MuxCommand):
|
|
"""
|
|
Speak privately as your character to another.
|
|
|
|
Usage:
|
|
whisper <character> = <message>
|
|
whisper <char1>, <char2> = <message>
|
|
|
|
Talk privately to one or more characters in your current location, without
|
|
others in the room being informed.
|
|
"""
|
|
|
|
key = "whisper"
|
|
priority = 0
|
|
locks = "cmd:all()"
|
|
rhs_split = ("=")
|
|
|
|
def func(self):
|
|
"""Implement the new 'whisper' command."""
|
|
if not self.args:
|
|
self.caller.msg("What are you whispering?")
|
|
return
|
|
|
|
if not self.rhs:
|
|
self.caller.msg("Usage: whisper <character> = <message>")
|
|
return
|
|
|
|
targets = split(r" *, *", self.lhs)
|
|
chars = [self.caller.search(target) for target in targets]
|
|
for c in chars:
|
|
if not c:
|
|
return
|
|
|
|
self.caller.msg(f"Wishering to {chars}")
|
|
full_speech = f"/Me whispers to you, \"{self.rhs}\""
|
|
send_emote(self.caller, chars, full_speech, msg_type="say",
|
|
anonymous_add=None, quiet=True)
|
|
|
|
to_list = [target.get_display_name(self) for target in chars]
|
|
full_speech = f"You whisper to {iter_to_str(to_list, endsep='and')}, \"{self.rhs}\""
|
|
self.caller.msg(full_speech, from_obj=self.caller)
|
|
|
|
|
|
class CmdSay(MuxCommand):
|
|
"""
|
|
Say something to the characters in the same area.
|
|
|
|
For instance:
|
|
|
|
|gsay Good evening!|n
|
|
|
|
However, you can use an alias and simply type:
|
|
|
|
|g"Good evening!
|
|
|
|
Or:
|
|
|
|
|g'Good evening!
|
|
|
|
Creatures and objects in this game can sometimes respond to what you say.
|
|
|
|
Usage:
|
|
say phrase
|
|
say/to char1 [, char2 ...] = phrase
|
|
say[/switches] phrase
|
|
|
|
Where switches can be any of the following:
|
|
|
|
- exclaim : To replace 'says' with 'exclaims'
|
|
- yell : To replace 'says' with 'yells'
|
|
- scream : To replace 'says' with 'screams'
|
|
- ask : To replace 'says' with 'asks'
|
|
- to : Directs phrase to one or more characters
|
|
in the same area. Note others can still hear
|
|
the statement (see the 'whisper' command).
|
|
- adverb : Any adverb-like word that ends in '-ly'
|
|
is added to the say command, for instance:
|
|
|
|
say/quietly Hi there.
|
|
|
|
Shows as:
|
|
|
|
You quietly say, "Hi there."
|
|
"""
|
|
|
|
key = "say"
|
|
aliases = ["says", "speak", "shout", "yell", "exclaim", "scream", "ask",
|
|
"reply", "respond", "\"", "'"]
|
|
priority = 0
|
|
locks = "cmd:all()"
|
|
rhs_split = ("=")
|
|
arg_regex = None
|
|
|
|
def func(self):
|
|
"""Implement the new 'say' command with switches."""
|
|
speaker = self.caller
|
|
|
|
if not self.args:
|
|
speaker.msg("Say what?")
|
|
return
|
|
|
|
if 'to' in self.switches and not self.rhs:
|
|
speaker.msg(paragraph("""
|
|
When attempting to say something to one or more
|
|
characters, use the '=' character to identify what you
|
|
want to say.
|
|
For instance, |gsay/to elf = Hi there.|n """))
|
|
return
|
|
|
|
if self.rhs:
|
|
speech = self.rhs
|
|
else:
|
|
speech = self.args
|
|
|
|
# If speech is empty, stop here
|
|
if not speaker.at_pre_say(speech):
|
|
return
|
|
|
|
adverb = ''
|
|
for switch in self.switches:
|
|
if switch.endswith('ly'):
|
|
adverb = switch + ' '
|
|
|
|
if 'scream' in self.switches or self.cmdstring == 'scream':
|
|
verb = 'scream'
|
|
elif 'shout' in self.switches or self.cmdstring == 'shout':
|
|
verb = 'shout'
|
|
elif 'respond' in self.switches or self.cmdstring == 'respond':
|
|
verb = 'respond'
|
|
elif 'reply' in self.switches or self.cmdstring == 'reply':
|
|
verb = 'reply'
|
|
elif 'yell' in self.switches or self.cmdstring == 'yell':
|
|
verb = 'yell'
|
|
elif 'exclaim' in self.switches or self.cmdstring == 'exclaim' or speech.endswith('!'):
|
|
verb = 'exclaim'
|
|
elif 'ask' in self.switches or self.cmdstring == 'ask' or speech.endswith('?'):
|
|
verb = 'ask'
|
|
else:
|
|
verb = "say"
|
|
|
|
for_me, for_others, verb = \
|
|
speech_effect(speech, verb,
|
|
speaker,
|
|
speaker.attributes.get(category="effect",
|
|
return_obj=True,
|
|
return_list=True))
|
|
|
|
to_who = to_whom = ''
|
|
|
|
if 'to' not in self.switches:
|
|
for char in speaker.location.contents:
|
|
if hasattr(char, 'other_say') and callable(char.other_say):
|
|
char.other_say(speaker, for_others)
|
|
else:
|
|
# Send the message to 'puppets' that have the special 'other_sayto'
|
|
# method (if they are the object of the message):
|
|
targets = split(r" *, *", self.lhs)
|
|
to_chars = [speaker.search(c, quiet=True) for c in targets]
|
|
to_chars = [c[0] for c in to_chars if len(c) > 0]
|
|
|
|
if to_chars == []:
|
|
speaker.msg(f"No match for {', '.join(targets)}")
|
|
return
|
|
|
|
for char in to_chars:
|
|
if hasattr(char, 'other_sayto') and callable(char.other_sayto):
|
|
logger.info(f"Found {char.key}: {for_others}")
|
|
char.other_sayto(speaker, for_others)
|
|
|
|
who = iter_to_str([c.get_display_name(speaker)
|
|
if isinstance(c, Character) else c
|
|
for c in to_chars])
|
|
whom = iter_to_str([f"/{c}" for c in targets])
|
|
|
|
if verb == "ask":
|
|
to_who = " " + who
|
|
to_whom = " " + whom
|
|
else:
|
|
to_who = " to " + who
|
|
to_whom = " to " + whom
|
|
|
|
# The `send_emote` is _global_, so if we want to say to 'Bob',
|
|
# 'You say ...', we have to both send him a message with the
|
|
# 'You' as well as everyone else with the 'send_emote'.
|
|
full_speech = f"You {adverb}{verb}{to_who}, \"{for_me}\""
|
|
speaker.msg(full_speech, from_obj=speaker)
|
|
|
|
# English is weird...
|
|
if verb == 'reply':
|
|
verb = 'replie'
|
|
|
|
targets = [item for item in speaker.location.contents
|
|
if item != speaker]
|
|
full_speech = f"/me {adverb}{verb}s{to_whom}, \"{for_others}\""
|
|
send_emote(speaker, targets, full_speech, msg_type="say",
|
|
anonymous_add=None)
|
|
|
|
|
|
class CmdThink(Command):
|
|
"""Think a thought out loud.
|
|
|
|
Usage:
|
|
think <message>
|
|
|
|
Similar to the 'say' or 'pose' commands, this communicates an
|
|
inner monologue to other players on the 'public' channel.
|
|
"""
|
|
|
|
key = "think"
|
|
aliases = ["thinks", "("]
|
|
arg_regex = None
|
|
|
|
def func(self):
|
|
"""Implement the think out loud command."""
|
|
if not self.args:
|
|
self.caller.msg("What do you want to think out loud?")
|
|
else:
|
|
thought = self.args.strip()
|
|
if (self.caller.db.thinking_count or 0) < 3 or random() < 0.4:
|
|
msg = routput(
|
|
f"<< thinks ^ wonders >> << out loud ^ aloud >> "
|
|
f"... o O ( {thought} )"
|
|
)
|
|
else:
|
|
msg = f". o O ( {thought} )"
|
|
self.caller.db.thinking_count = (self.caller.db.thinking_count or 0) + 1
|
|
|
|
self.caller.execute_cmd(f"pub :{msg}")
|
|
|
|
|
|
class CmdRead(Command):
|
|
"""
|
|
Return the inside contents of a book or other readable object.
|
|
|
|
Usage:
|
|
|
|
read <target>
|
|
|
|
To add something to read on target, use the @set command:
|
|
|
|
@set <target>/inside = 'This is the text to read.'
|
|
"""
|
|
|
|
key = "read"
|
|
|
|
def find_readable(self, reader, readable_str):
|
|
"""Search the room for a readable item."""
|
|
if readable_str == "chalkboard":
|
|
readable_str = "shrub"
|
|
elif readable_str.startswith("cocktail") or \
|
|
readable_str.startswith("drink") or readable_str == "list":
|
|
readable_str = "sign"
|
|
|
|
targets = reader.search(readable_str, quiet=True)
|
|
if not targets:
|
|
reader.msg(f"You don't see {readable_str}.")
|
|
return None
|
|
|
|
label_targets = [t for t in targets if t.db.inside]
|
|
|
|
if len(label_targets) == 1:
|
|
return label_targets[0]
|
|
|
|
if len(label_targets) == 0:
|
|
reader.msg(f"You can't find anything readable on {readable_str}.")
|
|
else:
|
|
reader.msg(f"Too many things match, '{readable_str}'. "
|
|
"Can you narrow it down with a title, "
|
|
"or preface with a number, like '2-paper'?")
|
|
|
|
def func(self):
|
|
"""Return the 'inside' attribute."""
|
|
reader = self.caller
|
|
target_str = self.args.strip()
|
|
if target_str == "":
|
|
reader.msg("Usage: |gread <object>|n")
|
|
return
|
|
|
|
book = self.find_readable(reader, target_str)
|
|
if book:
|
|
contents = book.db.inside
|
|
prefix = book.db.prefix
|
|
if prefix:
|
|
prefix = prefix + "|/"
|
|
if contents.startswith("file:"):
|
|
self.show_file(reader, contents[5:], prefix,
|
|
self.client_width(), reader.client_height())
|
|
else:
|
|
reader.msg((prefix or "") + contents)
|
|
|
|
def show_file(self, reader, filename, prefix, width, height):
|
|
"""
|
|
Display a file to the user.
|
|
|
|
The file is _somewhat_ Markdown formatted.
|
|
"""
|
|
with open(filename, "r") as myfile:
|
|
buf = myfile.read()
|
|
session = reader.sessions.get()[0]
|
|
width = self.client_width()
|
|
|
|
if reader.is_webclient():
|
|
tidied = md_to_html(buf)
|
|
else:
|
|
tidied = md_to_evennia(buf, reader.is_utf(), width)
|
|
if prefix:
|
|
tidied = prefix + "\n\n" + tidied
|
|
EvMore(reader, tidied, session=session,
|
|
justify=True, justify_kwargs={"width": width})
|
|
|
|
def md_to_evennia(text, utf, width):
|
|
brk = '─' if utf else '-'
|
|
line_brk = '|W' + (brk * width) + '|n'
|
|
breaks = [line_brk if line.startswith("#") else line
|
|
for line in md_preprocessor(text)]
|
|
return "\n".join(breaks)
|
|
|
|
def md_to_html(text):
|
|
brk = '───────────────────────────────────────────────────────'
|
|
breaks = [brk if line.startswith("#") else line
|
|
for line in md_preprocessor(text)]
|
|
return "\n".join(breaks)
|
|
|
|
def md_preprocessor(text):
|
|
lines = text.splitlines()
|
|
return [line for line in lines if not line.startswith(">")]
|
|
|
|
class CmdTake(CmdGet, NumberedTargetCommand):
|
|
"""
|
|
Get something, possibly from another character or NPC.
|
|
|
|
Usage:
|
|
get <thing> [ from <character> ]
|
|
|
|
Note that only some things can be stolen.
|
|
For instance, the brass ring from the door knocker.
|
|
"""
|
|
key = "take"
|
|
aliases = ["steal", "get"]
|
|
rhs_split = ("=", " from ")
|
|
|
|
def func(self):
|
|
"""
|
|
Implement the take command.
|
|
|
|
Since this command is designed to work on the object, we
|
|
operate only on self.obj.
|
|
"""
|
|
if not self.args:
|
|
self.caller.msg("What do you want to get?")
|
|
elif not self.rhs:
|
|
# This is soo bad to hard-code this game logic, but enough
|
|
# people complained about not being able to 'get' the
|
|
# ring, I need to:
|
|
if self.lhs == "ring" and self.caller.location.key == "Grotto":
|
|
self.caller.msg("From whom do you want to get this ring?")
|
|
else:
|
|
super().func() # Call the 'get' function instead.
|
|
else:
|
|
location = self.caller.search(self.rhs, quiet=True)
|
|
if len(location) == 0:
|
|
self.caller.msg(f"Can't take '{self.lhs}' from '{self.rhs}'.")
|
|
return
|
|
|
|
location = location[0]
|
|
if location.is_typeclass("typeclasses.rooms.Room"):
|
|
self.args = self.lhs
|
|
super().func() # Call the 'get' function instead.
|
|
return
|
|
self.caller.do_take(self.lhs, location)
|
|
|
|
|
|
class CmdDrink(Command):
|
|
"""
|
|
Drink a beverage in your inventory.
|
|
|
|
Usage:
|
|
|
|
drink [ container ]
|
|
|
|
If you are holding a teacup, or cocktail, and it is not empty,
|
|
you may drink.
|
|
|
|
This doesn't tell others of this particular activity.
|
|
"""
|
|
|
|
key = "drink"
|
|
aliases = ["sip", "quaff"]
|
|
|
|
def drink_item(self, name):
|
|
"""Find item in inventory, name, and call 'do_drink' on it."""
|
|
notfound = f"You don't have {name} in your inventory."
|
|
item = self.caller.search(name, location=self.caller,
|
|
nofound_string=notfound)
|
|
if item:
|
|
if item.has_method('do_drink'):
|
|
item.do_drink(self.caller)
|
|
else:
|
|
self.caller.msg(f"The {item.name} is not drinkable.")
|
|
|
|
def drink_anything(self):
|
|
"""Drink anything in your inventory, but only if you have one thing."""
|
|
def not_empty(item):
|
|
return (item.db.amount or 0) > 0
|
|
containers = self.search_contents_by_func('do_drink', not_empty)
|
|
if len(containers) == 1:
|
|
containers[0].do_drink(self.caller)
|
|
elif len(containers) > 1:
|
|
self.caller.msg("You have too many things you can drink. "
|
|
"Which one do you want?")
|
|
else:
|
|
self.caller.msg("You have nothing to drink.")
|
|
|
|
def func(self):
|
|
"""
|
|
If given the name of something to drink, find and drink it.
|
|
|
|
Otherwise, drink the first item you find in your inventory.
|
|
"""
|
|
goal = self.args.strip()
|
|
if goal and goal != "":
|
|
self.drink_item(goal)
|
|
else:
|
|
self.drink_anything()
|
|
|
|
|
|
class CmdEat(Command):
|
|
"""
|
|
Eat something edible in your inventory.
|
|
|
|
Usage:
|
|
|
|
eat [ food-item ]
|
|
|
|
This doesn't tell others of this particular activity.
|
|
"""
|
|
|
|
key = "eat"
|
|
aliases = ["consume", "bite"]
|
|
|
|
def eat_item(self, name):
|
|
"""Find item in inventory, name, and call 'do_eat' on it."""
|
|
notfound = f"You don't have {name} in your inventory."
|
|
item = self.caller.search(name, location=self.caller,
|
|
nofound_string=notfound)
|
|
if item:
|
|
if item.has_method('do_eat'):
|
|
item.do_eat(self.caller)
|
|
else:
|
|
self.caller.msg(f"The {item.name} is not edible.")
|
|
|
|
def eat_anything(self):
|
|
"""Eat something in your inventory, but only if you have one thing."""
|
|
def not_gone(item):
|
|
return (item.db.amount or 0) > 0
|
|
items = self.search_contents_by_func('do_eat', not_gone)
|
|
if len(items) == 0:
|
|
self.caller.msg("You have nothing to eat.")
|
|
else:
|
|
item = items[0]
|
|
if len(items) > 1:
|
|
self.caller.msg(f"Eating the {item.name}.")
|
|
item.do_eat(self.caller)
|
|
|
|
def func(self):
|
|
"""
|
|
If given the name of something to eat, find and eat it.
|
|
|
|
Otherwise, eat the first item you find in your inventory.
|
|
"""
|
|
goal = self.args.strip()
|
|
if goal and goal != "":
|
|
self.eat_item(goal)
|
|
else:
|
|
self.eat_anything()
|
|
|
|
|
|
class CmdFeed(MuxCommand):
|
|
"""
|
|
Feed or give something to an object that can eat.
|
|
Typically this is used to feed wood to a fire, or
|
|
berries to a beast.
|
|
|
|
Usage:
|
|
|
|
|gfeed <target>|n
|
|
|
|
Or:
|
|
|
|
|gfeed <food> to <target>|n
|
|
|
|
Where 'food' is something you have in your inventory,
|
|
and target is a pet or something that wants to eat.
|
|
|
|
If you don't specify the 'food', the target will eat
|
|
what you might have they're interested in.
|
|
"""
|
|
key = "feed"
|
|
rhs_split = ("=", " to ")
|
|
|
|
def func(self):
|
|
"""
|
|
Implements the feed (or give) command.
|
|
"""
|
|
if not self.args:
|
|
self.caller.msg("Feed what?")
|
|
return
|
|
|
|
feeder = self.caller
|
|
if self.rhs:
|
|
eater = feeder.search(self.rhs)
|
|
food = feeder.search(self.lhs, location=feeder)
|
|
if not food:
|
|
return
|
|
else:
|
|
eater = feeder.search(self.lhs)
|
|
food = None
|
|
|
|
if eater:
|
|
if eater.has_method('feed'):
|
|
eater.feed(feeder, food)
|
|
if food:
|
|
try:
|
|
food.delete()
|
|
# Allow the eater to delete the object.
|
|
except ObjectDoesNotExist:
|
|
pass
|
|
else:
|
|
name = eater.get_display_name(feeder)
|
|
feeder.msg(f"You can't feed, {name}.")
|
|
else:
|
|
feeder.msg(f"Don't see a '{self.lhs}' to feed.")
|
|
|
|
# Pass this off to CmdGive?
|
|
#
|
|
# feeder.execute_cmd("give " + self.args)
|
|
#
|
|
# supercmd = CmdGive()
|
|
# supercmd.caller = feeder
|
|
# supercmd.args = self.args
|
|
# supercmd.lhs = self.lhs
|
|
# supercmd.rhs = self.rhs
|
|
# supercmd.func()
|