diff --git a/docs/0.9.5/api/evennia.commands.default.building.html b/docs/0.9.5/api/evennia.commands.default.building.html
index ffb4bd5c68..14f5ec31d2 100644
--- a/docs/0.9.5/api/evennia.commands.default.building.html
+++ b/docs/0.9.5/api/evennia.commands.default.building.html
@@ -493,7 +493,7 @@ You can specify the /force switch to bypass this confirmation.
diff --git a/docs/0.9.5/api/evennia.commands.default.general.html b/docs/0.9.5/api/evennia.commands.default.general.html
index f7fd1e4a5e..705d6eb3bb 100644
--- a/docs/0.9.5/api/evennia.commands.default.general.html
+++ b/docs/0.9.5/api/evennia.commands.default.general.html
@@ -194,7 +194,7 @@ for everyone to use, you need build privileges and the alias command.
diff --git a/docs/0.9.5/api/evennia.commands.default.unloggedin.html b/docs/0.9.5/api/evennia.commands.default.unloggedin.html
index 25c0200eb3..a025a6170f 100644
--- a/docs/0.9.5/api/evennia.commands.default.unloggedin.html
+++ b/docs/0.9.5/api/evennia.commands.default.unloggedin.html
@@ -162,7 +162,7 @@ version is a bit more complicated.
diff --git a/docs/0.9.5/api/evennia.contrib.barter.html b/docs/0.9.5/api/evennia.contrib.barter.html
index 903c6a9849..d8ed59a894 100644
--- a/docs/0.9.5/api/evennia.contrib.barter.html
+++ b/docs/0.9.5/api/evennia.contrib.barter.html
@@ -650,7 +650,7 @@ try to influence the other part in the deal.
diff --git a/docs/0.9.5/api/evennia.contrib.dice.html b/docs/0.9.5/api/evennia.contrib.dice.html
index 366e7900eb..091a1c7b56 100644
--- a/docs/0.9.5/api/evennia.contrib.dice.html
+++ b/docs/0.9.5/api/evennia.contrib.dice.html
@@ -148,7 +148,7 @@ everyone but the person rolling.
diff --git a/docs/0.9.5/api/evennia.contrib.email_login.html b/docs/0.9.5/api/evennia.contrib.email_login.html
index 65a86bf2e0..67703d310e 100644
--- a/docs/0.9.5/api/evennia.contrib.email_login.html
+++ b/docs/0.9.5/api/evennia.contrib.email_login.html
@@ -170,7 +170,7 @@ version is a bit more complicated.
diff --git a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_basic.html b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_basic.html
index 697aed5087..52c32bffe5 100644
--- a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_basic.html
+++ b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_basic.html
@@ -559,7 +559,7 @@ if there are still any actions you can take.
diff --git a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_equip.html b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_equip.html
index 8275cb9482..3ee48e7e0e 100644
--- a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_equip.html
+++ b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_equip.html
@@ -676,7 +676,7 @@ if there are still any actions you can take.
diff --git a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_items.html b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_items.html
index dd5bc378dc..85135cbad3 100644
--- a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_items.html
+++ b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_items.html
@@ -710,7 +710,7 @@ if there are still any actions you can take.
diff --git a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_magic.html b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_magic.html
index 6650ac1cef..ef49f7ee95 100644
--- a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_magic.html
+++ b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_magic.html
@@ -582,7 +582,7 @@ if there are still any actions you can take.
diff --git a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_range.html b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_range.html
index 7cd74cd0dc..33d21a0459 100644
--- a/docs/0.9.5/api/evennia.contrib.turnbattle.tb_range.html
+++ b/docs/0.9.5/api/evennia.contrib.turnbattle.tb_range.html
@@ -994,7 +994,7 @@ if there are still any actions you can take.
diff --git a/docs/0.9.5/api/evennia.contrib.tutorial_examples.cmdset_red_button.html b/docs/0.9.5/api/evennia.contrib.tutorial_examples.cmdset_red_button.html
index 68d3e689bf..ce4d0e18e6 100644
--- a/docs/0.9.5/api/evennia.contrib.tutorial_examples.cmdset_red_button.html
+++ b/docs/0.9.5/api/evennia.contrib.tutorial_examples.cmdset_red_button.html
@@ -104,7 +104,7 @@ push the lid of the button away.
diff --git a/docs/0.9.5/api/evennia.utils.evmore.html b/docs/0.9.5/api/evennia.utils.evmore.html
index c1b8dca698..31fc312c9d 100644
--- a/docs/0.9.5/api/evennia.utils.evmore.html
+++ b/docs/0.9.5/api/evennia.utils.evmore.html
@@ -74,7 +74,7 @@ the caller.msg() construct every time the page is updated.
diff --git a/docs/1.0-dev/.buildinfo b/docs/1.0-dev/.buildinfo
index 56bac3a45b..c0e5e4e1bf 100644
--- a/docs/1.0-dev/.buildinfo
+++ b/docs/1.0-dev/.buildinfo
@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: 8d59ca17d9f31b2e60107187f8a52393
+config: 166ec97b932b6fa6f556720d3621f65e
tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/1.0-dev/Components/Channels.html b/docs/1.0-dev/Components/Channels.html
index a1ecafb30e..c169a890e1 100644
--- a/docs/1.0-dev/Components/Channels.html
+++ b/docs/1.0-dev/Components/Channels.html
@@ -39,7 +39,390 @@
In a multiplayer game, players often need other means of in-game communication
+than moving to the same room and use say or emote.
+
Channels allows Evennia’s to act as a fancy chat program. When a player is
+connected to a channel, sending a message to it will automatically distribute
+it to every other subscriber.
+
Channels can be used both for chats between Accounts and between
+Objects (usually Characters). Chats could be both OOC
+(out-of-character) or IC (in-charcter) in nature. Some examples:
+
+
A support channel for contacting staff (OOC)
+
A general chat for discussing anything and foster community (OOC)
+
Admin channel for private staff discussions (OOC)
+
Private guild channels for planning and organization (IC/OOC depending on game)
+
Cyberpunk-style retro chat rooms (IC)
+
In-game radio channels (IC)
+
Group telephathy (IC)
+
Walkie talkies (IC)
+
+
+
Changed in version 1.0: Channel system changed to use a central ‘channel’ command and nicks instead of
+auto-generated channel-commands and -cmdset. ChannelHandler was removed.
In the default command set, channels are all handled via the mighty
+channel command, channel (or
+chan). By default, this command will assume all entities dealing with
+channels are Accounts.
If the channel-name has spaces in it, you need to use a ‘=’:
+
channel rest room = Hello world!
+
+
+
Now, this is more to type than we’d like, so when you join a channel, the
+system automatically sets up an personal alias so you can do this instead:
+
publicHelloworld
+
+
+
+
Warning
+
This shortcut will not work if the channel-name has spaces in it.
+So channels with long names should make sure to provide a one-word alias as
+well.
+
+
Any user can make up their own channel aliases:
+
channel/aliaspublic=foo;bar
+
+
+
You can now just do
+
foo Hello world!
+bar Hello again!
+
+
+
And even remove the default one if they don’t want to use it
+
channel/unaliaspublic
+publicHello
+
+
+
But you can also use your alias with the channel command:
+
channel foo Hello world!
+
+
+
+
What happens when aliasing is that a nick is created that maps your
+alias + argument onto calling the channel command. So when you enter foohello,
+what the server sees is actually channelfoo=hello. The system is also
+clever enough to know that whenever you search for channels, your channel-nicks
+should also be considered so as to convert your input to an existing channel name.
+
+
You can check if you missed channel conversations by viewing the channel’s
+scrollback with
+
channel/historypublic
+
+
+
This retrieves the last 20 lines of text (also from a time when you were
+offline). You can step further back by specifying how many lines back to start:
+
channel/historypublic=30
+
+
+
This again retrieve 20 lines, but starting 30 lines back (so you’ll get lines
+30-50 counting backwards).
Aliases are optional but can be good for obvious shortcuts everyone may want to
+use. The description is used in channel-listings. You will automatically join a
+channel you created and will be controlling it. You can also use channel/desc to
+change the description on a channel you wnn later.
+
If you control a channel you can also kick people off it:
The last part is an optional reason to send to the user before they are booted.
+You can give a comma-separated list of channels to kick the same user from all
+those channels at once. The user will be unsubbed from the channel and all
+their aliases will be wiped. But they can still rejoin if they like.
Banning adds the user to the channels blacklist. This means they will not be
+able to rejoin if you boot them. You will need to run channel/boot to
+actually kick them out.
+
See the Channel command api
+docs (and in-game help) for more details.
+
Admin-level users can also modify channel’s locks:
listen - who may listen to the channel. Users without this access will not
+even be able to join the channel and it will not appear in listings for them.
+
send - who may send to the channel.
+
control - this is assigned to you automatically when you create the channel. With
+control over the channel you can edit it, boot users and do other management tasks.
By default everyone can use the channel command (evennia.commands.default.comms.CmdChannel)
+to create channels and will then control the channels they created (to boot/ban
+people etc). If you as a developer does not want regular players to do this
+(perhaps you want only staff to be able to spawn new channels), you can
+override the channel command and change its locks property.
+
The default help command has the following locks property:
cmd:pperm(channel_banned) - The cmd locktype is the standard one used for all Commands.
+an accessing object failing this will not even know that the command exists. The pperm() lockfunc
+checks an on-account [Permission](Building Permissions) ‘channel_banned’ - and the not means
+that if they have that ‘permission’ they are cut off from using the channel command. You usually
+don’t need to change this lock.
+
admin:all() - this is a lock checked in the channel command itself. It controls access to the
+/boot, /ban and /unban switches (by default letting everyone use them).
+
manage:all() - this controls access to the /create, /destroy, /desc switches.
+
changelocks:perm(Admin) - this controls access to the /lock and /unlock switches. By
+default this is something only [Admins](Building Permissions) can change.
+
+
+
Note - while admin:all() and manage:all() will let everyone use these switches, users
+will still only be able to admin or destroy channels they actually control!
+
+
If you only want (say) Builders and higher to be able to create and admin
+channels you could override the help command and change the lockstring to:
+
1
+2
+3
+4
+5
+6
# in for example mygame/commands/commands.py
+
+ fromevenniaimportdefault_cmds
+
+ classMyCustomChannelCmd(default_cmds.CmdChannel):
+ locks="cmd: not pperm(channel_banned);admin:perm(Builder);manage:perm(Builder);changelocks:perm(Admin)"
+
+
+
Add this custom command to your default cmdset and regular users wil now get an
+access-denied error when trying to use use these switches.
The default channel command (evennia.commands.default.comms.CmdChannel)
+sits in the Accountcommand set. It is set up such that it will
+always operate on Accounts, even if you were to add it to the
+CharacterCmdSet.
+
It’s a one-line change to make this command accept non-account callers. But for
+convenience we provide a version for Characters/Objects. Just import
+evennia.commands.default.comms.CmdObjectChannel
+and inherit from that instead.
When distributing a message, the channel will call a series of hooks on itself
+and (more importantly) on each recipient. So you can customize things a lot by
+just modifying hooks on your normal Object/Account typeclasses.
+
Internally, the message is sent with
+channel.msg(message,senders=sender,bypass_mute=False,**kwargs), where
+bypass_mute=True means the message ignores muting (good for alerts or if you
+delete the channel etc) and **kwargs are any extra info you may want to pass
+to the hooks. The senders (it’s always only one in the default implementation
+but could in principle be multiple) and bypass_mute are part of the kwargs
+below:
+
+
channel.at_pre_msg(message,**kwargs)
+
For each recipient:
+
+
message=recipient.at_pre_channel_msg(message,channel,**kwargs) -
+allows for the message to be tweaked per-receiver (for example coloring it depending
+on the users’ preferences). If this method returns False/None, that
+recipient is skipped.
+
recipient.channel_msg(message,channel,**kwargs) - actually sends to recipient.
+
recipient.at_post_channel_msg(message,channel,**kwargs) - any post-receive effects.
+
+
+
channel.at_post_channel_msg(message,**kwargs)
+
+
Note that Accounts and Objects both have their have separate sets of hooks.
+So make sure you modify the set actually used by your subcribers (or both).
+Default channels all use Account subscribers.
For most common changes, the default channel, the recipient hooks and possibly
+overriding the channel command will get you very far. But you can also tweak
+channels themselves.
+
Channels are Typeclassed entities. This means they are
+persistent in the database, can have attributes and Tags
+and can be easily extended.
+
To change which channel typeclass Evennia uses for default commands, change
+settings.BASE_CHANNEL_TYPECLASS. The base command class is
+evennia.comms.comms.DefaultChannel.
+There is an empty child class in mygame/typeclasses/channels.py, same
+as for other typelass-bases.
+
In code you create a new channel with evennia.create_channel or
+Channel.create:
fromevenniaimportcreate_channel,search_object
+ fromtypeclasses.channelsimportChannel
+
+ channel=create_channel("my channel",aliases=["mychan"],locks=...,typeclass=...)
+ # alternative
+ channel=Channel.create("my channel",aliases=["mychan"],locks=...)
+
+ # connect to it
+ me=search_object(key="Foo")[0]
+ channel.connect(me)
+
+ # send to it (this will trigger the channel_msg hooks described earlier)
+ channel.msg("Hello world!",senders=me)
+
+ # view subscriptions (the SubscriptionHandler handles all subs under the hood)
+ channel.subscriptions.has(me)# check we subbed
+ channel.subscriptions.all()# get all subs
+ channel.subscriptions.online()# get only subs currently online
+ channel.subscriptions.clear()# unsub all
+
+ # leave channel
+ channel.disconnect(me)
+
+ # permanently delete channel (will unsub everyone)
+ channel.delete()
+
+
+
The Channel’s .connect method will accept both Account and Object subscribers
+and will handle them transparently.
+
The channel has many more hooks, both hooks shared with all typeclasses as well
+as special ones related to muting/banning etc. See the channel class for
+details.
Changed in version 0.7: Channels changed from using Msg to TmpMsg and optional log files.
+
+
+
Changed in version 1.0: Channels stopped supporting Msg and TmpMsg, using only log files.
+
+
The channel messages are not stored in the database. A channel is instead
+always logged to a regular text log-file
+mygame/server/logs/channel_<channelname>.log. This is where channels/historychannelname
+gets its data from. A channel’s log will rotate when it grows too big, which
+thus also automatically limits the max amount of history a user can view with
+/history.
+
The log file name is set on the channel class as the log_file property. This
+is a string that takes the formatting token {channelname} to be replaced with
+the (lower-case) name of the channel. By default the log is written to in the
+channel’s at_post_channel_msg method.
Channels have all the standard properties of a Typeclassed entity (key,
+aliases, attributes, tags, locks etc). This is not an exhaustive list;
+see the Channel api docs for details.
+
+
send_to_online_only - this class boolean defaults to True and is a
+sensible optimization since people offline people will not see the message anyway.
+
log_file - this is a string that determines the name of the channel log file. Default
+is "channel_{channelname}.log". The log file will appear in settings.LOG_DIR (usually
+mygame/server/logs/). You should usually not change this.
+
channel_prefix_string - this property is a string to easily change how
+the channel is prefixed. It takes the channelname format key. Default is "[{channelname}]"
+and produces output like `[public] …``.
+
subscriptions - this is the SubscriptionHandler, which
+has methods has, add, remove, all, clear and also online (to get
+only actually online channel-members).
+
wholist, mutelist, banlist are properties that return a list of subscribers,
+as well as who are currently muted or banned.
+
channel_msg_nick_pattern - this is a regex pattern for performing the in-place nick
+replacement (detect that channelalias<msg means that you want to send a message to a channel).
+This pattern accepts an {alias} formatting marker. Don’t mess with this unless you really
+want to change how channels work.
+
channel_msg_nick_replacement - this is a string on the [nick replacement
+
form](Nicks). It accepts the {channelname} formatting tag. This is strongly tied to the
+channel command and is by default channel{channelname}=$1.
+
+
Notable Channel hooks:
+
+
at_pre_channel_msg(message,**kwargs) - called before sending a message, to
+modify it. Not used by default.
+
msg(message,senders=...,bypass_mute=False,**kwargs) - send the message onto
+the channel. The **kwargs are passed on into the other call hooks (also on the recipient).
+
at_post_channel_msg(message,**kwargs) - by default this is used to store the message
+to the log file.
+
channel_prefix(message) - this is called to allow the channel to prefix. This is called
+by the object/account when they build the message, so if wanting something else one can
+also just remove that call.
+
every channel message. By default it just returns channel_prefix_string.
+
has_connection(subscriber) - shortcut to check if an entity subscribes to
+this channel.
+
mute/unmute(subscriber) - this mutes the channel for this user.
+
ban/unban(subscriber) - adds/remove user from banlist.
+
connect/disconnect(subscriber) - adds/removes a subscriber.
+
add_user_channel_alias(user,alias,**kwargs) - sets up a user-nick for this channel. This is
+what maps e.g. alias<msg> to channelchannelname=<msg>.
+
remove_user_channel_alias(user,alias,**kwargs) - remove an alias. Note that this is
+a class-method that will happily remove found channel-aliases from the user linked to any
+channel, not only from the channel the method is called on.
+
pre_join_channel(subscriber) - if this returns False, connection will be refused.
+
post_join_channel(subscriber) - by default this sets up a users’s channel-nicks/aliases.
+
pre_leave_channel(subscriber) - if this returns False, the user is not allowed to leave.
+
post_leave_channel(subscriber) - this will clean up any channel aliases/nicks of the user.
+
delete the standard typeclass-delete mechanism will also automatically un-subscribe all
+subscribers (and thus wipe all their aliases).
Apart from moving around in the game world and talking, players might need other forms of
-communication. This is offered by Evennia’s Comm system. Stock evennia implements a ‘MUX-like’
-system of channels, but there is nothing stopping you from changing things to better suit your
-taste.
-
Comms rely on two main database objects - Msg and Channel. There is also the TempMsg which
-mimics the API of a Msg but has no connection to the database.
The Msg object is the basic unit of communication in Evennia. A message works a little like an
-e-mail; it always has a sender (a Account) and one or more recipients. The recipients
-may be either other Accounts, or a Channel (see below). You can mix recipients to send the message
-to both Channels and Accounts if you like.
-
Once created, a Msg is normally not changed. It is peristently saved in the database. This allows
-for comprehensive logging of communications. This could be useful for allowing senders/receivers to
-have ‘mailboxes’ with the messages they want to keep.
senders - this is a reference to one or many Account or Objects (normally
-Characters) sending the message. This could also be an External Connection such as a message
-coming in over IRC/IMC2 (see below). There is usually only one sender, but the types can also be
-mixed in any combination.
-
receivers - a list of target Accounts, Objects (usually Characters) or
-Channels to send the message to. The types of receivers can be mixed in any combination.
-
header - this is a text field for storing a title or header for the message.
hide_from - this can optionally hold a list of objects, accounts or channels to hide this Msg
-from. This relationship is stored in the database primarily for optimization reasons, allowing for
-quickly post-filter out messages not intended for a given target. There is no in-game methods for
-setting this, it’s intended to be done in code.
+
Channels - are used for implementing in-game chat rooms.
+
Msg-objects are used for storing messages in the database (email-like)
+and is a building block for implementing other game systems. It’s used by the
+page command by default.
-
You create new messages in code using evennia.create_message (or
-evennia.utils.create.create_message.)
evennia.comms.models also has TempMsg which mimics the API of Msg but is not connected to the
-database. TempMsgs are used by Evennia for channel messages by default. They can be used for any
-system expecting a Msg but when you don’t actually want to save anything.
Channels are Typeclassed entities, which mean they can be easily extended and their
-functionality modified. To change which channel typeclass Evennia uses, change
-settings.BASE_CHANNEL_TYPECLASS.
-
Channels act as generic distributors of messages. Think of them as “switch boards” redistributing
-Msg or TempMsg objects. Internally they hold a list of “listening” objects and any Msg (or
-TempMsg) sent to the channel will be distributed out to all channel listeners. Channels have
-Locks to limit who may listen and/or send messages through them.
-
The sending of text to a channel is handled by a dynamically created Command that
-always have the same name as the channel. This is created for each channel by the global
-ChannelHandler. The Channel command is added to the Account’s cmdset and normal command locks are
-used to determine which channels are possible to write to. When subscribing to a channel, you can
-then just write the channel name and the text to send.
-
The default ChannelCommand (which can be customized by pointing settings.CHANNEL_COMMAND_CLASS to
-your own command), implements a few convenient features:
-
-
It only sends TempMsg objects. Instead of storing individual entries in the database it instead
-dumps channel output a file log in server/logs/channel_<channelname>.log. This is mainly for
-practical reasons - we find one rarely need to query individual Msg objects at a later date. Just
-stupidly dumping the log to a file also means a lot less database overhead.
-
It adds a /history switch to view the 20 last messages in the channel. These are read from the
-end of the log file. One can also supply a line number to start further back in the file (but always
-20 entries at a time). It’s used like this:
-
>public/history
- >public/history35
-
-
-
-
-
There are two default channels created in stock Evennia - MudInfo and Public. MudInfo
-receives server-related messages meant for Admins whereas Public is open to everyone to chat on
-(all new accounts are automatically joined to it when logging in, it is useful for asking
-questions). The default channels are defined by the DEFAULT_CHANNELS list (see
-evennia/settings_default.py for more details).
-
You create new channels with evennia.create_channel (or evennia.utils.create.create_channel).
-
In code, messages are sent to a channel using the msg or tempmsg methods of channels:
The argument msgobj can be either a string, a previously constructed Msg or a TempMsg - in the
-latter cases all the following keywords are ignored since the message objects already contains all
-this information. If msgobj is a string, the other keywords are used for creating a new Msg or
-TempMsg on the fly, depending on if persistent is set or not. By default, a TempMsg is emitted
-for channel communication (since the default ChannelCommand instead logs to a file).
-
1
-2
-3
-4
# assume we have a 'sender' object and a channel named 'mychan'
-
- # manually sending a message to a channel
- mychan.msg("Hello!",senders=[sender])
-
An important part of Evennia is the online help system. This allows the players and staff alike to
-learn how to use the game’s commands as well as other information pertinent to the game. The help
-system has many different aspects, from the normal editing of help entries from inside the game, to
-auto-generated help entries during code development using the auto-help system.
Evennia has an extensive help system covering both command-help and regular
+free-form help documentation. It supports subtopics and if failing to find a
+match it will provide suggestsions, first from alternative topics and then by
+finding mentions of the search term in help entries.
This will show a list of help entries, ordered after categories. You will find two sections,
-Command help entries and Other help entries (initially you will only have the first one). You
-can use help to get more info about an entry; you can also give partial matches to get suggestions.
-If you give category names you will only be shown the topics in that category.
Use the /edit switch to open the EvEditor for more convenient in-game writing
+(but note that devs can also create help entries outside the game using their
+regular code editor, see below).
Auto-generated command help - this is literally the doc-strings of the Command classes.
+The idea is that the command docs are easier to maintain and keep up-to-date if
+the developer can change them at the same time as they do the code.
+
Database-stored help entries - These are created in-game (using the default sethelp command
+as exemplified in the previous section).
+
File-stored help entries - These are created outside the game, as dicts in
+normal Python modules. They allows developers to write and maintain their help files using
+a proper text editor.
All help entries (no matter the source) have the following properties:
+
+
key - This is the main topic-name. For Commands, this is literally the command’s key.
+
aliases - Alternate names for the help entry. This can be useful if the main name is hard to remember.
+
help_category - The general grouping of the entry. This is optional. If not given it will use the
+default category given by settings.COMMAND_DEFAULT_HELP_CATEGORY for Commands and settings.DEFAULT_HELP_CATEGORY
+for file+db help entries.
+
locks - This defines who may read this entry. The locktype checked by the help command is view. In the
+case of Commands, it’s more common that the cmd lock fails - in that case the command is not loaded
+into the help parser at all.
+
tags - This is not used by default, but could be used to further organize help entries.
+
text - The actual help entry text. This will be dedented and stripped of
+extra space at beginning and end.
+
+
A text that scrolls off the screen will automatically be paginated by
+the EvMore pager (you can control this with
+settings.HELP_MORE_ENABLED=False). If you use EvMore and want to control
+exactly where the pager should break the page, mark the break with the control
+character \f.
Rather than making a very long help entry, the text may also be broken up
+into subtopics. A list of the next level of subtopics are shown below the
+main help text and allows the user to read more about some particular detail
+that wouldn’t fit in the main text.
+
Subtopics use a markup slightly similar to markdown headings. The top level
+heading must be named #subtopics (non case-sensitive) and the following
+headers must be sub-headings to this (so ##subtopicname etc). All headings
+are non-case sensitive (the help command will format them). The topics can be
+nested at most to a depth of 5 (which is probably too many levels already). The
+parser uses fuzzy matching to find the subtopic, so one does not have to type
+it all out exactly.
+
Below is an example of a text with sub topics.
+
The theatre is the heart of the city, here you can find ...
+(This is the main help text, what you get with `help theatre`)
+
+# subtopics
+
+## lore
+
+The theatre holds many mysterious things...
+(`help theatre/lore`)
+
+### the grand opening
+
+The grand opening is the name for a mysterious event where ghosts appeared ...
+(`this is a subsub-topic to lore, accessible as `help theatre/lore/grand` or
+any other partial match).
+
+### the Phantom
+
+Deep under the theatre, rumors has it a monster hides ...
+(another subsubtopic, accessible as `help theatre/lore/phantom`)
+
+## layout
+
+The theatre is a two-story building situated at ...
+(`help theatre/layout`)
+
+## dramatis personae
+
+There are many interesting people prowling the halls of the theatre ...
+(`help theatre/dramatis` or `help theathre/drama` or `help theatre/personae` would work)
+
+### Primadonna Ada
+
+Everyone knows the primadonna! She is ...
+(A subtopic under dramatis personae, accessible as `help theatre/drama/ada` etc)
+
+### The gatekeeper
+
+He always keeps an eye on the door and ...
+(`help theatre/drama/gate`)
+
A common item that requires help entries are in-game commands. Keeping these entries up-to-date with
-the actual source code functionality can be a chore. Evennia’s commands are therefore auto-
-documenting straight from the sources through its auto-help system. Only commands that you and
-your character can actually currently use are picked up by the auto-help system. That means an admin
-will see a considerably larger amount of help topics than a normal player when using the default
-help command.
-
The auto-help system uses the __doc__ strings of your command classes and formats this to a nice-
-looking help entry. This makes for a very easy way to keep the help updated - just document your
-commands well and updating the help file is just a @reload away. There is no need to manually
-create and maintain help database entries for commands; as long as you keep the docstrings updated
-your help will be dynamically updated for you as well.
The auto-help system uses the __doc__ strings of your command classes and
+formats this to a nice- looking help entry. This makes for a very easy way to
+keep the help updated - just document your commands well and updating the help
+file is just a reload away.
Example (from a module with command definitions):
1
2
@@ -114,59 +234,157 @@ shown to users looking for help. Try to use a consistent format - all default co
structure shown above.
You should also supply the help_category class property if you can; this helps to group help
entries together for people to more easily find them. See the help command in-game to see the
-default categories. If you don’t specify the category, “General” is assumed.
+default categories. If you don’t specify the category, settings.COMMAND_DEFAULT_HELP_CATEGORY
+(default is “General”) is used.
If you don’t want your command to be picked up by the auto-help system at all (like if you want to
write its docs manually using the info in the next section or you use a cmdset that
has its own help functionality) you can explicitly set auto_help class property to False in your
command definition.
Alternatively, you can keep the advantages of auto-help in commands, but control the display of
-command helps. You can do so by overriding the command’s get_help() method. By default, this
+command helps. You can do so by overriding the command’s get_help(caller,cmdset) method. By default, this
method will return the class docstring. You could modify it to add custom behavior: the text
returned by this method will be displayed to the character asking for help in this command.
These are all help entries not involving commands (this is handled automatically by the Command
-Auto-help system). Non-automatic help entries describe how
-your particular game is played - its rules, world descriptions and so on.
-
A help entry consists of four parts:
-
-
The topic. This is the name of the help entry. This is what players search for when they are
-looking for help. The topic can contain spaces and also partial matches will be found.
-
The help category. Examples are Administration, Building, Comms or General. This is an
-overall grouping of similar help topics, used by the engine to give a better overview.
-
The text - the help text itself, of any length.
-
locks - a lock definition. This can be used to limit access to this help entry, maybe
-because it’s staff-only or otherwise meant to be restricted. Help commands check for access_types
-view and edit. An example of a lock string would be view:perm(Builders).
-
-
You can create new help entries in code by using evennia.create_help_entry().
These are most commonly created in-game using the sethelp command. If you need to create one
+manually, you can do so with evennia.create_help_entry():
1
2
3
-4
fromevenniaimportcreate_help_entry
+4
+5
+fromevenniaimportcreate_help_entryentry=create_help_entry("emote","Emoting is important because ...",category="Roleplaying",locks="view:all()")
-
From inside the game those with the right permissions can use the @sethelp command to add and
-modify help entries.
-
>@sethelp/addemote=Theemotecommandis...
-
+
The entity being created is a evennia.help.models.HelpEntry
+object. This is not a Typeclassed entity and is not meant to
+be modified to any great degree. It holds the properties listed earlier. The
+text is stored in a field entrytext. It does not provide a get_help method
+like commands, stores and returns the entrytext directly.
+
You can search for HelpEntry objects using evennia.search_help but note
+that this will not return the two other types of help entries.
-
Using @sethelp you can add, delete and append text to existing entries. By default new entries
-will go in the General help category. You can change this using a different form of the @sethelp
-command:
File-help entries are created by the game development team outside of the game. The
+help entries are defined in normal Python modules (.py file ending) containing
+a dict to represent each entry. They require a server reload before any changes
+apply.
+
+
Evennia will look through all modules given by settings.FILE_HELP_ENTRY_MODULES. This
+should be a list of python-paths for Evennia to import.
+
If this module contains a top-level variable HELP_ENTRY_DICTS, this will be imported
+and must be a list of help-entry dicts.
+
If no HELP_ENTRY_DICTS list is found, every top-level variable in the
+module that is a dict will be read as a help entry. The variable-names will
+be ignored in this case.
+
+
If you add multiple modules to be read, same-keyed help entries added later in the list
+will override coming before.
+
Each entry dict must define keys to match that needed by all help entries.
+Here’s an example of a help module:
+# in a module pointed to by settings.FILE_HELP_ENTRY_MODULES
+
+HELP_ENTRY_DICTS=[
+ {
+ "key":"The Gods",# case-insensitive, can be searched by 'gods' too
+ "aliases":['pantheon','religion']
+ "category":"Lore",
+ "text":'''
+ The gods formed the world ...
+
+ # Subtopics
+
+ ## Pantheon
+
+ The pantheon consists of 40 gods that ...
+
+ ### God of love
+
+ The most prominent god is ...
+
+ ### God of war
+
+ Also known as 'the angry god', this god is known to ...
+
+ '''
+ },
+ {
+ "key":"The mortals",
+
+ }
+]
+
+
The help entry text will be dedented and will retain paragraphs. You should try
+to keep your strings a reasonable width (it will look better). Just reload the
+server and the file-based help entries will be available to view.
Since the available commands may vary from moment to moment, help is
+responsible for collating the three sources of help-entries (commands/db/file)
+together and search through them on the fly. It also does all the formatting of
+the output.
+
To make it easier to tweak the look, the parts of the code that changes the
+visual presentation has been broken out into separate methods format_help_entry and
+format_help_index - override these in your version of help to change the display
+as you please. See the api link above for details.
Since it needs to search so different types of data, the help system has to
+collect all possibilities in memory before searching through the entire set. It
+uses the Lunr search engine to
+search through the main bulk of help entries. Lunr is a mature engine used for
+web-pages and produces much more sensible results than previous solutions.
+
Once the main entry has been found, subtopics are then searched with
+simple ==, startswith and in matching (there are so relatively few of them
+at that point).
+
+
Changed in version 1.0: Replaced the bag-of-words algorithm with lunr.
The Msg object represents a database-saved
+piece of communication. Think of it as a discrete piece of email - it contains
+a message, some metadata and will always have a sender and one or more
+recipients.
+
Once created, a Msg is normally not changed. It is persitently saved in the
+database. This allows for comprehensive logging of communications. Here are some
+good uses for Msg objects:
+
+
page/tells (the page command is how Evennia uses them out of the box)
+
messages in a bulletin board
+
game-wide email stored in ‘mailboxes’.
+
+
+
Important
+
A Msg does not have any in-game representation. So if you want to use them
+to represent in-game mail/letters, the physical letters would never be
+visible in a room (possible to steal, spy on etc) unless you make your
+spy-system access the Msgs directly (or go to the trouble of spawning an
+actual in-game letter-object based on the Msg)
+
+
+
Changed in version 1.0: Channels dropped Msg-support. Now only used in page command by default.
The Msg is intended to be used exclusively in code, to build other game systems. It is not
+a Typeclassed entity, which means it cannot (easily) be overridden. It
+doesn’t support Attributes (but it does support Tags). It tries to be lean
+and small since a new one is created for every message.
+
You create a new message with evennia.create_message:
fromevenniaimportsearch_message,Msg
+
+ # args are optional. Only a single sender/receiver should be passed
+ messages=search_message(sender=...,receiver=...,freetext=...,dbref=...)
+
+ # get all messages for a given sender/receiver
+ messages=Msg.objects.get_msg_by_sender(sender)
+ messages=Msg.objects.get_msg_by_receiver(recipient)
+
senders - there must always be at least one sender. This is one of Account, Object, Script
+or external - which is a string uniquely identifying the sender. The latter can be used by
+a sender-system that doesn’t fit into Evennia’s normal typeclass-system.
+While most systems expect a single sender, it’s possible to have any number of them.
+
receivers - these are the ones to see the Msg. These are again one of
+Account, Object or Script. It’s in principle possible to have
+zero receivers but most usages of Msg expects one or more.
+
header - this is an optional text field that can contain meta-information about the message. For
+an email-like system it would be the subject line. This can be independently searched, making
+this a powerful place for quickly finding messages.
+
message - the actual text being sent.
+
date_sent - this is auto-set to the time the Msg was created (and thus presumably sent).
+
locks - the Evennia lock handler. Use with locks.add() etc and check locks with msg.access()
+like for all other lockable entities. This can be used to limit access to the contents
+of the Msg. The default lock-type to check is 'read'.
+
hide_from - this is an optional list of Accounts or Objects that
+will not see this Msg. This relationship is available mainly for optimization
+reasons since it allows quick filtering of messages not intended for a given
+target.
evennia.comms.models.TempMsg is an object
+that implements the same API as the regular Msg, but which has no database
+component (and thus cannot be searched). It’s meant to plugged into systems
+expecting a Msg but where you just want to process the message without saving
+it.
+
+
+
\ No newline at end of file
diff --git a/docs/1.0-dev/Contribs/Crafting.html b/docs/1.0-dev/Contribs/Crafting.html
index a98b9f2cae..6fad3d71cc 100644
--- a/docs/1.0-dev/Contribs/Crafting.html
+++ b/docs/1.0-dev/Contribs/Crafting.html
@@ -127,7 +127,7 @@ with underscore) are considered by the system as viable recipes.
{"key":"A carved wooden doll","typeclass":"typeclasses.objects.decorations.Toys","desc":"A small carved doll"}
- ]
+ ]
This specifies which tags to look for in the inputs. It defines a Prototype
@@ -200,11 +200,11 @@ to be exactly right, if a failure still consumes the ingredients or not, and muc
pre_craft - this should handle input validation and store its data in .validated_consumables and
validated_tools respectively. On error, this reports the error to the crafter and raises the
CraftingValidationError.
-
do_craft - this will only be called if pre_craft finished without an exception. This should
+
craft - this will only be called if pre_craft finished without an exception. This should
return the result of the crafting, by spawnging the prototypes. Or the empty list if crafting
fails for some reason. This is the place to add skill-checks or random chance if you need it
for your game.
-
post_craft - this receives the result from do_craft and handles error messages and also deletes
+
post_craft - this receives the result from craft and handles error messages and also deletes
any consumables as needed. It may also modify the result before returning it.
msg - this is a wrapper for self.crafter.msg and should be used to send messages to the
crafter. Centralizing this means you can also easily modify the sending style in one place later.
@@ -247,7 +247,7 @@ this you need to make your own recipe parent class and have your recipes inherit
difficulty=20
- defdo_craft(self,**kwargs):
+ defcraft(self,**kwargs):"""The input is ok. Determine if crafting succeeds"""# this is set at initialization
@@ -259,7 +259,7 @@ this you need to make your own recipe parent class and have your recipes inherit
# roll for success:ifrandint(1,100)<=(crafting_skill-self.difficulty):# all is good, craft away
- returnsuper().do_craft()
+ returnsuper().craft()else:self.msg("You are not good enough to craft this. Better luck next time!")return[]
diff --git a/docs/1.0-dev/Howto/Customize-channels.html b/docs/1.0-dev/Howto/Customize-channels.html
deleted file mode 100644
index 3b420ed8fa..0000000000
--- a/docs/1.0-dev/Howto/Customize-channels.html
+++ /dev/null
@@ -1,788 +0,0 @@
-
-
-
-
-
-
-
- Customize channels — Evennia 1.0-dev documentation
-
-
-
-
-
-
-
-
-
-
-
-
By default, Evennia’s default channel commands are inspired by MUX. They all
-begin with “c” followed by the action to perform (like “ccreate” or “cdesc”).
-If this default seems strange to you compared to other Evennia commands that
-rely on switches, you might want to check this tutorial out.
-
This tutorial will also give you insight into the workings of the channel system.
-So it may be useful even if you don’t plan to make the exact changes shown here.
Our mission: change the default channel commands to have a different syntax.
-
This tutorial will do the following changes:
-
-
Remove all the default commands to handle channels.
-
Add a + and - command to join and leave a channel. So, assuming there is
-a public channel on your game (most often the case), you could type +public
-to join it and -public to leave it.
-
Group the commands to manipulate channels under the channel name, after a
-switch. For instance, instead of writing cdescpublic=Mypublicchannel,
-you would write public/descMypublicchannel.
-
-
-
I listed removing the default Evennia commands as a first step in the
-process. Actually, we’ll move it at the very bottom of the list, since we
-still want to use them, we might get it wrong and rely on Evennia commands
-for a while longer.
We’ll do the most simple task at first: create two commands, one to join a
-channel, one to leave.
-
-
Why not have them as switches? public/join and public/leave for instance?
-
-
For security reasons, I will hide channels to which the caller is not
-connected. It means that if the caller is not connected to the “public”
-channel, he won’t be able to use the “public” command. This is somewhat
-standard: if we create an administrator-only channel, we don’t want players to
-try (or even know) the channel command. Again, you could design it a different
-way should you want to.
-
First create a file named comms.py in your commands package. It’s
-a rather logical place, since we’ll write different commands to handle
-communication.
-
Okay, let’s add the first command to join a channel:
# in commands/comms.py
-fromevennia.utils.searchimportsearch_channel
-fromcommands.commandimportCommand
-
-classCmdConnect(Command):
- """
- Connect to a channel.
- """
-
- key="+"
- help_category="Comms"
- locks="cmd:not pperm(channel_banned)"
- auto_help=False
-
- deffunc(self):
- """Implement the command"""
- caller=self.caller
- args=self.args
- ifnotargs:
- self.msg("Which channel do you want to connect to?")
- return
-
- channelname=self.args
- channel=search_channel(channelname)
- ifnotchannel:
- return
-
- # Check permissions
- ifnotchannel.access(caller,'listen'):
- self.msg("%s: You are not allowed to listen to this channel."%channel.key)
- return
-
- # If not connected to the channel, try to connect
- ifnotchannel.has_connection(caller):
- ifnotchannel.connect(caller):
- self.msg("%s: You are not allowed to join this channel."%channel.key)
- return
- else:
- self.msg("You now are connected to the %s channel. "%channel.key.lower())
- else:
- self.msg("You already are connected to the %s channel. "%channel.key.lower())
-
-
-
Okay, let’s review this code, but if you’re used to Evennia commands, it shouldn’t be too strange:
-
-
We import search_channel. This is a little helper function that we will use to search for
-channels by name and aliases, found in evennia.utils.search. It’s just more convenient.
-
Our class CmdConnect contains the body of our command to join a channel.
-
Notice the key of this command is simply "+". When you enter +something in the game, it will
-try to find a command key +something. Failing that, it will look at other potential matches.
-Evennia is smart enough to understand that when we type +something, + is the command key and
-something is the command argument. This will, of course, fail if you have a command beginning by
-+ conflicting with the CmdConnect key.
-
We have altered some class attributes, like auto_help. If you want to know what they do and
-why they have changed here, you can check the documentation on commands.
-
In the command body, we begin by extracting the channel name. Remember that this name should be
-in the command arguments (that is, in self.args). Following the same example, if a player enters
-+something, self.args should contain "something". We use search_channel to see if this
-channel exists.
-
We then check the access level of the channel, to see if the caller can listen to it (not
-necessarily use it to speak, mind you, just listen to others speak, as these are two different locks
-on Evennia).
-
Finally, we connect the caller if he’s not already connected to the channel. We use the
-channel’s connect method to do this. Pretty straightforward eh?
-
-
Now we’ll add a command to leave a channel. It’s almost the same, turned upside down:
classCmdDisconnect(Command):
- """
- Disconnect from a channel.
- """
-
- key="-"
- help_category="Comms"
- locks="cmd:not pperm(channel_banned)"
- auto_help=False
-
- deffunc(self):
- """Implement the command"""
- caller=self.caller
- args=self.args
- ifnotargs:
- self.msg("Which channel do you want to disconnect from?")
- return
-
- channelname=self.args
- channel=search_channel(channelname)
- ifnotchannel:
- return
-
- # If connected to the channel, try to disconnect
- ifchannel.has_connection(caller):
- ifnotchannel.disconnect(caller):
- self.msg("%s: You are not allowed to disconnect from this channel."%channel.key)
- return
- else:
- self.msg("You stop listening to the %s channel. "%channel.key.lower())
- else:
- self.msg("You are not connected to the %s channel. "%channel.key.lower())
-
-
-
So far, you shouldn’t have trouble following what this command does: it’s
-pretty much the same as the CmdConnect class in logic, though it accomplishes
-the opposite. If you are connected to the channel public you could
-disconnect from it using -public. Remember, you can use channel aliases too
-(+pub and -pub will also work, assuming you have the alias pub on the
-public channel).
-
It’s time to test this code, and to do so, you will need to add these two
-commands. Here is a good time to say it: by default, Evennia connects accounts
-to channels. Some other games (usually with a higher multisession mode) will
-want to connect characters instead of accounts, so that several characters in
-the same account can be connected to various channels. You can definitely add
-these commands either in the AccountCmdSet or CharacterCmdSet, the caller
-will be different and the command will add or remove accounts of characters.
-If you decide to install these commands on the CharacterCmdSet, you might
-have to disconnect your superuser account (account #1) from the channel before
-joining it with your characters, as Evennia tends to subscribe all accounts
-automatically if you don’t tell it otherwise.
-
So here’s an example of how to add these commands into your AccountCmdSet.
-Edit the file commands/default_cmdsets.py to change a few things:
# In commands/default_cmdsets.py
-fromevenniaimportdefault_cmds
-fromcommands.commsimportCmdConnect,CmdDisconnect
-
-
-# ... Skip to the AccountCmdSet class ...
-
-classAccountCmdSet(default_cmds.AccountCmdSet):
- """
- This is the cmdset available to the Account at all times. It is
- combined with the `CharacterCmdSet` when the Account puppets a
- Character. It holds game-account-specific commands, channel
- commands, etc.
- """
- key="DefaultAccount"
-
- defat_cmdset_creation(self):
- """
- Populates the cmdset
- """
- super().at_cmdset_creation()
-
- # Channel commands
- self.add(CmdConnect())
- self.add(CmdDisconnect())
-
-
-
Save, reload your game, and you should be able to use +public and -public
-now!
It’s time to dive a little deeper into channel processing. What happens in
-Evennia when a player enters publicHelloeverybody!?
-
Like exits, channels are a particular command that Evennia automatically
-creates and attaches to individual channels. So when you enter publicmessage in your game, Evennia calls the public command.
-
-
But I didn’t add any public command…
-
-
Evennia will just create these commands automatically based on the existing
-channels. The base command is the command we’ll need to edit.
-
-
Why edit it? It works just fine to talk.
-
-
Unfortunately, if we want to add switches to our channel names, we’ll have to
-edit this command. It’s not too hard, however, we’ll just start writing a
-standard command with minor twitches.
# In commands/comms.py
-classChannelCommand(Command):
- """
- {channelkey} channel
-
- {channeldesc}
-
- Usage:
- {lower_channelkey} <message>
- {lower_channelkey}/history [start]
- {lower_channelkey}/me <message>
- {lower_channelkey}/who
-
- Switch:
- history: View 20 previous messages, either from the end or
- from <start> number of messages from the end.
- me: Perform an emote on this channel.
- who: View who is connected to this channel.
-
- Example:
- {lower_channelkey} Hello World!
- {lower_channelkey}/history
- {lower_channelkey}/history 30
- {lower_channelkey}/me grins.
- {lower_channelkey}/who
- """
- # note that channeldesc and lower_channelkey will be filled
- # automatically by ChannelHandler
-
- # this flag is what identifies this cmd as a channel cmd
- # and branches off to the system send-to-channel command
- # (which is customizable by admin)
- is_channel=True
- key="general"
- help_category="Channel Names"
- obj=None
- arg_regex=""
-
-
-
There are some differences here compared to most common commands.
-
-
There is something disconcerting in the class docstring. Some information is
-between curly braces. This is a format-style which is only used for channel
-commands. {channelkey} will be replaced by the actual channel key (like
-public). {channeldesc} will be replaced by the channel description (like
-“public channel”). And {lower_channelkey}.
-
We have set is_channel to True in the command class variables. You
-shouldn’t worry too much about that: it just tells Evennia this is a special
-command just for channels.
-
key is a bit misleading because it will be replaced eventually. So we
-could set it to virtually anything.
-
The obj class variable is another one we won’t detail right now.
-
arg_regex is important: the default arg_regex in the channel command will
-forbid to use switches (a slash just after the channel name is not allowed).
-That’s why we enforce it here, we allow any syntax.
-
-
-
What will become of this command?
-
-
Well, when we’ll be through with it, and once we’ll add it as the default
-command to handle channels, Evennia will create one per existing channel. For
-instance, the public channel will receive one command of this class, with key
-set to public and aliases set to the channel aliases (like ['pub']).
-
-
Can I see it work?
-
-
Not just yet, there’s still a lot of code needed.
-
Okay we have the command structure but it’s rather empty.
Reading the comments we see that the channel handler will send the command in a
-strange way: a string with the channel name, a colon and the actual message
-entered by the player. So if the player enters “public hello”, the command
-args will contain "public:hello". You can look at the way the channel name
-and message are parsed, this can be used in a lot of different commands.
-
Next we check if there’s any switch, that is, if the message starts with a
-slash. This would be the case if a player entered public/mejumpsupanddown, for instance. If there is a switch, we save it in self.switch. We
-alter self.args at the end to contain a tuple with two values: the channel
-name, and the message (if a switch was used, notice that the switch will be
-stored in self.switch, not in the second element of self.args).
# ...
- deffunc(self):
- """
- Create a new message and send it to channel, using
- the already formatted input.
- """
- channelkey,msg=self.args
- caller=self.caller
- channel=ChannelDB.objects.get_channel(channelkey)
-
- # Check that the channel exists
- ifnotchannel:
- self.msg(_("Channel '%s' not found.")%channelkey)
- return
-
- # Check that the caller is connected
- ifnotchannel.has_connection(caller):
- string="You are not connected to channel '%s'."
- self.msg(string%channelkey)
- return
-
- # Check that the caller has send access
- ifnotchannel.access(caller,'send'):
- string="You are not permitted to send to channel '%s'."
- self.msg(string%channelkey)
- return
-
- # Handle the various switches
- ifself.switch=="me":
- ifnotmsg:
- self.msg("What do you want to do on this channel?")
- else:
- msg="{}{}".format(caller.key,msg)
- channel.msg(msg,online=True)
- elifself.switch:
- self.msg("{}: Invalid switch {}.".format(channel.key,self.switch))
- elifnotmsg:
- self.msg("Say what?")
- else:
- ifcallerinchannel.mutelist:
- self.msg("You currently have %s muted."%channel)
- return
- channel.msg(msg,senders=self.caller,online=True)
-
-
-
-
First of all, we try to get the channel object from the channel name we have
-in the self.args tuple. We use ChannelDB.objects.get_channel this time
-because we know the channel name isn’t an alias (that was part of the deal,
-channelname in the parse method contains a command key).
-
We check that the channel does exist.
-
We then check that the caller is connected to the channel. Remember, if the
-caller isn’t connected, we shouldn’t allow him to use this command (that
-includes the switches on channels).
-
We then check that the caller has access to the channel’s send lock. This
-time, we make sure the caller can send messages to the channel, no matter what
-operation he’s trying to perform.
-
Finally we handle switches. We try only one switch: me. This switch would
-be used if a player entered public/mejumpsupanddown (to do a channel
-emote).
-
We handle the case where the switch is unknown and where there’s no switch
-(the player simply wants to talk on this channel).
-
-
The good news: The code is not too complicated by itself. The bad news is that
-this is just an abridged version of the code. If you want to handle all the
-switches mentioned in the command help, you will have more code to write. This
-is left as an exercise.
It’s almost done, but we need to add a method in this command class that isn’t
-often used. I won’t detail it’s usage too much, just know that Evennia will use
-it and will get angry if you don’t add it. So at the end of your class, just
-add:
-
1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
# ...
- defget_extra_info(self,caller,**kwargs):
- """
- Let users know that this command is for communicating on a channel.
-
- Args:
- caller (TypedObject): A Character or Account who has entered an ambiguous command.
-
- Returns:
- A string with identifying information to disambiguate the object, conventionally with a
-preceding space.
- """
- return" (channel)"
-
Contrary to most Evennia commands, we won’t add our ChannelCommand to a
-CmdSet. Instead we need to tell Evennia that it should use the command we
-just created instead of its default channel-command.
-
In your server/conf/settings.py file, add a new setting:
Then you can reload your game. Try to type publichello and public/mejumpsupanddown. Don’t forget to enter helppublic to see if your command has
-truly been added.
That was some adventure! And there’s still things to do! But hopefully, this
-tutorial will have helped you in designing your own channel system. Here are a
-few things to do:
-
-
Add more switches to handle various actions, like changing the description of
-a channel for instance, or listing the connected participants.
-
Remove the default Evennia commands to handle channels.
-
Alter the behavior of the channel system so it better aligns with what you
-want to do.
-
-
As a special bonus, you can find a full, working example of a communication
-system similar to the one I’ve shown you: this is a working example, it
-integrates all switches and does ever some extra checking, but it’s also very
-close from the code I’ve provided here. Notice, however, that this resource is
-external to Evennia and not maintained by anyone but the original author of
-this article.
-
[Read the full example on Github](https://github.com/vincent-
-lg/avenew/blob/master/commands/comms.py)
-
-
-
\ No newline at end of file
diff --git a/docs/1.0-dev/Howto/Howto-Overview.html b/docs/1.0-dev/Howto/Howto-Overview.html
index 820e636126..da295faab0 100644
--- a/docs/1.0-dev/Howto/Howto-Overview.html
+++ b/docs/1.0-dev/Howto/Howto-Overview.html
@@ -124,7 +124,6 @@ in mind for your own game, this will give you a good start.
diff --git a/docs/1.0-dev/_modules/evennia.html b/docs/1.0-dev/_modules/evennia.html
index 2df53c1903..ddc37efd9e 100644
--- a/docs/1.0-dev/_modules/evennia.html
+++ b/docs/1.0-dev/_modules/evennia.html
@@ -136,7 +136,6 @@
TASK_HANDLER=NoneTICKER_HANDLER=NoneMONITOR_HANDLER=None
-CHANNEL_HANDLER=None# ContainersGLOBAL_SCRIPTS=None
@@ -190,7 +189,7 @@
globalsignalsglobalsettings,lockfuncs,logger,utils,gametime,ansi,spawn,managersglobalcontrib,TICKER_HANDLER,MONITOR_HANDLER,SESSION_HANDLER
- globalCHANNEL_HANDLER,TASK_HANDLER
+ globalTASK_HANDLERglobalGLOBAL_SCRIPTS,OPTION_CLASSESglobalEvMenu,EvTable,EvForm,EvMore,EvEditorglobalANSIString
@@ -253,7 +252,6 @@
from.scripts.tickerhandlerimportTICKER_HANDLERfrom.scripts.taskhandlerimportTASK_HANDLERfrom.server.sessionhandlerimportSESSION_HANDLER
- from.comms.channelhandlerimportCHANNEL_HANDLERfrom.scripts.monitorhandlerimportMONITOR_HANDLER# containers
@@ -382,7 +380,6 @@
CMD_NOINPUT - no input was given on command line CMD_NOMATCH - no valid command key was found CMD_MULTIMATCH - multiple command matches were found
- CMD_CHANNEL - the command name is a channel name CMD_LOGINSTART - this command will be called as the very first command when an account connects to the server.
@@ -397,7 +394,6 @@
CMD_NOINPUT=cmdhandler.CMD_NOINPUTCMD_NOMATCH=cmdhandler.CMD_NOMATCHCMD_MULTIMATCH=cmdhandler.CMD_MULTIMATCH
- CMD_CHANNEL=cmdhandler.CMD_CHANNELCMD_LOGINSTART=cmdhandler.CMD_LOGINSTARTdelcmdhandler
diff --git a/docs/1.0-dev/_modules/evennia/accounts/accounts.html b/docs/1.0-dev/_modules/evennia/accounts/accounts.html
index e5086eb54e..ea77e74b43 100644
--- a/docs/1.0-dev/_modules/evennia/accounts/accounts.html
+++ b/docs/1.0-dev/_modules/evennia/accounts/accounts.html
@@ -90,6 +90,7 @@
_MAX_NR_CHARACTERS=settings.MAX_NR_CHARACTERS_CMDSET_ACCOUNT=settings.CMDSET_ACCOUNT_MUDINFO_CHANNEL=None
+_CONNECT_CHANNEL=None_CMDHANDLER=None# Create throttles for too many account-creations and login attempts
@@ -275,6 +276,21 @@
returnobjs
+
[docs]defget_display_name(self,looker,**kwargs):
+ """
+ This is used by channels and other OOC communications methods to give a
+ custom display of this account's input.
+
+ Args:
+ looker (Account): The one that will see this name.
+ **kwargs: Unused by default, can be used to pass game-specific data.
+
+ Returns:
+ str: The name, possibly modified.
+
+ """
+ returnf"|c{self.key}|n"
[docs]defat_pre_channel_msg(self,message,channel,senders=None,**kwargs):
+ """
+ Called by the Channel just before passing a message into `channel_msg`.
+ This allows for tweak messages per-user and also to abort the
+ receive on the receiver-level.
+
+ Args:
+ message (str): The message sent to the channel.
+ channel (Channel): The sending channel.
+ senders (list, optional): Accounts or Objects acting as senders.
+ For most normal messages, there is only a single sender. If
+ there are no senders, this may be a broadcasting message.
+ **kwargs: These are additional keywords passed into `channel_msg`.
+ If `no_prefix=True` or `emit=True` are passed, the channel
+ prefix will not be added (`[channelname]: ` by default)
+
+ Returns:
+ str or None: Allows for customizing the message for this recipient.
+ If returning `None` (or `False`) message-receiving is aborted.
+ The returning string will be passed into `self.channel_msg`.
+
+ Notes:
+ This support posing/emotes by starting channel-send with : or ;.
+
+ """
+ ifsenders:
+ sender_string=', '.join(sender.get_display_name(self)forsenderinsenders)
+ message_lstrip=message.lstrip()
+ ifmessage_lstrip.startswith((':',';')):
+ # this is a pose, should show as e.g. "User1 smiles to channel"
+ spacing=""ifmessage_lstrip[1:].startswith((':','\'',','))else" "
+ message=f"{sender_string}{spacing}{message_lstrip[1:]}"
+ else:
+ # normal message
+ message=f"{sender_string}: {message}"
+
+ ifnotkwargs.get("no_prefix")ornotkwargs.get("emit"):
+ message=channel.channel_prefix()+message
+
+ returnmessage
+
+
[docs]defchannel_msg(self,message,channel,senders=None,**kwargs):
+ """
+ This performs the actions of receiving a message to an un-muted
+ channel.
+
+ Args:
+ message (str): The message sent to the channel.
+ channel (Channel): The sending channel.
+ senders (list, optional): Accounts or Objects acting as senders.
+ For most normal messages, there is only a single sender. If
+ there are no senders, this may be a broadcasting message or
+ similar.
+ **kwargs: These are additional keywords originally passed into
+ `Channel.msg`.
+
+ Notes:
+ Before this, `Channel.at_pre_channel_msg` will fire, which offers a way
+ to customize the message for the receiver on the channel-level.
+
+ """
+ self.msg(text=(message,{"from_channel":channel.id}),
+ from_obj=senders,options={"from_channel":channel.id})
+
+
[docs]defat_post_channel_msg(self,message,channel,senders=None,**kwargs):
+ """
+ Called by `self.channel_msg` after message was received.
+
+ Args:
+ message (str): The message sent to the channel.
+ channel (Channel): The sending channel.
+ senders (list, optional): Accounts or Objects acting as senders.
+ For most normal messages, there is only a single sender. If
+ there are no senders, this may be a broadcasting message.
+ **kwargs: These are additional keywords passed into `channel_msg`.
+
+ """
+ pass
+
+ # search method
+
[docs]defsearch(self,searchdata,
@@ -1294,30 +1393,42 @@
def_send_to_connect_channel(self,message):"""
- Helper method for loading and sending to the comm channel
- dedicated to connection messages.
+ Helper method for loading and sending to the comm channel dedicated to
+ connection messages. This will also be sent to the mudinfo channel. Args: message (str): A message to send to the connect channel. """
- global_MUDINFO_CHANNEL
- ifnot_MUDINFO_CHANNEL:
- try:
- _MUDINFO_CHANNEL=ChannelDB.objects.filter(db_key=settings.CHANNEL_MUDINFO["key"])[
- 0
- ]
- exceptException:
- logger.log_trace()
+ global_MUDINFO_CHANNEL,_CONNECT_CHANNEL
+ if_MUDINFO_CHANNELisNone:
+ ifsettings.CHANNEL_MUDINFO:
+ try:
+ _MUDINFO_CHANNEL=ChannelDB.objects.get(
+ db_key=settings.CHANNEL_MUDINFO["key"])
+ exceptChannelDB.DoesNotExist:
+ logger.log_trace()
+ else:
+ _MUDINFO=False
+ if_CONNECT_CHANNELisNone:
+ ifsettings.CHANNEL_CONNECTINFO:
+ try:
+ _CONNECT_CHANNEL=ChannelDB.objects.get(
+ db_key=settings.CHANNEL_CONNECTINFO["key"])
+ exceptChannelDB.DoesNotExist:
+ logger.log_trace()
+ else:
+ _CONNECT_CHANNEL=False
+
ifsettings.USE_TZ:now=timezone.localtime()else:now=timezone.now()now="%02i-%02i-%02i(%02i:%02i)"%(now.year,now.month,now.day,now.hour,now.minute)if_MUDINFO_CHANNEL:
- _MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")
- else:
- logger.log_info(f"[{now}]: {message}")
+ _MUDINFO_CHANNEL.msg(f"[{now}]: {message}")
+ if_CONNECT_CHANNEL:
+ _CONNECT_CHANNEL.msg(f"[{now}]: {message}")
[docs]defat_post_login(self,session=None,**kwargs):"""
diff --git a/docs/1.0-dev/_modules/evennia/commands/cmdhandler.html b/docs/1.0-dev/_modules/evennia/commands/cmdhandler.html
index 1f9b7076e4..056b5104a9 100644
--- a/docs/1.0-dev/_modules/evennia/commands/cmdhandler.html
+++ b/docs/1.0-dev/_modules/evennia/commands/cmdhandler.html
@@ -48,10 +48,6 @@
1. The calling object (caller) is analyzed based on its callertype.2. Cmdsets are gathered from different sources:
- - channels: all available channel names are auto-created into a cmdset, to allow
- for giving the channel name and have the following immediately
- sent to the channel. The sending is performed by the CMD_CHANNEL
- system command. - object cmdsets: all objects at caller's location are scanned for non-empty cmdsets. This includes cmdsets on exits. - caller: the caller is searched for its own currently active cmdset.
@@ -65,14 +61,12 @@
cmdset, or fallback to error message. Exit.7. If no match was found -> check for CMD_NOMATCH in current cmdset or fallback to error message. Exit.
-8. A single match was found. If this is a channel-command (i.e. the
- ommand name is that of a channel), --> check for CMD_CHANNEL in
- current cmdset or use channelhandler default. Exit.
-9. At this point we have found a normal command. We assign useful variables to it that
+8. At this point we have found a normal command. We assign useful variables to it that will be available to the command coder at run-time.
-12. We have a unique cmdobject, primed for use. Call all hooks:
+9. We have a unique cmdobject, primed for use. Call all hooks: `at_pre_cmd()`, `cmdobj.parse()`, `cmdobj.func()` and finally `at_post_cmd()`.
-13. Return deferred that will fire with the return from `cmdobj.func()` (unused by default).
+10. Return deferred that will fire with the return from `cmdobj.func()` (unused by default).
+
"""fromcollectionsimportdefaultdict
@@ -86,7 +80,6 @@
fromtwisted.internet.deferimportinlineCallbacks,returnValuefromdjango.confimportsettingsfromevennia.commands.commandimportInterruptCommand
-fromevennia.comms.channelhandlerimportCHANNELHANDLERfromevennia.utilsimportlogger,utilsfromevennia.utils.utilsimportstring_suggestions
@@ -117,12 +110,11 @@
CMD_NOMATCH="__nomatch_command"# command to call if multiple command matches were foundCMD_MULTIMATCH="__multimatch_command"
-# command to call if found command is the name of a channel
-CMD_CHANNEL="__send_to_channel_command"# command to call as the very first one when the user connects.# (is expected to display the login screen)CMD_LOGINSTART="__unloggedin_look_command"
+
# Function for handling multiple command matches._SEARCH_AT_RESULT=utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".",1))
@@ -345,19 +337,6 @@
"""try:
- @inlineCallbacks
- def_get_channel_cmdset(account_or_obj):
- """
- Helper-method; Get channel-cmdsets
- """
- # Create cmdset for all account's available channels
- try:
- channel_cmdset=yieldCHANNELHANDLER.get_cmdset(account_or_obj)
- returnValue([channel_cmdset])
- exceptException:
- _msg_err(caller,_ERROR_CMDSETS)
- raiseErrorReported(raw_string)
-
@inlineCallbacksdef_get_local_obj_cmdsets(obj):"""
@@ -447,13 +426,13 @@
cmdsetforcmdsetinlocal_obj_cmdsetsifcmdset.key!="ExitCmdSet"]cmdsets+=local_obj_cmdsets
- ifnotcurrent.no_channels:
- # also objs may have channels
- channel_cmdsets=yield_get_channel_cmdset(obj)
- cmdsets+=channel_cmdsets
- ifnotcurrent.no_channels:
- channel_cmdsets=yield_get_channel_cmdset(account)
- cmdsets+=channel_cmdsets
+ # if not current.no_channels:
+ # # also objs may have channels
+ # channel_cmdsets = yield _get_channel_cmdset(obj)
+ # cmdsets += channel_cmdsets
+ # if not current.no_channels:
+ # channel_cmdsets = yield _get_channel_cmdset(account)
+ # cmdsets += channel_cmdsetselifcallertype=="account":# we are calling the command from the account level
@@ -471,11 +450,11 @@
cmdsetforcmdsetinlocal_obj_cmdsetsifcmdset.key!="ExitCmdSet"]cmdsets+=local_obj_cmdsets
- ifnotcurrent.no_channels:
- # also objs may have channels
- cmdsets+=yield_get_channel_cmdset(obj)
- ifnotcurrent.no_channels:
- cmdsets+=yield_get_channel_cmdset(account)
+ # if not current.no_channels:
+ # # also objs may have channels
+ # cmdsets += yield _get_channel_cmdset(obj)
+ # if not current.no_channels:
+ # cmdsets += yield _get_channel_cmdset(account)elifcallertype=="object":# we are calling the command from the object level
@@ -489,9 +468,9 @@
cmdsetforcmdsetinlocal_obj_cmdsetsifcmdset.key!="ExitCmdSet"]cmdsets+=yieldlocal_obj_cmdsets
- ifnotcurrent.no_channels:
- # also objs may have channels
- cmdsets+=yield_get_channel_cmdset(obj)
+ # if not current.no_channels:
+ # # also objs may have channels
+ # cmdsets += yield _get_channel_cmdset(obj)else:raiseException("get_and_merge_cmdsets: callertype %s is not valid."%callertype)
@@ -814,18 +793,6 @@
sysarg+=_(' Type "help" for help.')raiseExecSystemCommand(syscmd,sysarg)
- # Check if this is a Channel-cmd match.
- ifhasattr(cmd,"is_channel")andcmd.is_channel:
- # even if a user-defined syscmd is not defined, the
- # found cmd is already a system command in its own right.
- syscmd=yieldcmdset.get(CMD_CHANNEL)
- ifsyscmd:
- # replace system command with custom version
- cmd=syscmd
- cmd.session=session
- sysarg="%s:%s"%(cmdname,args)
- raiseExecSystemCommand(cmd,sysarg)
-
# A normal command.ret=yield_run_command(cmd,cmdname,args,raw_cmdname,cmdset,session,account)returnValue(ret)
diff --git a/docs/1.0-dev/_modules/evennia/commands/command.html b/docs/1.0-dev/_modules/evennia/commands/command.html
index feced9619c..f804b1c862 100644
--- a/docs/1.0-dev/_modules/evennia/commands/command.html
+++ b/docs/1.0-dev/_modules/evennia/commands/command.html
@@ -161,9 +161,11 @@
# parsing errors.
-
[docs]classCommand(metaclass=CommandMeta):"""
- Base command
+ ## Base command
+
+ (you may see this if a child command had no help text defined) Usage: command [args]
diff --git a/docs/1.0-dev/_modules/evennia/commands/default/cmdset_account.html b/docs/1.0-dev/_modules/evennia/commands/default/cmdset_account.html
index 6c6363ee3f..759184ca32 100644
--- a/docs/1.0-dev/_modules/evennia/commands/default/cmdset_account.html
+++ b/docs/1.0-dev/_modules/evennia/commands/default/cmdset_account.html
@@ -101,15 +101,15 @@
self.add(admin.CmdNewPassword())# Comm commands
+ self.add(comms.CmdChannel())
+ # self.add(comms.CmdChannels())self.add(comms.CmdAddCom())self.add(comms.CmdDelCom())self.add(comms.CmdAllCom())
- self.add(comms.CmdChannels())self.add(comms.CmdCdestroy())self.add(comms.CmdChannelCreate())self.add(comms.CmdClock())self.add(comms.CmdCBoot())
- self.add(comms.CmdCemit())self.add(comms.CmdCWho())self.add(comms.CmdCdesc())self.add(comms.CmdPage())
diff --git a/docs/1.0-dev/_modules/evennia/commands/default/comms.html b/docs/1.0-dev/_modules/evennia/commands/default/comms.html
index d6a32b0a69..2ea04614ef 100644
--- a/docs/1.0-dev/_modules/evennia/commands/default/comms.html
+++ b/docs/1.0-dev/_modules/evennia/commands/default/comms.html
@@ -41,24 +41,24 @@
Source code for evennia.commands.default.comms
"""
-Comsystem command module.
+Communication commands:
-Comm commands are OOC commands and intended to be made available to
-the Account at all times (they go into the AccountCmdSet). So we
-make sure to homogenize self.caller to always be the account object
-for easy handling.
+- channel
+- page
+- irc/rss/grapevine linking"""
-importhashlib
-importtime
+
fromdjango.confimportsettings
-fromevennia.comms.modelsimportChannelDB,Msg
+fromevennia.comms.modelsimportMsgfromevennia.accounts.modelsimportAccountDBfromevennia.accountsimportbots
-fromevennia.comms.channelhandlerimportCHANNELHANDLERfromevennia.locks.lockhandlerimportLockException
-fromevennia.utilsimportcreate,logger,utils,evtable
-fromevennia.utils.utilsimportmake_iter,class_from_module
+fromevennia.comms.commsimportDefaultChannel
+fromevennia.utilsimportcreate,logger,utils
+fromevennia.utils.loggerimporttail_log_file
+fromevennia.utils.utilsimportclass_from_module
+fromevennia.utils.evmenuimportask_yes_noCOMMAND_DEFAULT_CLASS=class_from_module(settings.COMMAND_DEFAULT_CLASS)CHANNEL_DEFAULT_TYPECLASS=class_from_module(
@@ -67,18 +67,20 @@
# limit symbol import for API__all__=(
+ "CmdChannel",
+
"CmdAddCom","CmdDelCom","CmdAllCom",
- "CmdChannels","CmdCdestroy","CmdCBoot",
- "CmdCemit","CmdCWho","CmdChannelCreate","CmdClock","CmdCdesc",
+
"CmdPage",
+
"CmdIRC2Chan","CmdIRCStatus","CmdRSS2Chan",
@@ -86,36 +88,1212 @@
)_DEFAULT_WIDTH=settings.CLIENT_DEFAULT_WIDTH
+# helper functions to make it easier to override the main CmdChannel
+# command and to keep the legacy addcom etc commands around.
-deffind_channel(caller,channelname,silent=False,noaliases=False):
+
+
[docs]classCmdChannel(COMMAND_DEFAULT_CLASS):"""
- Helper function for searching for a single channel with
- some error handling.
+ Use and manage in-game channels.
+
+ Usage:
+ channel channelname <msg>
+ channel channel name = <msg>
+ channel (show all subscription)
+ channel/all (show available channels)
+ channel/alias channelname = alias[;alias...]
+ channel/unalias alias
+ channel/who channelname
+ channel/history channelname [= index]
+ channel/sub channelname [= alias[;alias...]]
+ channel/unsub channelname[,channelname, ...]
+ channel/mute channelname[,channelname,...]
+ channel/unmute channelname[,channelname,...]
+
+ channel/create channelname[;alias;alias[:typeclass]] [= description]
+ channel/destroy channelname [= reason]
+ channel/desc channelname = description
+ channel/lock channelname = lockstring
+ channel/unlock channelname = lockstring
+ channel/ban channelname (list bans)
+ channel/ban[/quiet] channelname[, channelname, ...] = subscribername [: reason]
+ channel/unban[/quiet] channelname[, channelname, ...] = subscribername
+ channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]
+
+ # subtopics
+
+ ## sending
+
+ Usage: channel channelname msg
+ channel channel name = msg (with space in channel name)
+
+ This sends a message to the channel. Note that you will rarely use this
+ command like this; instead you can use the alias
+
+ channelname <msg>
+ channelalias <msg>
+
+ For example
+
+ public Hello World
+ pub Hello World
+
+ (this shortcut doesn't work for aliases containing spaces)
+
+ See channel/alias for help on setting channel aliases.
+
+ ## alias and unalias
+
+ Usage: channel/alias channel = alias[;alias[;alias...]]
+ channel/unalias alias
+ channel - this will list your subs and aliases to each channel
+
+ Set one or more personal aliases for referencing a channel. For example:
+
+ channel/alias warrior's guild = warrior;wguild;warchannel;warrior guild
+
+ You can now send to the channel using all of these:
+
+ warrior's guild Hello
+ warrior Hello
+ wguild Hello
+ warchannel Hello
+
+ Note that this will not work if the alias has a space in it. So the
+ 'warrior guild' alias must be used with the `channel` command:
+
+ channel warrior guild = Hello
+
+ Channel-aliases can be removed one at a time, using the '/unalias' switch.
+
+ ## who
+
+ Usage: channel/who channelname
+
+ List the channel's subscribers. Shows who are currently offline or are
+ muting the channel. Subscribers who are 'muting' will not see messages sent
+ to the channel (use channel/mute to mute a channel).
+
+ ## history
+
+ Usage: channel/history channel [= index]
+
+ This will display the last |c20|n lines of channel history. By supplying an
+ index number, you will step that many lines back before viewing those 20 lines.
+
+ For example:
+
+ channel/history public = 35
+
+ will go back 35 lines and show the previous 20 lines from that point (so
+ lines -35 to -55).
+
+ ## sub and unsub
+
+ Usage: channel/sub channel [=alias[;alias;...]]
+ channel/unsub channel
+
+ This subscribes you to a channel and optionally assigns personal shortcuts
+ for you to use to send to that channel (see aliases). When you unsub, all
+ your personal aliases will also be removed.
+
+ ## mute and unmute
+
+ Usage: channel/mute channelname
+ channel/unmute channelname
+
+ Muting silences all output from the channel without actually
+ un-subscribing. Other channel members will see that you are muted in the /who
+ list. Sending a message to the channel will automatically unmute you.
+
+ ## create and destroy
+
+ Usage: channel/create channelname[;alias;alias[:typeclass]] [= description]
+ channel/destroy channelname [= reason]
+
+ Creates a new channel (or destroys one you control). You will automatically
+ join the channel you create and everyone will be kicked and loose all aliases
+ to a destroyed channel.
+
+ ## lock and unlock
+
+ Usage: channel/lock channelname = lockstring
+ channel/unlock channelname = lockstring
+
+ Note: this is an admin command.
+
+ A lockstring is on the form locktype:lockfunc(). Channels understand three
+ locktypes:
+ listen - who may listen or join the channel.
+ send - who may send messages to the channel
+ control - who controls the channel. This is usually the one creating
+ the channel.
+
+ Common lockfuncs are all() and perm(). To make a channel everyone can
+ listen to but only builders can talk on, use this:
+
+ listen:all()
+ send: perm(Builders)
+
+ ## boot and ban
+
+ Usage:
+ channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]
+ channel/ban channelname[, channelname, ...] = subscribername [: reason]
+ channel/unban channelname[, channelname, ...] = subscribername
+ channel/unban channelname
+ channel/ban channelname (list bans)
+
+ Booting will kick a named subscriber from channel(s) temporarily. The
+ 'reason' will be passed to the booted user. Unless the /quiet switch is
+ used, the channel will also be informed of the action. A booted user is
+ still able to re-connect, but they'll have to set up their aliases again.
+
+ Banning will blacklist a user from (re)joining the provided channels. It
+ will then proceed to boot them from those channels if they were connected.
+ The 'reason' and `/quiet` works the same as for booting.
+
+ Example:
+ boot mychannel1 = EvilUser : Kicking you to cool down a bit.
+ ban mychannel1,mychannel2= EvilUser : Was banned for spamming.
+
"""
- channels=CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname)
- ifnotchannels:
- ifnotnoaliases:
- channels=[
- chan
- forchaninCHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
- ifchannelnameinchan.aliases.all()
- ]
- ifchannels:
+ key="channel"
+ aliases=["chan","channels"]
+ help_category="Comms"
+ # these cmd: lock controls access to the channel command itself
+ # the admin: lock controls access to /boot/ban/unban switches
+ # the manage: lock controls access to /create/destroy/desc/lock/unlock switches
+ locks="cmd:not pperm(channel_banned);admin:all();manage:all();changelocks:perm(Admin)"
+ switch_options=(
+ "list","all","history","sub","unsub","mute","unmute","alias","unalias",
+ "create","destroy","desc","lock","unlock","boot","ban","unban","who",)
+ # disable this in child command classes if wanting on-character channels
+ account_caller=True
+
+
[docs]defsearch_channel(self,channelname,exact=False,handle_errors=True):
+ """
+ Helper function for searching for a single channel with some error
+ handling.
+
+ Args:
+ channelname (str): Name, alias #dbref or partial name/alias to search
+ for.
+ exact (bool, optional): If an exact or fuzzy-match of the name should be done.
+ Note that even for a fuzzy match, an exactly given, unique channel name
+ will always be returned.
+ handle_errors (bool): If true, use `self.msg` to report errors if
+ there are non/multiple matches. If so, the return will always be
+ a single match or None.
+ Returns:
+ object, list or None: If `handle_errors` is `True`, this is either a found Channel
+ or `None`. Otherwise it's a list of zero, one or more channels found.
+ Notes:
+ The 'listen' and 'control' accesses are checked before returning.
+
+ """
+ caller=self.caller
+ # first see if this is a personal alias
+ channelname=caller.nicks.get(key=channelname,category="channel")orchannelname
+
+ # always try the exact match first.
+ channels=CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname,exact=True)
+
+ ifnotchannelsandnotexact:
+ # try fuzzy matching as well
+ channels=CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname,exact=exact)
+
+ # check permissions
+ channels=[channelforchannelinchannels
+ ifchannel.access(caller,'listen')orchannel.access(caller,'control')]
+
+ ifhandle_errors:
+ ifnotchannels:
+ self.msg(f"No channel found matching '{channelname}' "
+ "(could also be due to missing access).")
+ returnNone
+ eliflen(channels)>1:
+ self.msg("Multiple possible channel matches/alias for "
+ "'{channelname}':\n"+", ".join(chan.keyforchaninchannels))
+ returnNonereturnchannels[0]
- ifnotsilent:
- caller.msg("Channel '%s' not found."%channelname)
- returnNone
- eliflen(channels)>1:
- matches=", ".join(["%s(%s)"%(chan.key,chan.id)forchaninchannels])
- ifnotsilent:
- caller.msg("Multiple channels match (be more specific): \n%s"%matches)
- returnNone
- returnchannels[0]
+ else:
+ ifnotchannels:
+ return[]
+ eliflen(channels)>1:
+ returnlist(channels)
+ return[channels[0]]
+
+
[docs]defmsg_channel(self,channel,message,**kwargs):
+ """
+ Send a message to a given channel. This will check the 'send'
+ permission on the channel.
+
+ Args:
+ channel (Channel): The channel to send to.
+ message (str): The message to send.
+ **kwargs: Unused by default. These kwargs will be passed into
+ all channel messaging hooks for custom overriding.
+
+ """
+ ifnotchannel.access(self.caller,"send"):
+ caller.msg(f"You are not allowed to send messages to channel {channel}")
+ return
+
+ channel.msg(message,senders=self.caller,**kwargs)
+
+
[docs]defget_channel_history(self,channel,start_index=0):
+ """
+ View a channel's history.
+
+ Args:
+ channel (Channel): The channel to access.
+ message (str): The message to send.
+ **kwargs: Unused by default. These kwargs will be passed into
+ all channel messaging hooks for custom overriding.
+
+ """
+ caller=self.caller
+ log_file=channel.get_log_filename()
+
+ defsend_msg(lines):
+ returnself.msg(
+ "".join(line.split("[-]",1)[1]if"[-]"inlineelselineforlineinlines)
+ )
+ # asynchronously tail the log file
+ tail_log_file(log_file,start_index,20,callback=send_msg)
+
+
[docs]defsub_to_channel(self,channel):
+ """
+ Subscribe to a channel. Note that all permissions should
+ be checked before this step.
+
+ Args:
+ channel (Channel): The channel to access.
+
+ Returns:
+ bool, str: True, None if connection failed. If False,
+ the second part is an error string.
+
+ """
+ caller=self.caller
+
+ ifchannel.has_connection(caller):
+ returnFalse,f"Already listening to channel {channel.key}."
+
+ # this sets up aliases in post_join_channel by default
+ result=channel.connect(caller)
+
+ returnresult,""ifresultelsef"Were not allowed to subscribe to channel {channel.key}"
+
+
[docs]defunsub_from_channel(self,channel,**kwargs):
+ """
+ Un-Subscribe to a channel. Note that all permissions should
+ be checked before this step.
+
+ Args:
+ channel (Channel): The channel to unsub from.
+ **kwargs: Passed on to nick removal.
+
+ Returns:
+ bool, str: True, None if un-connection succeeded. If False,
+ the second part is an error string.
+
+ """
+ caller=self.caller
+
+ ifnotchannel.has_connection(caller):
+ returnFalse,f"Not listening to channel {channel.key}."
+
+ # this will also clean aliases
+ result=channel.disconnect(caller)
+
+ returnresult,""ifresultelsef"Could not unsubscribe from channel {channel.key}"
+
+
[docs]defadd_alias(self,channel,alias,**kwargs):
+ """
+ Add a new alias (nick) for the user to use with this channel.
+
+ Args:
+ channel (Channel): The channel to alias.
+ alias (str): The personal alias to use for this channel.
+ **kwargs: If given, passed into nicks.add.
+
+ Note:
+ We add two nicks - one is a plain `alias -> channel.key` that
+ we need to be able to reference this channel easily. The other
+ is a templated nick to easily be able to send messages to the
+ channel without needing to give the full `channel` command. The
+ structure of this nick is given by `self.channel_msg_pattern`
+ and `self.channel_msg_nick_replacement`. By default it maps
+ `alias <msg> -> channel <channelname> = <msg>`, so that you can
+ for example just write `pub Hello` to send a message.
+
+ The alias created is `alias $1 -> channel channel = $1`, to allow
+ for sending to channel using the main channel command.
+
+ """
+ channel.add_user_channel_alias(self.caller,alias,**kwargs)
+
+
[docs]defremove_alias(self,alias,**kwargs):
+ """
+ Remove an alias from a channel.
+
+ Args:
+ alias (str, optional): The alias to remove.
+ The channel will be reverse-determined from the
+ alias, if it exists.
+
+ Returns:
+ bool, str: True, None if removal succeeded. If False,
+ the second part is an error string.
+ **kwargs: If given, passed into nicks.get/add.
+
+ Note:
+ This will remove two nicks - the plain channel alias and the templated
+ nick used for easily sending messages to the channel.
+
+ """
+ ifself.caller.nicks.has(alias,category="channel",**kwargs):
+ DefaultChannel.remove_user_channel_alias(self.caller,alias)
+ returnTrue,""
+ returnFalse,"No such alias was defined."
+
+
[docs]defget_channel_aliases(self,channel):
+ """
+ Get a user's aliases for a given channel. The user is retrieved
+ through self.caller.
+
+ Args:
+ channel (Channel): The channel to act on.
+
+ Returns:
+ list: A list of zero, one or more alias-strings.
+
+ """
+ chan_key=channel.key.lower()
+ nicktuples=self.caller.nicks.get(category="channel",return_tuple=True,return_list=True)
+ ifnicktuples:
+ return[tup[2]fortupinnicktuplesiftup[3].lower()==chan_key]
+ return[]
+
+
[docs]defmute_channel(self,channel):
+ """
+ Temporarily mute a channel.
+
+ Args:
+ channel (Channel): The channel to alias.
+
+ Returns:
+ bool, str: True, None if muting successful. If False,
+ the second part is an error string.
+ """
+ ifchannel.mute(self.caller):
+ returnTrue,""
+ returnFalse,f"Channel {channel.key} was already muted."
+
+
[docs]defunmute_channel(self,channel):
+ """
+ Unmute a channel.
+
+ Args:
+ channel (Channel): The channel to alias.
+
+ Returns:
+ bool, str: True, None if unmuting successful. If False,
+ the second part is an error string.
+
+ """
+ ifchannel.unmute(self.caller):
+ returnTrue,""
+ returnFalse,f"Channel {channel.key} was already unmuted."
+
+
[docs]defcreate_channel(self,name,description,typeclass=None,aliases=None):
+ """
+ Create a new channel. Its name must not previously exist
+ (users can alias as needed). Will also connect to the
+ new channel.
+
+ Args:
+ name (str): The new channel name/key.
+ description (str): This is used in listings.
+ aliases (list): A list of strings - alternative aliases for the channel
+ (not to be confused with per-user aliases; these are available for
+ everyone).
+
+ Returns:
+ channel, str: new_channel, "" if creation successful. If False,
+ the second part is an error string.
+
+ """
+ caller=self.caller
+ iftypeclass:
+ typeclass=class_from_module(typeclass)
+ else:
+ typeclass=CHANNEL_DEFAULT_TYPECLASS
+
+ iftypeclass.objects.channel_search(name,exact=True):
+ returnFalse,f"Channel {name} already exists."
+
+ # set up the new channel
+ lockstring="send:all();listen:all();control:id(%s)"%caller.id
+
+ new_chan=create.create_channel(
+ name,aliases=aliases,desc=description,locks=lockstring,typeclass=typeclass)
+ self.sub_to_channel(new_chan)
+ returnnew_chan,""
+
+
[docs]defdestroy_channel(self,channel,message=None):
+ """
+ Destroy an existing channel. Access should be checked before
+ calling this function.
+
+ Args:
+ channel (Channel): The channel to alias.
+ message (str, optional): Final message to send onto the channel
+ before destroying it. If not given, a default message is
+ used. Set to the empty string for no message.
+
+ if typeclass:
+ pass
+
+ """
+ caller=self.caller
+
+ channel_key=channel.key
+ ifmessageisNone:
+ message=(f"|rChannel {channel_key} is being destroyed. "
+ "Make sure to clean any channel aliases.|n")
+ ifmessage:
+ channel.msg(message,senders=caller,bypass_mute=True)
+ channel.delete()
+ logger.log_sec(
+ "Channel {} was deleted by {}".format(channel_key,caller)
+ )
+
+
[docs]defset_lock(self,channel,lockstring):
+ """
+ Set a lockstring on a channel. Permissions must have been
+ checked before this call.
+
+ Args:
+ channel (Channel): The channel to operate on.
+ lockstring (str): A lockstring on the form 'type:lockfunc();...'
+
+ Returns:
+ bool, str: True, None if setting lock was successful. If False,
+ the second part is an error string.
+
+ """
+ try:
+ channel.locks.add(lockstring)
+ exceptLockExceptionaserr:
+ returnFalse,err
+ returnTrue,""
+
+
[docs]defunset_lock(self,channel,lockstring):
+ """
+ Remove locks in a lockstring on a channel. Permissions must have been
+ checked before this call.
+
+ Args:
+ channel (Channel): The channel to operate on.
+ lockstring (str): A lockstring on the form 'type:lockfunc();...'
+
+ Returns:
+ bool, str: True, None if setting lock was successful. If False,
+ the second part is an error string.
+
+ """
+ try:
+ channel.locks.remove(lockstring)
+ exceptLockExceptionaserr:
+ returnFalse,err
+ returnTrue,""
+
+
[docs]defset_desc(self,channel,description):
+ """
+ Set a channel description. This is shown in listings etc.
+
+ Args:
+ caller (Object or Account): The entity performing the action.
+ channel (Channel): The channel to operate on.
+ description (str): A short description of the channel.
+
+ Returns:
+ bool, str: True, None if setting lock was successful. If False,
+ the second part is an error string.
+
+ """
+ channel.db.desc=description
+
+
[docs]defboot_user(self,channel,target,quiet=False,reason=""):
+ """
+ Boot a user from a channel, with optional reason. This will
+ also remove all their aliases for this channel.
+
+ Args:
+ channel (Channel): The channel to operate on.
+ target (Object or Account): The entity to boot.
+ quiet (bool, optional): Whether or not to announce to channel.
+ reason (str, optional): A reason for the boot.
+
+ Returns:
+ bool, str: True, None if setting lock was successful. If False,
+ the second part is an error string.
+
+ """
+ ifnotchannel.subscriptions.has(target):
+ returnFalse,f"{target} is not connected to channel {channel.key}."
+ # find all of target's nicks linked to this channel and delete them
+ fornickin[
+ nick
+ fornickintarget.nicks.get(category="channel")or[]
+ ifnick.value[3].lower()==channel.key
+ ]:
+ nick.delete()
+ channel.disconnect(target)
+ reason=f" Reason: {reason}"ifreasonelse""
+ target.msg(f"You were booted from channel {channel.key} by {self.caller.key}.{reason}")
+ ifnotquiet:
+ channel.msg(f"{target.key} was booted from channel by {self.caller.key}.{reason}")
+
+ logger.log_sec(f"Channel Boot: {target} (Channel: {channel}, "
+ f"Reason: {reason.strip()}, Caller: {self.caller}")
+ returnTrue,""
+
+
[docs]defban_user(self,channel,target,quiet=False,reason=""):
+ """
+ Ban a user from a channel, by locking them out. This will also
+ boot them, if they are currently connected.
+
+ Args:
+ channel (Channel): The channel to operate on.
+ target (Object or Account): The entity to ban
+ quiet (bool, optional): Whether or not to announce to channel.
+ reason (str, optional): A reason for the ban
+
+ Returns:
+ bool, str: True, None if banning was successful. If False,
+ the second part is an error string.
+
+ """
+ self.boot_user(channel,target,quiet=quiet,reason=reason)
+ ifchannel.ban(target):
+ returnTrue,""
+ returnFalse,f"{target} is already banned from this channel."
+
+
[docs]defunban_user(self,channel,target):
+ """
+ Un-Ban a user from a channel. This will not reconnect them
+ to the channel, just allow them to connect again (assuming
+ they have the suitable 'listen' lock like everyone else).
+
+ Args:
+ channel (Channel): The channel to operate on.
+ target (Object or Account): The entity to unban
+
+ Returns:
+ bool, str: True, None if unbanning was successful. If False,
+ the second part is an error string.
+
+ """
+ ifchannel.unban(target):
+ returnTrue,""
+ returnFalse,f"{target} was not previously banned from this channel."
+
+
[docs]defchannel_list_bans(self,channel):
+ """
+ Show a channel's bans.
+
+ Args:
+ channel (Channel): The channel to operate on.
+
+ Returns:
+ list: A list of strings, each the name of a banned user.
+
+ """
+ return[banned.keyforbannedinchannel.banlist]
+
+
[docs]defchannel_list_who(self,channel):
+ """
+ Show a list of online people is subscribing to a channel. This will check
+ the 'control' permission of `caller` to determine if only online users
+ should be returned or everyone.
+
+ Args:
+ channel (Channel): The channel to operate on.
+
+ Returns:
+ list: A list of prepared strings, with name + markers for if they are
+ muted or offline.
+
+ """
+ caller=self.caller
+ mute_list=list(channel.mutelist)
+ online_list=channel.subscriptions.online()
+ ifchannel.access(caller,'control'):
+ # for those with channel control, show also offline users
+ all_subs=list(channel.subscriptions.all())
+ else:
+ # for others, only show online users
+ all_subs=online_list
+
+ who_list=[]
+ forsubscriberinall_subs:
+ name=subscriber.get_display_name(caller)
+ conditions=("muting"ifsubscriberinmute_listelse"",
+ "offline"ifsubscribernotinonline_listelse"")
+ conditions=[condforcondinconditionsifcond]
+ cond_text="("+", ".join(conditions)+")"ifconditionselse""
+ who_list.append(f"{name}{cond_text}")
+
+ returnwho_list
+
+
[docs]deflist_channels(self,channelcls=CHANNEL_DEFAULT_TYPECLASS):
+ """
+ Return a available channels.
+
+ Args:
+ channelcls (Channel, optional): The channel-class to query on. Defaults
+ to the default channel class from settings.
+
+ Returns:
+ tuple: A tuple `(subbed_chans, available_chans)` with the channels
+ currently subscribed to, and those we have 'listen' access to but
+ don't actually sub to yet.
+
+ """
+ caller=self.caller
+ subscribed_channels=list(channelcls.objects.get_subscriptions(caller))
+ unsubscribed_available_channels=[
+ chan
+ forchaninchannelcls.objects.get_all_channels()
+ ifchannotinsubscribed_channelsandchan.access(caller,"listen")
+ ]
+ returnsubscribed_channels,unsubscribed_available_channels
[docs]deffunc(self):
+ """
+ Main functionality of command.
+ """
+ # from evennia import set_trace;set_trace()
+
+ caller=self.caller
+ switches=self.switches
+ channel_names=[namefornameinself.lhslistifname]
+
+ #from evennia import set_trace;set_trace()
+
+ if'all'inswitches:
+ # show all available channels
+ subscribed,available=self.list_channels()
+ table=self.display_all_channels(subscribed,available)
+
+ self.msg(
+ "\n|wAvailable channels|n (use no argument to "
+ f"only show your subscriptions)\n{table}")
+ return
+
+ ifnotchannel_names:
+ # empty arg show only subscribed channels
+ subscribed,_=self.list_channels()
+ table=self.display_subbed_channels(subscribed)
+
+ self.msg("\n|wChannel subscriptions|n "
+ f"(use |w/all|n to see all available):\n{table}")
+ return
+
+ ifnotself.switchesandnotself.args:
+ self.msg("Usage[/switches]: channel [= message]")
+ return
+
+ if'create'inswitches:
+ # create a new channel
+
+ ifnotself.access(caller,"manage"):
+ self.msg("You don't have access to use channel/create.")
+ return
+
+ config=self.lhs
+ ifnotconfig:
+ self.msg("To create: channel/create name[;aliases][:typeclass] [= description]")
+ return
+ name,*typeclass=config.rsplit(":",1)
+ typeclass=typeclass[0]iftypeclasselseNone
+ name,*aliases=name.rsplit(";")
+ description=self.rhsor""
+ chan,err=self.create_channel(name,description,typeclass=typeclass,aliases=aliases)
+ ifchan:
+ self.msg(f"Created (and joined) new channel '{chan.key}'.")
+ else:
+ self.msg(err)
+ return
+
+ if'unalias'inswitches:
+ # remove a personal alias (no channel needed)
+ alias=self.args.strip()
+ ifnotalias:
+ self.msg("Specify the alias to remove as channel/unalias <alias>")
+ return
+ success,err=self.remove_alias(alias)
+ ifsuccess:
+ self.msg(f"Removed your channel alias '{alias}'.")
+ else:
+ self.msg(err)
+ return
+
+ possible_lhs_message=""
+ ifnotself.rhsandself.argsand" "inself.args:
+ # since we want to support messaging with `channel name text` (for
+ # channels without a space in their name), we need to check if the
+ # first 'channel name' is in fact 'channelname text'
+ no_rhs_channel_name=self.args.split(" ",1)[0]
+ possible_lhs_message=self.args[len(no_rhs_channel_name):]
+ ifpossible_lhs_message.strip()=='=':
+ possible_lhs_message=""
+ channel_names.append(no_rhs_channel_name)
-
[docs]classCmdAddCom(COMMAND_DEFAULT_CLASS):
+ channels=[]
+ errors=[]
+ forchannel_nameinchannel_names:
+ # find a channel by fuzzy-matching. This also checks
+ # 'listen/control' perms.
+ found_channels=self.search_channel(channel_name,exact=False,handle_errors=False)
+ ifnotfound_channels:
+ errors.append(f"No channel found matching '{channel_name}' "
+ "(could also be due to missing access).")
+ eliflen(found_channels)>1:
+ errors.append("Multiple possible channel matches/alias for "
+ "'{channel_name}':\n"+", ".join(chan.keyforchaninfound_channels))
+ else:
+ channels.append(found_channels[0])
+
+ ifnotchannels:
+ self.msg('\n'.join(errors))
+ return
+
+ # we have at least one channel at this point
+ channel=channels[0]
+
+ ifnotswitches:
+ ifself.rhs:
+ # send message to channel
+ self.msg_channel(channel,self.rhs.strip())
+ elifchannelandpossible_lhs_message:
+ # called on the form channelname message without =
+ self.msg_channel(channel,possible_lhs_message.strip())
+ else:
+ # inspect a given channel
+ subscribed,available=self.list_channels()
+ ifchannelinsubscribed:
+ table=self.display_subbed_channels([channel])
+ header=f"Channel |w{channel.key}|n"
+ self.msg(f"{header}\n(use |w{channel.key} <msg>|n (or a channel-alias) "
+ f"to chat and the 'channel' command "
+ f"to customize)\n{table}")
+ elifchannelinavailable:
+ table=self.display_all_channels([],[channel])
+ self.msg(
+ "\n|wNot subscribed to this channel|n (use /list to "
+ f"show all subscriptions)\n{table}")
+ return
+
+ if'history'inswitchesor'hist'inswitches:
+ # view channel history
+
+ index=self.rhsor0
+ try:
+ index=max(0,int(index))
+ exceptValueError:
+ self.msg("The history index (describing how many lines to go back) "
+ "must be an integer >= 0.")
+ return
+ self.get_channel_history(channel,start_index=index)
+ return
+
+ if'sub'inswitches:
+ # subscribe to a channel
+ aliases=[]
+ ifself.rhs:
+ aliases=set(alias.strip().lower()foraliasinself.rhs.split(";"))
+ success,err=self.sub_to_channel(channel)
+ ifsuccess:
+ foraliasinaliases:
+ self.add_alias(channel,alias)
+ alias_txt=', '.join(aliases)
+ alias_txt=f" using alias(es) {alias_txt}"ifaliaseselse''
+ self.msg("You are now subscribed "
+ f"to the channel {channel.key}{alias_txt}. Use /alias to "
+ "add additional aliases for referring to the channel.")
+ else:
+ self.msg(err)
+ return
+
+ if'unsub'inswitches:
+ # un-subscribe from a channel
+ success,err=self.unsub_from_channel(channel)
+ ifsuccess:
+ self.msg(f"You un-subscribed from channel {channel.key}. "
+ "All aliases were cleared.")
+ else:
+ self.msg(err)
+ return
+
+ if'alias'inswitches:
+ # create a new personal alias for a channel
+ alias=self.rhs
+ ifnotalias:
+ self.msg("Specify the alias as channel/alias channelname = alias")
+ return
+ self.add_alias(channel,alias)
+ self.msg(f"Added/updated your alias '{alias}' for channel {channel.key}.")
+ return
+
+ if'mute'inswitches:
+ # mute a given channel
+ success,err=self.mute_channel(channel)
+ ifsuccess:
+ self.msg(f"Muted channel {channel.key}.")
+ else:
+ self.msg(err)
+ return
+
+ if'unmute'inswitches:
+ # unmute a given channel
+ success,err=self.unmute_channel(channel)
+ ifsuccess:
+ self.msg(f"Un-muted channel {channel.key}.")
+ else:
+ self.msg(err)
+ return
+
+ if'destroy'inswitchesor'delete'inswitches:
+ # destroy a channel we control
+
+ ifnotself.access(caller,"manage"):
+ self.msg("You don't have access to use channel/destroy.")
+ return
+
+ ifnotchannel.access(caller,"control"):
+ self.msg("You can only delete channels you control.")
+ return
+
+ reason=self.rhsorNone
+
+ def_perform_delete(caller,*args,**kwargs):
+ self.destroy_channel(channel,message=reason)
+ self.msg(f"Channel {channel.key} was successfully deleted.")
+
+ ask_yes_no(
+ caller,
+ prompt=f"Are you sure you want to delete channel '{channel.key}' "
+ "(make sure name is correct!)?\nThis will disconnect and "
+ "remove all users' aliases. {options}?",
+ yes_action=_perform_delete,
+ no_action="Aborted.",
+ default="N"
+ )
+
+ if'desc'inswitches:
+ # set channel description
+
+ ifnotself.access(caller,"manage"):
+ self.msg("You don't have access to use channel/desc.")
+ return
+
+ ifnotchannel.access(caller,"control"):
+ self.msg("You can only change description of channels you control.")
+ return
+
+ desc=self.rhs.strip()
+
+ ifnotdesc:
+ self.msg("Usage: /desc channel = description")
+ return
+
+ self.set_desc(channel,desc)
+ self.msg("Updated channel description.")
+
+ if'lock'inswitches:
+ # add a lockstring to channel
+
+ ifnotself.access(caller,"changelocks"):
+ self.msg("You don't have access to use channel/lock.")
+ return
+
+ ifnotchannel.access(caller,"control"):
+ self.msg("You need 'control'-access to change locks on this channel.")
+ return
+
+ lockstring=self.rhs.strip()
+
+ ifnotlockstring:
+ self.msg("Usage: channel/lock channelname = lockstring")
+ return
+
+ success,err=self.set_lock(channel,self.rhs)
+ ifsuccess:
+ self.msg("Added/updated lock on channel.")
+ else:
+ self.msg(f"Could not add/update lock: {err}")
+ return
+
+ if'unlock'inswitches:
+ # remove/update lockstring from channel
+
+ ifnotself.access(caller,"changelocks"):
+ self.msg("You don't have access to use channel/unlock.")
+ return
+
+ ifnotchannel.access(caller,"control"):
+ self.msg("You need 'control'-access to change locks on this channel.")
+ return
+
+ lockstring=self.rhs.strip()
+
+ ifnotlockstring:
+ self.msg("Usage: channel/unlock channelname = lockstring")
+ return
+
+ success,err=self.unset_lock(channel,self.rhs)
+ ifsuccess:
+ self.msg("Removed lock from channel.")
+ else:
+ self.msg(f"Could not remove lock: {err}")
+ return
+
+ if'boot'inswitches:
+ # boot a user from channel(s)
+
+ ifnotself.access(caller,"admin"):
+ self.msg("You don't have access to use channel/boot.")
+ return
+
+ ifnotself.rhs:
+ self.msg("Usage: channel/boot channel[,channel,...] = username [:reason]")
+ return
+
+ target_str,*reason=self.rhs.rsplit(":",1)
+ reason=reason[0].strip()ifreasonelse""
+
+ forchaninchannels:
+
+ ifnotchan.access(caller,"control"):
+ self.msg(f"You need 'control'-access to boot a user from {chan.key}.")
+ return
+
+ # the target must be a member of all given channels
+ target=caller.search(target_str,candidates=chan.subscriptions.all())
+ ifnottarget:
+ self.msg(f"Cannot boot '{target_str}' - not in channel {chan.key}.")
+ return
+
+ def_boot_user(caller,*args,**kwargs):
+ forchaninchannels:
+ success,err=self.boot_user(chan,target,quiet=False,reason=reason)
+ ifsuccess:
+ self.msg(f"Booted {target.key} from channel {chan.key}.")
+ else:
+ self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}")
+
+ channames=", ".join(chan.keyforchaninchannels)
+ reasonwarn=(". Also note that your reason will be echoed to the channel"
+ ifreasonelse'')
+ ask_yes_no(
+ caller,
+ prompt=f"Are you sure you want to boot user {target.key} from "
+ f"channel(s) {channames} (make sure name/channels are correct{reasonwarn}). "
+ "{options}?",
+ yes_action=_boot_user,
+ no_action="Aborted.",
+ default="Y"
+ )
+ return
+
+ if'ban'inswitches:
+ # ban a user from channel(s)
+
+ ifnotself.access(caller,"admin"):
+ self.msg("You don't have access to use channel/ban.")
+ return
+
+ ifnotself.rhs:
+ # view bans for channels
+
+ ifnotchannel.access(caller,"control"):
+ self.msg(f"You need 'control'-access to view bans on channel {channel.key}")
+ return
+
+ bans=["Channel bans "
+ "(to ban, use channel/ban channel[,channel,...] = username [:reason]"]
+ bans.extend(self.channel_list_bans(channel))
+ self.msg("\n".join(bans))
+ return
+
+ target_str,*reason=self.rhs.rsplit(":",1)
+ reason=reason[0].strip()ifreasonelse""
+
+ forchaninchannels:
+ # the target must be a member of all given channels
+ ifnotchan.access(caller,"control"):
+ self.msg(f"You don't have access to ban users on channel {chan.key}")
+ return
+
+ target=caller.search(target_str,candidates=chan.subscriptions.all())
+
+ ifnottarget:
+ self.msg(f"Cannot ban '{target_str}' - not in channel {chan.key}.")
+ return
+
+ def_ban_user(caller,*args,**kwargs):
+ forchaninchannels:
+ success,err=self.ban_user(chan,target,quiet=False,reason=reason)
+ ifsuccess:
+ self.msg(f"Banned {target.key} from channel {chan.key}.")
+ else:
+ self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}")
+
+ channames=", ".join(chan.keyforchaninchannels)
+ reasonwarn=(". Also note that your reason will be echoed to the channel"
+ ifreasonelse'')
+ ask_yes_no(
+ caller,
+ f"Are you sure you want to ban user {target.key} from "
+ f"channel(s) {channames} (make sure name/channels are correct{reasonwarn}) "
+ "{options}?",
+ _ban_user,
+ "Aborted.",
+ )
+ return
+
+ if'unban'inswitches:
+ # unban a previously banned user from channel
+
+ ifnotself.access(caller,"admin"):
+ self.msg("You don't have access to use channel/unban.")
+ return
+
+ target_str=self.rhs.strip()
+
+ ifnottarget_str:
+ self.msg("Usage: channel[,channel,...] = user")
+ return
+
+ banlists=[]
+ forchaninchannels:
+ # the target must be a member of all given channels
+ ifnotchan.access(caller,"control"):
+ self.msg(f"You don't have access to unban users on channel {chan.key}")
+ return
+ banlists.extend(chan.banlist)
+
+ target=caller.search(target_str,candidates=banlists)
+ ifnottarget:
+ self.msg("Could not find a banned user '{target_str}' in given channel(s).")
+ return
+
+ forchaninchannels:
+ success,err=self.unban_user(channel,target)
+ ifsuccess:
+ self.msg(f"Un-banned {target_str} from channel {chan.key}")
+ else:
+ self.msg(err)
+ return
+
+ if"who"inswitches:
+ # view who's a member of a channel
+
+ who_list=[f"Subscribed to {channel.key}:"]
+ who_list.extend(self.channel_list_who(channel))
+ self.msg("\n".join(who_list))
+ return
+
+
+# a channel-command parent for use with Characters/Objects.
+classCmdObjectChannel(CmdChannel):
+ account_caller=False
+
+
+
[docs]classCmdAddCom(CmdChannel):"""
- add a channel alias and/or subscribe to a channel
+ Add a channel alias and/or subscribe to a channel Usage: addcom [alias=] <channel>
@@ -139,7 +1317,6 @@
caller=self.callerargs=self.args
- account=callerifnotargs:self.msg("Usage: addcom [alias =] channelname.")
@@ -153,42 +1330,36 @@
channelname=self.argsalias=None
- channel=find_channel(caller,channelname)
+ channel=self.search_channel(channelname)ifnotchannel:
- # we use the custom search method to handle errors.
- return
-
- # check permissions
- ifnotchannel.access(account,"listen"):
- self.msg("%s: You are not allowed to listen to this channel."%channel.key)returnstring=""
- ifnotchannel.has_connection(account):
+ ifnotchannel.has_connection(caller):# we want to connect as well.
- ifnotchannel.connect(account):
+ success,err=self.sub_to_channel(channel)
+ ifsuccess:# if this would have returned True, the account is connected
- self.msg("%s: You are not allowed to join this channel."%channel.key)
+ self.msg(f"You now listen to the channel {channel.key}")
+ else:
+ self.msg(f"{channel.key}: You are not allowed to join this channel.")return
- else:
- string+="You now listen to the channel %s. "%channel.key
+
+ ifchannel.unmute(caller):
+ self.msg(f"You unmute channel {channel.key}.")else:
- ifchannel.unmute(account):
- string+="You unmute channel %s."%channel.key
- else:
- string+="You are already connected to channel %s."%channel.key
+ self.msg(f"You are already connected to channel {channel.key}.")ifalias:# create a nick and add it to the caller.
- caller.nicks.add(alias,channel.key,category="channel")
- string+=" You can now refer to the channel %s with the alias '%s'."
- self.msg(string%(channel.key,alias))
+ self.add_alias(channel,alias)
+ self.msg(f" You can now refer to the channel {channel} with the alias '{alias}'.")else:string+=" No alias added."self.msg(string)
[docs]classCmdDelCom(CmdChannel):""" remove a channel alias and/or unsubscribe from channel
@@ -214,49 +1385,42 @@
"""Implementing the command. """caller=self.caller
- account=callerifnotself.args:self.msg("Usage: delcom <alias or channel>")return
- ostring=self.args.lower()
+ ostring=self.args.lower().strip()
- channel=find_channel(caller,ostring,silent=True,noaliases=True)
- ifchannel:
- # we have given a channel name - unsubscribe
- ifnotchannel.has_connection(account):
- self.msg("You are not listening to that channel.")
- return
- chkey=channel.key.lower()
+ channel=self.search_channel(ostring)
+ ifnotchannel:
+ return
+
+ ifnotchannel.has_connection(caller):
+ self.msg("You are not listening to that channel.")
+ return
+
+ ifostring==channel.key.lower():
+ # an exact channel name - unsubscribedelnicks="all"inself.switches# find all nicks linked to this channel and delete themifdelnicks:
- fornickin[
- nick
- fornickinmake_iter(caller.nicks.get(category="channel",return_obj=True))
- ifnickandnick.pkandnick.value[3].lower()==chkey
- ]:
- nick.delete()
- disconnect=channel.disconnect(account)
- ifdisconnect:
+ aliases=self.get_channel_aliases(channel)
+ foraliasinaliases:
+ self.remove_alias(alias)
+ success,err=self.unsub_from_channel(channel)
+ ifsuccess:wipednicks=" Eventual aliases were removed."ifdelnickselse""
- self.msg("You stop listening to channel '%s'.%s"%(channel.key,wipednicks))
+ self.msg(f"You stop listening to channel '{channel.key}'.{wipednicks}")
+ else:
+ self.msg(err)returnelse:# we are removing a channel nick
- channame=caller.nicks.get(key=ostring,category="channel")
- channel=find_channel(caller,channame,silent=True)
- ifnotchannel:
- self.msg("No channel with alias '%s' was found."%ostring)
- else:
- ifcaller.nicks.get(ostring,category="channel"):
- caller.nicks.remove(ostring,category="channel")
- self.msg("Your alias '%s' for channel %s was cleared."%(ostring,channel.key))
- else:
- self.msg("You had no such alias defined for this channel.")
+ self.remove_alias(ostring)
+ self.msg(f"Any alias '{ostring}' for channel {channel.key} was cleared.")
[docs]classCmdAllCom(CmdChannel):""" perform admin operations on all channels
@@ -271,6 +1435,7 @@
"""key="allcom"
+ aliases=[]# important to not inherit parent's aliaseslocks="cmd: not pperm(channel_banned)"help_category="Comms"
@@ -283,8 +1448,11 @@
caller=self.callerargs=self.argsifnotargs:
- self.execute_cmd("channels")
- self.msg("(Usage: allcom on | off | who | destroy)")
+ subscribed,available=self.list_channels()
+ table=self.display_all_channels(subscribed,available)
+ self.msg(
+ "\n|wAvailable channels:\n{table}")
+ returnreturnifargs=="on":
@@ -328,125 +1496,7 @@
# wrong inputself.msg("Usage: allcom on | off | who | clear")
-
-
[docs]classCmdChannels(COMMAND_DEFAULT_CLASS):
- """
- list all channels available to you
-
- Usage:
- channels
- clist
- comlist
-
- Lists all channels available to you, whether you listen to them or not.
- Use 'comlist' to only view your current channel subscriptions.
- Use addcom/delcom to join and leave channels
- """
-
- key="channels"
- aliases=["clist","comlist","chanlist","channellist","all channels"]
- help_category="Comms"
- locks="cmd: not pperm(channel_banned)"
-
- # this is used by the COMMAND_DEFAULT_CLASS parent
- account_caller=True
-
-
[docs]deffunc(self):
- """Implement function"""
-
- caller=self.caller
-
- # all channels we have available to listen to
- channels=[
- chan
- forchaninCHANNEL_DEFAULT_TYPECLASS.objects.get_all_channels()
- ifchan.access(caller,"listen")
- ]
- ifnotchannels:
- self.msg("No channels available.")
- return
- # all channel we are already subscribed to
- subs=CHANNEL_DEFAULT_TYPECLASS.objects.get_subscriptions(caller)
-
- ifself.cmdstring=="comlist":
- # just display the subscribed channels with no extra info
- comtable=self.styled_table(
- "|wchannel|n",
- "|wmy aliases|n",
- "|wdescription|n",
- align="l",
- maxwidth=_DEFAULT_WIDTH,
- )
- forchaninsubs:
- clower=chan.key.lower()
- nicks=caller.nicks.get(category="channel",return_obj=True)
- comtable.add_row(
- *[
- "%s%s"
- %(
- chan.key,
- chan.aliases.all()and"(%s)"%",".join(chan.aliases.all())or"",
- ),
- "%s"
- %",".join(
- nick.db_key
- fornickinmake_iter(nicks)
- ifnickandnick.value[3].lower()==clower
- ),
- chan.db.desc,
- ]
- )
- self.msg(
- "\n|wChannel subscriptions|n (use |wchannels|n to list all,"
- " |waddcom|n/|wdelcom|n to sub/unsub):|n\n%s"%comtable
- )
- else:
- # full listing (of channels caller is able to listen to)
- comtable=self.styled_table(
- "|wsub|n",
- "|wchannel|n",
- "|wmy aliases|n",
- "|wlocks|n",
- "|wdescription|n",
- maxwidth=_DEFAULT_WIDTH,
- )
- forchaninchannels:
- clower=chan.key.lower()
- nicks=caller.nicks.get(category="channel",return_obj=True)
- nicks=nicksor[]
- ifchannotinsubs:
- substatus="|rNo|n"
- elifcallerinchan.mutelist:
- substatus="|rMuted|n"
- else:
- substatus="|gYes|n"
- comtable.add_row(
- *[
- substatus,
- "%s%s"
- %(
- chan.key,
- chan.aliases.all()and"(%s)"%",".join(chan.aliases.all())or"",
- ),
- "%s"
- %",".join(
- nick.db_key
- fornickinmake_iter(nicks)
- ifnick.value[3].lower()==clower
- ),
- str(chan.locks),
- chan.db.desc,
- ]
- )
- comtable.reformat_column(0,width=9)
- comtable.reformat_column(3,width=14)
- self.msg(
- "\n|wAvailable channels|n (use |wcomlist|n,|waddcom|n and |wdelcom|n"
- " to manage subscriptions):\n%s"%comtable
- )
[docs]classCmdCdestroy(CmdChannel):""" destroy a channel you created
@@ -457,6 +1507,7 @@
"""key="cdestroy"
+ aliases=[]help_category="Comms"locks="cmd: not pperm(channel_banned)"
@@ -465,12 +1516,15 @@
[docs]deffunc(self):"""Destroy objects cleanly."""
+
caller=self.callerifnotself.args:self.msg("Usage: cdestroy <channelname>")return
- channel=find_channel(caller,self.args)
+
+ channel=self.search_channel(self.args)
+
ifnotchannel:self.msg("Could not find channel %s."%self.args)return
@@ -478,11 +1532,8 @@
self.msg("You are not allowed to do that.")returnchannel_key=channel.key
- message="%s is being destroyed. Make sure to change your aliases."%channel_key
- msgobj=create.create_message(caller,message,channel)
- channel.msg(msgobj)
- channel.delete()
- CHANNELHANDLER.update()
+ message=f"{channel.key} is being destroyed. Make sure to change your aliases."
+ self.destroy_channel(channel,message)self.msg("Channel '%s' was destroyed."%channel_key)logger.log_sec("Channel Deleted: %s (Caller: %s, IP: %s)."
@@ -490,7 +1541,7 @@
)
[docs]classCmdCemit(COMMAND_DEFAULT_CLASS):
- """
- send an admin message to a channel you control
-
- Usage:
- cemit[/switches] <channel> = <message>
-
- Switches:
- sendername - attach the sender's name before the message
- quiet - don't echo the message back to sender
-
- Allows the user to broadcast a message over a channel as long as
- they control it. It does not show the user's name unless they
- provide the /sendername switch.
-
- """
-
- key="cemit"
- aliases=["cmsg"]
- switch_options=("sendername","quiet")
- locks="cmd: not pperm(channel_banned) and pperm(Player)"
- help_category="Comms"
-
- # this is used by the COMMAND_DEFAULT_CLASS parent
- account_caller=True
-
-
[docs]classCmdClock(CmdChannel):""" change channel locks of a channel you control
@@ -708,8 +1699,8 @@
"""key="clock"
- locks="cmd:not pperm(channel_banned)"aliases=["clock"]
+ locks="cmd:not pperm(channel_banned) and perm(Admin)"help_category="Comms"# this is used by the COMMAND_DEFAULT_CLASS parent
@@ -723,14 +1714,13 @@
self.msg(string)return
- channel=find_channel(self.caller,self.lhs)
+ channel=self.search_channel(self.lhs)ifnotchannel:return
+
ifnotself.rhs:# no =, so just view the current locks
- string="Current locks on %s:"%channel.key
- string="%s\n%s"%(string,channel.locks)
- self.msg(string)
+ self.msg(f"Current locks on {channel.key}\n{channel.locks}")return# we want to add/change a lock.ifnotchannel.access(self.caller,"control"):
@@ -738,18 +1728,13 @@
self.msg(string)return# Try to add the lock
- try:
- channel.locks.add(self.rhs)
- exceptLockExceptionaserr:
- self.msg(err)
- return
- string="Lock(s) applied. "
- string+="Current locks on %s:"%channel.key
- string="%s\n%s"%(string,channel.locks)
- self.msg(string)
+ success,err=self.set_lock(channel,self.rhs)
+ ifsuccess:
+ self.msg(f"Lock(s) applied. Current locks on {channel.key}:\n{channel.locks}")
+ else:
+ self.msg(err)
[docs]classCmdCdesc(CmdChannel):""" describe a channel you control
@@ -758,9 +1743,11 @@
Changes the description of the channel as shown in channel lists.
+
"""key="cdesc"
+ aliases=[]locks="cmd:not pperm(channel_banned)"help_category="Comms"
@@ -775,18 +1762,15 @@
ifnotself.rhs:self.msg("Usage: cdesc <channel> = <description>")return
- channel=find_channel(caller,self.lhs)
+ channel=self.search_channel(self.lhs)ifnotchannel:
- self.msg("Channel '%s' not found."%self.lhs)return# check permissionsifnotchannel.access(caller,"control"):self.msg("You cannot admin this channel.")return
- # set the description
- channel.db.desc=self.rhs
- channel.save()
- self.msg("Description of channel '%s' set to '%s'."%(channel.key,self.rhs))
+ self.set_desc(channel,self.rhs)
+ self.msg(f"Description of channel '{channel.key}' set to '{self.rhs}'.")
[docs]classCmdPage(COMMAND_DEFAULT_CLASS):
@@ -794,16 +1778,19 @@
send a private message to another account Usage:
+ page <account> <message> page[/switches] [<account>,<account>,... = <message>] tell '' page <number>
- Switch:
+ Switches: last - shows who you last messaged list - show your last <number> of tells/pages (default)
- Send a message to target user (if online). If no
- argument is given, you will get a list of your latest messages.
+ Send a message to target user (if online). If no argument is given, you
+ will get a list of your latest messages. The equal sign is needed for
+ multiple targets or if sending to target with space in the name.
+
"""key="page"
@@ -822,9 +1809,10 @@
caller=self.caller# get the messages we've sent (not to channels)
- pages_we_sent=Msg.objects.get_messages_by_sender(caller,exclude_channel_messages=True)
+ pages_we_sent=Msg.objects.get_messages_by_sender(caller)# get last messages we've gotpages_we_got=Msg.objects.get_messages_by_receiver(caller)
+ targets,message,number=[],None,Noneif"last"inself.switches:ifpages_we_sent:
@@ -835,19 +1823,76 @@
self.msg("You haven't paged anyone yet.")return
- ifnotself.argsornotself.rhs:
- pages=pages_we_sent+pages_we_got
- pages=sorted(pages,key=lambdapage:page.date_created)
+ ifself.args:
+ ifself.rhs:
+ fortargetinself.lhslist:
+ target_obj=self.caller.search(target)
+ ifnottarget_obj:
+ return
+ targets.append(target_obj)
+ message=self.rhs.strip()
+ else:
+ target,*message=self.args.split(" ",4)
+ iftargetandtarget.isnumeric():
+ # a number to specify a historic page
+ number=int(target)
+ eliftarget:
+ target_obj=self.caller.search(target,quiet=True)
+ iftarget_obj:
+ # a proper target
+ targets=[target_obj[0]]
+ message=message[0].strip()
+ else:
+ # a message with a space in it - put it back together
+ message=target+" "+(message[0]ifmessageelse"")
+ else:
+ # a single-word message
+ message=message[0].strip()
- number=5
- ifself.args:
- try:
- number=int(self.args)
- exceptValueError:
- self.msg("Usage: tell [<account> = msg]")
+ pages=list(pages_we_sent)+list(pages_we_got)
+ pages=sorted(pages,key=lambdapage:page.date_created)
+
+ ifmessage:
+ # send a message
+ ifnottargets:
+ # no target given - send to last person we paged
+ ifpages_we_sent:
+ targets=pages_we_sent[-1].receivers
+ else:
+ self.msg("Who do you want page?")return
- iflen(pages)>number:
+ header="|wAccount|n |c%s|n |wpages:|n"%caller.key
+ ifmessage.startswith(":"):
+ message="%s%s"%(caller.key,message.strip(":").strip())
+
+ # create the persistent message object
+ create.create_message(caller,message,receivers=targets)
+
+ # tell the accounts they got a message.
+ received=[]
+ rstrings=[]
+ fortargetintargets:
+ ifnottarget.access(caller,"msg"):
+ rstrings.append(f"You are not allowed to page {target}.")
+ continue
+ target.msg(f"{header}{message}")
+ ifhasattr(target,"sessions")andnottarget.sessions.count():
+ received.append(f"|C{target.name}|n")
+ rstrings.append(
+ f"{received[-1]} is offline. They will see your message "
+ "if they list their pages later."
+ )
+ else:
+ received.append(f"|c{target.name}|n")
+ ifrstrings:
+ self.msg("\n".join(rstrings))
+ self.msg("You paged %s with: '%s'."%(", ".join(received),message))
+ return
+
+ else:
+ # no message to send
+ ifnumberisnotNoneandlen(pages)>number:lastpages=pages[-number:]else:lastpages=pages
@@ -876,7 +1921,7 @@
receiver=f"|n,{clr}".join([obj.nameforobjinpage.receivers])ifsending:template=to_template
- sender=f"{sender} "ifmulti_sendelse""
+ sender=f"{sender} "ifmulti_sendelse""receiver=f" {receiver}"ifmulti_recvelsef" {receiver}"else:template=from_template
@@ -900,65 +1945,7 @@
else:string="You haven't paged anyone yet."self.msg(string)
- return
-
- # We are sending. Build a list of targets
-
- ifnotself.lhs:
- # If there are no targets, then set the targets
- # to the last person we paged.
- ifpages_we_sent:
- receivers=pages_we_sent[-1].receivers
- else:
- self.msg("Who do you want to page?")
- return
- else:
- receivers=self.lhslist
-
- recobjs=[]
- forreceiverinset(receivers):
- ifisinstance(receiver,str):
- pobj=caller.search(receiver)
- elifhasattr(receiver,"character"):
- pobj=receiver
- else:
- self.msg("Who do you want to page?")
- return
- ifpobj:
- recobjs.append(pobj)
- ifnotrecobjs:
- self.msg("Noone found to page.")
- return
-
- header="|wAccount|n |c%s|n |wpages:|n"%caller.key
- message=self.rhs
-
- # if message begins with a :, we assume it is a 'page-pose'
- ifmessage.startswith(":"):
- message="%s%s"%(caller.key,message.strip(":").strip())
-
- # create the persistent message object
- create.create_message(caller,message,receivers=recobjs)
-
- # tell the accounts they got a message.
- received=[]
- rstrings=[]
- forpobjinrecobjs:
- ifnotpobj.access(caller,"msg"):
- rstrings.append("You are not allowed to page %s."%pobj)
- continue
- pobj.msg("%s%s"%(header,message))
- ifhasattr(pobj,"sessions")andnotpobj.sessions.count():
- received.append("|C%s|n"%pobj.name)
- rstrings.append(
- "%s is offline. They will see your message if they list their pages later."
- %received[-1]
- )
- else:
- received.append("|c%s|n"%pobj.name)
- ifrstrings:
- self.msg("\n".join(rstrings))
- self.msg("You paged %s with: '%s'."%(", ".join(received),message))
[docs]classCmdIRC2Chan(COMMAND_DEFAULT_CLASS):""" Link an evennia channel to an external IRC channel
@@ -1122,7 +2108,7 @@
Check and reboot IRC bot. Usage:
- ircstatus [#dbref ping||nicklist||reconnect]
+ ircstatus [#dbref ping | nicklist | reconnect] If not given arguments, will return a list of all bots (like irc2chan/list). The 'ping' argument will ping the IRC network to
diff --git a/docs/1.0-dev/_modules/evennia/commands/default/help.html b/docs/1.0-dev/_modules/evennia/commands/default/help.html
index 186c46b0fe..078fa8cfe8 100644
--- a/docs/1.0-dev/_modules/evennia/commands/default/help.html
+++ b/docs/1.0-dev/_modules/evennia/commands/default/help.html
@@ -41,114 +41,75 @@
Source code for evennia.commands.default.help
"""
-The help command. The basic idea is that help texts for commands
-are best written by those that write the commands - the admins. So
-command-help is all auto-loaded and searched from the current command
-set. The normal, database-tied help system is used for collaborative
-creation of other help topics such as RP help or game-world aides.
+The help command. The basic idea is that help texts for commands are best
+written by those that write the commands - the developers. So command-help is
+all auto-loaded and searched from the current command set. The normal,
+database-tied help system is used for collaborative creation of other help
+topics such as RP help or game-world aides. Help entries can also be created
+outside the game in modules given by ``settings.FILE_HELP_ENTRY_MODULES``.
+
"""
+importre
+fromdataclassesimportdataclassfromdjango.confimportsettingsfromcollectionsimportdefaultdictfromevennia.utils.utilsimportfill,dedent
-fromevennia.commands.commandimportCommandfromevennia.help.modelsimportHelpEntryfromevennia.utilsimportcreate,evmorefromevennia.utils.ansiimportANSIString
+fromevennia.help.filehelpimportFILE_HELP_ENTRIESfromevennia.utils.eveditorimportEvEditorfromevennia.utils.utilsimport(
- string_suggestions,class_from_module,inherits_from,
- format_grid,
+ format_grid,pad)
+fromevennia.help.utilsimporthelp_search_with_index,parse_entry_for_subcategoriesCOMMAND_DEFAULT_CLASS=class_from_module(settings.COMMAND_DEFAULT_CLASS)
-HELP_MORE=settings.HELP_MORE
-CMD_IGNORE_PREFIXES=settings.CMD_IGNORE_PREFIXES
+HELP_MORE_ENABLED=settings.HELP_MORE_ENABLED
+DEFAULT_HELP_CATEGORY=settings.DEFAULT_HELP_CATEGORY# limit symbol import for API__all__=("CmdHelp","CmdSetHelp")
-_DEFAULT_WIDTH=settings.CLIENT_DEFAULT_WIDTH
-_SEP="|C"+"-"*_DEFAULT_WIDTH+"|n"
-
-_LUNR=None
-_LUNR_EXCEPTION=None
+@dataclassclassHelpCategory:
- def__init__(self,key):
- self.key=key
+ """
+ Mock 'help entry' to search categories with the same code.
+
+ """
+ key:str@propertydefsearch_index_entry(self):return{
- "key":str(self),
+ "key":self.key,"aliases":"","category":self.key,"tags":"","text":"",}
- def__str__(self):
- returnf"Category: {self.key}"
-
- def__eq__(self,other):
- returnstr(self).lower()==str(other).lower()
-
def__hash__(self):
- returnid(self)
-
-
-defhelp_search_with_index(query,candidate_entries,suggestion_maxnum=5):
- """
- Lunr-powered fast index search and suggestion wrapper
- """
- global_LUNR,_LUNR_EXCEPTION
- ifnot_LUNR:
- # we have to delay-load lunr because it messes with logging if it's imported
- # before twisted's logging has been set up
- fromlunrimportlunras_LUNR
- fromlunr.exceptionsimportQueryParseErroras_LUNR_EXCEPTION
-
- indx=[cnd.search_index_entryforcndincandidate_entries]
- mapping={indx[ix]["key"]:candforix,candinenumerate(candidate_entries)}
-
- search_index=_LUNR(
- ref="key",
- fields=[
- {"field_name":"key","boost":10},
- {"field_name":"aliases","boost":9},
- {"field_name":"category","boost":8},
- {"field_name":"tags","boost":5},
- {"field_name":"text","boost":1},
- ],
- documents=indx,
- )
- try:
- matches=search_index.search(query)[:suggestion_maxnum]
- except_LUNR_EXCEPTION:
- # this is a user-input problem
- matches=[]
-
- # matches (objs), suggestions (strs)
- return(
- [mapping[match["ref"]]formatchinmatches],
- [str(match["ref"])formatchinmatches],# + f" (score {match['score']})") # good debug
- )
+ returnhash(id(self))
[docs]classCmdHelp(COMMAND_DEFAULT_CLASS):"""
- View help or a list of topics
+ Get help. Usage:
- help <topic or command>
- help list
- help all
+ help
+ help <topic, command or category>
+ help <topic>/<subtopic>
+ help <topic>/<subtopic>/<subsubtopic> ...
+
+ Use the 'help' command alone to see an index of all help topics, organized
+ by category.eSome big topics may offer additional sub-topics.
- This will search for help on commands and other
- topics related to the game. """key="help"
@@ -162,8 +123,13 @@
# Help messages are wrapped in an EvMore call (unless using the webclient# with separate help popups) If you want to avoid this, simply add
- # 'HELP_MORE = False' in your settings/conf/settings.py
- help_more=HELP_MORE
+ # 'HELP_MORE_ENABLED = False' in your settings/conf/settings.py
+ help_more=HELP_MORE_ENABLED
+
+ # colors for the help index
+ index_type_separator_clr="|w"
+ index_category_clr="|W"
+ index_topic_clr="|G"# suggestion cutoff, between 0 and 1 (1 => perfect match)suggestion_cutoff=0.6
@@ -171,6 +137,9 @@
# number of suggestions (set to 0 to remove suggestions from help)suggestion_maxnum=5
+ # separator between subtopics:
+ subtopic_separator_char=r"/"
+
[docs]defmsg_help(self,text):""" messages text to the caller, adding an extra oob argument to indicate
@@ -194,65 +163,142 @@
self.msg(text=(text,{"type":"help"}))
[docs]defformat_help_entry(self,topic="",help_text="",aliases=None,suggested=None,
+ subtopics=None):""" This visually formats the help entry. This method can be overriden to customize the way a help entry is displayed. Args:
- title (str): the title of the help entry.
- help_text (str): the text of the help entry.
- aliases (list of str or None): the list of aliases.
- suggested (list of str or None): suggested reading.
+ title (str): The title of the help entry.
+ help_text (str): Text of the help entry.
+ aliases (list): List of help-aliases (displayed in header).
+ suggested (list): Strings suggested reading (based on title).
+ subtopics (list): A list of strings - the subcategories available
+ for this entry. Returns the formatted string, ready to be sent. """
- start=f"{_SEP}\n"
- title=f"|CHelp for |w{title}|n"iftitleelse""
- aliases=(
- " |C(aliases: {}|C)|n".format("|C,|n ".join(f"|w{ali}|n"foraliinaliases))
- ifaliaseselse"")
- help_text=(
- f"\n{dedent(help_text.rstrip())}"ifhelp_textelse"")
- suggested=(
- "\n\n|CSuggested:|n {}".format(
- fill("|C,|n ".join(f"|w{sug}|n"forsuginsuggested)))
- ifsuggestedelse"")
- end=f"\n{_SEP}"
+ separator="|C"+"-"*self.client_width()+"|n"
+ start=f"{separator}\n"
- return"".join((start,title,aliases,help_text,suggested,end))
+ title=f"|CHelp for |w{topic}|n"iftopicelse"|rNo help found|n"
-
[docs]defformat_help_index(self,cmd_help_dict=None,db_help_dict=None,title_lone_category=False):"""
- Output a category-ordered list. The input are the
+ Output a category-ordered g for displaying the main help, grouped by
+ category.
+
+ Args:
+ cmd_help_dict (dict): A dict `{"category": [topic, topic, ...]}` for
+ command-based help.
+ db_help_dict (dict): A dict `{"category": [topic, topic], ...]}` for
+ database-based help.
+ title_lone_category (bool, optional): If a lone category should
+ be titled with the category name or not. While pointless in a
+ general index, the title should probably show when explicitly
+ listing the category itself.
+
+ Returns:
+ str: The help index organized into a grid.
+
+ The input are the pre-loaded help files for commands and database-helpfiles respectively. You can override this method to return a custom display of the list of commands and topics.
+
"""
- category_clr="|w"
- topic_clr="|G"
+ def_group_by_category(help_dict):
+ grid=[]
+ verbatim_elements=[]
+
+ iflen(help_dict)==1andnottitle_lone_category:
+ # don't list categories if there is only one
+ forcategoryinhelp_dict:
+ entries=sorted(set(help_dict.get(category,[])))
+ grid.extend(entries)
+ else:
+ # list the categories
+ forcategoryinsorted(set(list(help_dict.keys()))):
+ category_str=f"-- {category.title()} "
+ grid.append(
+ ANSIString(
+ self.index_category_clr+category_str
+ +"-"*(width-len(category_str))
+ +self.index_topic_clr
+ )
+ )
+ verbatim_elements.append(len(grid)-1)
+
+ entries=sorted(set(help_dict.get(category,[])))
+ grid.extend(entries)
+
+ returngrid,verbatim_elements
+
+ help_index=""width=self.client_width()grid=[]verbatim_elements=[]
- forcategoryinsorted(set(list(hdict_cmds.keys())+list(hdict_db.keys()))):
+ cmd_grid,db_grid="",""
- category_str=f"-- {category.title()} "
- grid.append(
- ANSIString(
- category_clr+category_str+"-"*(width-len(category_str))+topic_clr
- )
- )
- verbatim_elements.append(len(grid)-1)
+ ifany(cmd_help_dict.values()):
+ # get the command-help entries by-category
+ sep1=(self.index_type_separator_clr
+ +pad("Commands",width=width,fillchar='-')
+ +self.index_topic_clr)
+ grid,verbatim_elements=_group_by_category(cmd_help_dict)
+ gridrows=format_grid(grid,width,sep=" ",verbatim_elements=verbatim_elements)
+ cmd_grid=ANSIString("\n").join(gridrows)ifgridrowselse""
- entries=sorted(set(hdict_cmds.get(category,[])+hdict_db.get(category,[])))
- grid.extend(entries)
+ ifany(db_help_dict.values()):
+ # get db-based help entries by-category
+ sep2=(self.index_type_separator_clr
+ +pad("Game & World",width=width,fillchar='-')
+ +self.index_topic_clr)
+ grid,verbatim_elements=_group_by_category(db_help_dict)
+ gridrows=format_grid(grid,width,sep=" ",verbatim_elements=verbatim_elements)
+ db_grid=ANSIString("\n").join(gridrows)ifgridrowselse""
- gridrows=format_grid(grid,width,sep=" ",verbatim_elements=verbatim_elements)
- gridrows=ANSIString("\n").join(gridrows)
- returngridrows
+ # only show the main separators if there are actually both cmd and db-based help
+ ifcmd_gridanddb_grid:
+ help_index=f"{sep1}\n{cmd_grid}\n{sep2}\n{db_grid}"
+ else:
+ help_index=f"{cmd_grid}{db_grid}"
+
+ returnhelp_index
[docs]defparse(self):""" input is a string containing the command or topic to match.
+
+ The allowed syntax is
+ ::
+
+ help <topic>[/<subtopic>[/<subtopic>[/...]]]
+
+ The database/command query is always for `<topic>`, and any subtopics
+ is then parsed from there. If a `<topic>` has spaces in it, it is
+ always matched before assuming the space begins a subtopic.
+
"""
- self.original_args=self.args.strip()
- self.args=self.args.strip().lower()
[docs]deffunc(self):""" Run the dynamic help entry creator. """
- query,cmdset=self.args,self.cmdsetcaller=self.caller
-
- suggestion_cutoff=self.suggestion_cutoff
- suggestion_maxnum=self.suggestion_maxnum
-
- ifnotquery:
- query="all"
+ query,subtopics,cmdset=self.topic,self.subtopics,self.cmdset# removing doublets in cmdset, caused by cmdhandler# having to allow doublet commands to manage exits etc.cmdset.make_unique(caller)
- # retrieve all available commands and database topics
+ # retrieve all available commands and database / file-help topicsall_cmds=[cmdforcmdincmdsetifself.check_show_help(cmd,caller)]
- all_topics=[
- topicfortopicinHelpEntry.objects.all()iftopic.access(caller,"view",default=True)
- ]
- all_categories=list(
- set(
- [HelpCategory(cmd.help_category)forcmdinall_cmds]
- +[HelpCategory(topic.help_category)fortopicinall_topics]
- )
- )
- ifqueryin("list","all"):
- # we want to list all available help entries, grouped by category
- hdict_cmd=defaultdict(list)
- hdict_topic=defaultdict(list)
- # create the dictionaries {category:[topic, topic ...]} required by format_help_list
+ # we group the file-help topics with the db ones, giving the db ones priority
+ file_help_topics=FILE_HELP_ENTRIES.all(return_dict=True)
+ db_topics={
+ topic.key.lower().strip():topicfortopicinHelpEntry.objects.all()
+ iftopic.access(caller,"view",default=True)
+ }
+ all_db_topics=list({**file_help_topics,**db_topics}.values())
+
+ all_categories=list(set(
+ [HelpCategory(cmd.help_category)forcmdinall_cmds]
+ +[HelpCategory(topic.help_category)fortopicinall_db_topics]
+ ))
+
+ ifnotquery:
+ # list all available help entries, grouped by category. We want to
+ # build dictionaries {category: [topic, topic, ...], ...}
+ cmd_help_dict=defaultdict(list)
+ db_help_dict=defaultdict(list)
+
# Filter commands that should be reached by the help# system, but not be displayed in the table, or be displayed differently.forcmdinall_cmds:ifself.should_list_cmd(cmd,caller):key=(cmd.auto_help_display_keyifhasattr(cmd,"auto_help_display_key")elsecmd.key)
- hdict_cmd[cmd.help_category].append(key)
- [hdict_topic[topic.help_category].append(topic.key)fortopicinall_topics]
- # report back
- self.msg_help(self.format_help_list(hdict_cmd,hdict_topic))
+ cmd_help_dict[cmd.help_category].append(key)
+
+ fordb_topicinall_db_topics:
+ db_help_dict[db_topic.help_category].append(db_topic.key)
+
+ output=self.format_help_index(cmd_help_dict,db_help_dict)
+ self.msg_help(output)
+
return
- # Try to access a particular help entry or category
- entries=[cmdforcmdinall_cmdsifcmd]+list(HelpEntry.objects.all())+all_categories
+ # We have a query - try to find a specific topic/category using the
+ # Lunr search engine
- formatch_queryin[f"{query}~1",f"{query}*"]:
- # We first do an exact word-match followed by a start-by query
+ # all available options
+ entries=[cmdforcmdinall_cmdsifcmd]+all_db_topics+all_categories
+
+ # lunr search fields/boosts
+ search_fields=[
+ {"field_name":"key","boost":10},
+ {"field_name":"aliases","boost":9},
+ {"field_name":"category","boost":8},
+ {"field_name":"tags","boost":1},# tags are not used by default
+ ]
+ match,suggestions=None,None
+
+ formatch_queryin(query,f"{query}*"):
+ # We first do an exact word-match followed by a start-by query. The
+ # return of this will either be a HelpCategory, a Command or a
+ # HelpEntry/FileHelpEntry.matches,suggestions=help_search_with_index(
- match_query,entries,suggestion_maxnum=self.suggestion_maxnum
+ match_query,entries,
+ suggestion_maxnum=self.suggestion_maxnum,
+ fields=search_fields)
-
ifmatches:match=matches[0]
- ifisinstance(match,HelpCategory):
- formatted=self.format_help_list(
- {
- match.key:[
- cmd.key
- forcmdinall_cmds
- ifmatch.key.lower()==cmd.help_category
- ]
- },
- {
- match.key:[
- topic.key
- fortopicinall_topics
- ifmatch.key.lower()==topic.help_category
- ]
- },
- )
- elifinherits_from(match,"evennia.commands.command.Command"):
- formatted=self.format_help_entry(
- match.key,
- match.get_help(caller,cmdset),
- aliases=match.aliases,
- suggested=suggestions[1:],
- )
- else:
- formatted=self.format_help_entry(
- match.key,
- match.entrytext,
- aliases=match.aliases.all(),
- suggested=suggestions[1:],
+ break
+
+ ifnotmatch:
+ # no topic matches found. Only give suggestions.
+ help_text=f"There is no help topic matching '{query}'."
+
+ ifnotsuggestions:
+ # we don't even have a good suggestion. Run a second search,
+ # doing a full-text search in the actual texts of the help
+ # entries
+
+ search_fields=[
+ {"field_name":"text","boost":1},
+ ]
+
+ formatch_queryin[query,f"{query}*"]:
+ _,suggestions=help_search_with_index(
+ match_query,entries,
+ suggestion_maxnum=self.suggestion_maxnum,
+ fields=search_fields)
- self.msg_help(formatted)
- return
+ ifsuggestions:
+ help_text+="\n... But matches where found within the help texts of the suggestions below."
+ break
- # no exact matches found. Just give suggestions.
- self.msg(
- self.format_help_entry(
- "",f"No help entry found for '{query}'",None,suggested=suggestions
- ),
- options={"type":"help"},
- )
+ output=self.format_help_entry(
+ topic=None,# this will give a no-match style title
+ help_text=help_text,
+ suggested=suggestions
+ )
+
+ self.msg_help(output)
+ return
+
+ ifisinstance(match,HelpCategory):
+ # no subtopics for categories - these are just lists of topics
+
+ output=self.format_help_index(
+ {
+ match.key:[
+ cmd.key
+ forcmdinall_cmds
+ ifmatch.key.lower()==cmd.help_category
+ ]
+ },
+ {
+ match.key:[
+ topic.key
+ fortopicinall_db_topics
+ ifmatch.key.lower()==topic.help_category
+ ]
+ },
+ title_lone_category=True
+ )
+ self.msg_help(output)
+ return
+
+ ifinherits_from(match,"evennia.commands.command.Command"):
+ # a command match
+ topic=match.key
+ help_text=match.get_help(caller,cmdset)
+ aliases=match.aliases
+ suggested=suggestions[1:]
+ else:
+ # a database (or file-help) match
+ topic=match.key
+ help_text=match.entrytext
+ aliases=match.aliasesifisinstance(match.aliases,list)elsematch.aliases.all()
+ suggested=suggestions[1:]
+
+ # parse for subtopics. The subtopic_map is a dict with the current topic/subtopic
+ # text is stored under a `None` key and all other keys are subtopic titles pointing
+ # to nested dicts.
+
+ subtopic_map=parse_entry_for_subcategories(help_text)
+ help_text=subtopic_map[None]
+ subtopic_index=[subtopicforsubtopicinsubtopic_mapifsubtopicisnotNone]
+
+ ifsubtopics:
+ # if we asked for subtopics, parse the found topic_text to see if any match.
+ # the subtopics is a list describing the path through the subtopic_map.
+
+ forsubtopic_queryinsubtopics:
+
+ ifsubtopic_querynotinsubtopic_map:
+ # exact match failed. Try startswith-match
+ fuzzy_match=False
+ forkeyinsubtopic_map:
+ ifkeyandkey.startswith(subtopic_query):
+ subtopic_query=key
+ fuzzy_match=True
+ break
+
+ ifnotfuzzy_match:
+ # startswith failed - try an 'in' match
+ forkeyinsubtopic_map:
+ ifkeyandsubtopic_queryinkey:
+ subtopic_query=key
+ fuzzy_match=True
+ break
+
+ ifnotfuzzy_match:
+ # no match found - give up
+ checked_topic=topic+f"/{subtopic_query}"
+ output=self.format_help_entry(
+ topic=topic,
+ help_text=f"No help entry found for '{checked_topic}'",
+ subtopics=subtopic_index
+ )
+ self.msg_help(output)
+ return
+
+ # if we get here we have an exact or fuzzy match
+
+ subtopic_map=subtopic_map.pop(subtopic_query)
+ subtopic_index=[subtopicforsubtopicinsubtopic_mapifsubtopicisnotNone]
+ # keep stepping down into the tree, append path to show position
+ topic=topic+f"/{subtopic_query}"
+
+ # we reached the bottom of the topic tree
+ help_text=subtopic_map[None]
+
+ output=self.format_help_entry(
+ topic=topic,
+ help_text=help_text,
+ aliases=aliasesifnotsubtopicselseNone,
+ subtopics=subtopic_index
+ )
+
+ self.msg_help(output)
def_loadhelp(caller):
@@ -438,15 +608,51 @@
delete - remove help topic. Examples:
- sethelp throw = This throws something at ...
+ sethelp lore = In the beginning was ... sethelp/append pickpocketing,Thievery = This steals ... sethelp/replace pickpocketing, ,attr(is_thief) = This steals ... sethelp/edit thievery
- This command manipulates the help database. A help entry can be created,
- appended/merged to and deleted. If you don't assign a category, the
- "General" category will be used. If no lockstring is specified, default
- is to let everyone read the help file.
+ If not assigning a category, the `settings.DEFAULT_HELP_CATEGORY` category
+ will be used. If no lockstring is specified, everyone will be able to read
+ the help entry. Sub-topics are embedded in the help text.
+
+ Note that this cannot modify command-help entries - these are modified
+ in-code, outside the game.
+
+ # SUBTOPICS
+
+ ## Adding subtopics
+
+ Subtopics helps to break up a long help entry into sub-sections. Users can
+ access subtopics with |whelp topic/subtopic/...|n Subtopics are created and
+ stored together with the main topic.
+
+ To start adding subtopics, add the text '# SUBTOPICS' on a new line at the
+ end of your help text. After this you can now add any number of subtopics,
+ each starting with '## <subtopic-name>' on a line, followed by the
+ help-text of that subtopic.
+ Use '### <subsub-name>' to add a sub-subtopic and so on. Max depth is 5. A
+ subtopic's title is case-insensitive and can consist of multiple words -
+ the user will be able to enter a partial match to access it.
+
+ For example:
+
+ | Main help text for <topic>
+ |
+ | # SUBTOPICS
+ |
+ | ## about
+ |
+ | Text for the '<topic>/about' subtopic'
+ |
+ | ### more about-info
+ |
+ | Text for the '<topic>/about/more about-info sub-subtopic
+ |
+ | ## extra
+ |
+ | Text for the '<topic>/extra' subtopic """
@@ -491,7 +697,7 @@
lockstring=",".join(lhslist[2:])ifnlist>2elseold_entry.locks.get()exceptException:old_entry=None
- category=lhslist[1]ifnlist>1else"General"
+ category=lhslist[1]ifnlist>1elseDEFAULT_HELP_CATEGORYlockstring=",".join(lhslist[2:])ifnlist>2else"view:all()"category=category.lower()
@@ -534,6 +740,7 @@
old_entry.aliases.add(aliases)self.msg("Entry updated:\n%s%s"%(old_entry.entrytext,aliastxt))return
+
if"delete"inswitchesor"del"inswitches:# delete the help entryifnotold_entry:
diff --git a/docs/1.0-dev/_modules/evennia/commands/default/syscommands.html b/docs/1.0-dev/_modules/evennia/commands/default/syscommands.html
index d0971db3b5..eb2758fb53 100644
--- a/docs/1.0-dev/_modules/evennia/commands/default/syscommands.html
+++ b/docs/1.0-dev/_modules/evennia/commands/default/syscommands.html
@@ -69,7 +69,6 @@
fromevennia.commands.cmdhandlerimportCMD_NOINPUTfromevennia.commands.cmdhandlerimportCMD_NOMATCHfromevennia.commands.cmdhandlerimportCMD_MULTIMATCH
-fromevennia.commands.cmdhandlerimportCMD_CHANNELfromevennia.utilsimportutilsfromdjango.confimportsettings
@@ -146,53 +145,6 @@
matches=self.matches# at_search_result will itself msg the multimatch options to the caller.at_search_result([match[2]formatchinmatches],self.caller,query=matches[0][0])
-
-
-# Command called when the command given at the command line
-# was identified as a channel name, like there existing a
-# channel named 'ooc' and the user wrote
-# > ooc Hello!
-
-
-
[docs]classSystemSendToChannel(COMMAND_DEFAULT_CLASS):
- """
- This is a special command that the cmdhandler calls
- when it detects that the command given matches
- an existing Channel object key (or alias).
- """
-
- key=CMD_CHANNEL
- locks="cmd:all()"
-
-
[docs]deffunc(self):
- """
- Create a new message and send it to channel, using
- the already formatted input.
- """
- caller=self.caller
- channelkey,msg=self.args
- ifnotmsg:
- caller.msg("Say what?")
- return
- channel=ChannelDB.objects.get_channel(channelkey)
- ifnotchannel:
- caller.msg("Channel '%s' not found."%channelkey)
- return
- ifnotchannel.has_connection(caller):
- string="You are not connected to channel '%s'."
- caller.msg(string%channelkey)
- return
- ifnotchannel.access(caller,"send"):
- string="You are not permitted to send to channel '%s'."
- caller.msg(string%channelkey)
- return
- msg="[%s] %s: %s"%(channel.key,caller.name,msg)
- msgobj=create.create_message(caller,msg,channels=[channel])
- channel.msg(msgobj)
diff --git a/docs/1.0-dev/_modules/evennia/commands/default/tests.html b/docs/1.0-dev/_modules/evennia/commands/default/tests.html
index 94454d4b62..b931b27ffe 100644
--- a/docs/1.0-dev/_modules/evennia/commands/default/tests.html
+++ b/docs/1.0-dev/_modules/evennia/commands/default/tests.html
@@ -58,6 +58,7 @@
importdatetimefromanythingimportAnything
+fromparameterizedimportparameterizedfromdjango.confimportsettingsfromunittest.mockimportpatch,Mock,MagicMock
@@ -65,7 +66,7 @@
fromevennia.commands.default.cmdset_characterimportCharacterCmdSetfromevennia.utils.test_resourcesimportEvenniaTestfromevennia.commands.defaultimport(
- help,
+ helpashelp_module,general,system,admin,
@@ -76,7 +77,6 @@
unloggedin,syscommands,)
-fromevennia.commands.cmdparserimportbuild_matchesfromevennia.commands.default.muxcommandimportMuxCommandfromevennia.commands.commandimportCommand,InterruptCommandfromevennia.commandsimportcmdparser
@@ -244,7 +244,7 @@
else:# a single expected string and thus a single receiver (defaults to caller)receiver=receiverifreceiverelsecaller
- receiver_mapping[receiver]=str(msg).strip()ifmsgelseNone
+ receiver_mapping[receiver]=str(msg).strip()ifmsgisnotNoneelseNoneunmocked_msg_methods={}forreceiverinreceiver_mapping:
@@ -289,6 +289,12 @@
exceptInterruptCommand:pass
+ forinpininputs:
+ # if there are any inputs left, we may have a non-generator
+ # input to handle (get_input/ask_yes_no that uses a separate
+ # cmdset rather than a yield
+ caller.execute_cmd(inp)
+
# At this point the mocked .msg methods on each receiver will have# stored all calls made to them (that's a basic function of the Mock# class). We will not extract them and compare to what we expected to
@@ -443,6 +449,9 @@
[docs]defsetUp(self):super().setUp()# we need to set up a logger here since lunr takes over the logger otherwise
@@ -457,15 +466,135 @@
logging.disable(level=logging.ERROR)
[docs]deftest_set_help(self):self.call(
- help.CmdSetHelp(),
+ help_module.CmdSetHelp(),"testhelp, General = This is a test","Topic 'testhelp' was successfully created.",)
- self.call(help.CmdHelp(),"testhelp","Help for testhelp",cmdset=CharacterCmdSet())
+ self.call(help_module.CmdHelp(),"testhelp","Help for testhelp",cmdset=CharacterCmdSet())
+
+ @parameterized.expand([
+ ("test",# main help entry
+ "Help for test\n\n"
+ "Main help text\n\n"
+ "Subtopics:\n"
+ " test/creating extra stuff"
+ " test/something else"
+ " test/more"
+ ),
+ ("test/creating extra stuff",# subtopic, full match
+ "Help for test/creating extra stuff\n\n"
+ "Help on creating extra stuff.\n\n"
+ "Subtopics:\n"
+ " test/creating extra stuff/subsubtopic\n"
+ ),
+ ("test/creating",# startswith-match
+ "Help for test/creating extra stuff\n\n"
+ "Help on creating extra stuff.\n\n"
+ "Subtopics:\n"
+ " test/creating extra stuff/subsubtopic\n"
+ ),
+ ("test/extra",# partial match
+ "Help for test/creating extra stuff\n\n"
+ "Help on creating extra stuff.\n\n"
+ "Subtopics:\n"
+ " test/creating extra stuff/subsubtopic\n"
+ ),
+ ("test/extra/subsubtopic",# partial subsub-match
+ "Help for test/creating extra stuff/subsubtopic\n\n"
+ "A subsubtopic text"
+ ),
+ ("test/creating extra/subsub",# partial subsub-match
+ "Help for test/creating extra stuff/subsubtopic\n\n"
+ "A subsubtopic text"
+ ),
+ ("test/Something else",# case
+ "Help for test/something else\n\n"
+ "Something else"
+ ),
+ ("test/More",# case
+ "Help for test/more\n\n"
+ "Another text\n\n"
+ "Subtopics:\n"
+ " test/more/second-more"
+ ),
+ ("test/More/Second-more",
+ "Help for test/more/second-more\n\n"
+ "The Second More text.\n\n"
+ "Subtopics:\n"
+ " test/more/second-more/more again"
+ " test/more/second-more/third more"
+ ),
+ ("test/More/-more",# partial match
+ "Help for test/more/second-more\n\n"
+ "The Second More text.\n\n"
+ "Subtopics:\n"
+ " test/more/second-more/more again"
+ " test/more/second-more/third more"
+ ),
+ ("test/more/second/more again",
+ "Help for test/more/second-more/more again\n\n"
+ "Even more text.\n"
+ ),
+ ("test/more/second/third",
+ "Help for test/more/second-more/third more\n\n"
+ "Third more text\n"
+ ),
+ ])
+ deftest_subtopic_fetch(self,helparg,expected):
+ """
+ Check retrieval of subtopics.
+
+ """
+ classTestCmd(Command):
+ """
+ Main help text
+
+ # SUBTOPICS
+
+ ## creating extra stuff
+
+ Help on creating extra stuff.
+
+ ### subsubtopic
+
+ A subsubtopic text
+
+ ## Something else
+
+ Something else
+
+ ## More
+
+ Another text
+
+ ### Second-More
+
+ The Second More text.
+
+ #### More again
+
+ Even more text.
+
+ #### Third more
+
+ Third more text
+
+ """
+ key="test"
+
+ classTestCmdSet(CmdSet):
+ defat_cmdset_creation(self):
+ self.add(TestCmd())
+ self.add(help_module.CmdHelp())
+
+ self.call(help_module.CmdHelp(),
+ helparg,
+ expected,
+ cmdset=TestCmdSet())
[docs]classTestSystem(CommandTest):
@@ -1603,21 +1732,13 @@
self.call(comms.CmdAddCom(),"tc = testchan",
- "You are already connected to channel testchan. You can now",
+ "You are already connected to channel testchan.| You can now",receiver=self.account,)self.call(comms.CmdDelCom(),"tc",
- "Your alias 'tc' for channel testchan was cleared.",
- receiver=self.account,
- )
-
-
[docs]deftest_channels(self):
- self.call(
- comms.CmdChannels(),
- "",
- "Available channels (use comlist,addcom and delcom to manage",
+ "Any alias 'tc' for channel testchan was cleared.",receiver=self.account,)
-"""
-The channel handler, accessed from this module as CHANNEL_HANDLER is a
-singleton that handles the stored set of channels and how they are
-represented against the cmdhandler.
-
-If there is a channel named 'newbie', we want to be able to just write
-
- newbie Hello!
-
-For this to work, 'newbie', the name of the channel, must be
-identified by the cmdhandler as a command name. The channelhandler
-stores all channels as custom 'commands' that the cmdhandler can
-import and look through.
-
-> Warning - channel names take precedence over command names, so make
-sure to not pick clashing channel names.
-
-Unless deleting a channel you normally don't need to bother about the
-channelhandler at all - the create_channel method handles the update.
-
-To delete a channel cleanly, delete the channel object, then call
-update() on the channelhandler. Or use Channel.objects.delete() which
-does this for you.
-
-"""
-fromdjango.confimportsettings
-fromevennia.commandsimportcmdset
-fromevennia.utils.loggerimporttail_log_file
-fromevennia.utils.utilsimportclass_from_module
-fromdjango.utils.translationimportgettextas_
-
-# we must late-import these since any overloads are likely to
-# themselves be using these classes leading to a circular import.
-
-_CHANNEL_HANDLER_CLASS=None
-_CHANNEL_COMMAND_CLASS=None
-_CHANNELDB=None
-_COMMAND_DEFAULT_CLASS=class_from_module(settings.COMMAND_DEFAULT_CLASS)
-
-
[docs]classChannelCommand(_COMMAND_DEFAULT_CLASS):
- """
- {channelkey} channel
-
- {channeldesc}
-
- Usage:
- {lower_channelkey} <message>
- {lower_channelkey}/history [start]
- {lower_channelkey} off - mutes the channel
- {lower_channelkey} on - unmutes the channel
-
- Switch:
- history: View 20 previous messages, either from the end or
- from <start> number of messages from the end.
-
- Example:
- {lower_channelkey} Hello World!
- {lower_channelkey}/history
- {lower_channelkey}/history 30
-
- """
-
- # ^note that channeldesc and lower_channelkey will be filled
- # automatically by ChannelHandler
-
- # this flag is what identifies this cmd as a channel cmd
- # and branches off to the system send-to-channel command
- # (which is customizable by admin)
- is_channel=True
- key="general"
- help_category="Channel Names"
- obj=None
- arg_regex=r"\s.*?|/history.*?"
-
-
[docs]defparse(self):
- """
- Simple parser
- """
- # cmdhandler sends channame:msg here.
- channelname,msg=self.args.split(":",1)
- self.history_start=None
- ifmsg.startswith("/history"):
- arg=msg[8:]
- try:
- self.history_start=int(arg)ifargelse0
- exceptValueError:
- # if no valid number was given, ignore it
- pass
- self.args=(channelname.strip(),msg.strip())
-
-
[docs]deffunc(self):
- """
- Create a new message and send it to channel, using
- the already formatted input.
- """
- global_CHANNELDB
- ifnot_CHANNELDB:
- fromevennia.comms.modelsimportChannelDBas_CHANNELDB
-
- channelkey,msg=self.args
- caller=self.caller
- ifnotmsg:
- self.msg(_("Say what?"))
- return
- channel=_CHANNELDB.objects.get_channel(channelkey)
-
- ifnotchannel:
- self.msg(_("Channel '%s' not found.")%channelkey)
- return
- ifnotchannel.has_connection(caller):
- string=_("You are not connected to channel '%s'.")
- self.msg(string%channelkey)
- return
- ifnotchannel.access(caller,"send"):
- string=_("You are not permitted to send to channel '%s'.")
- self.msg(string%channelkey)
- return
- ifmsg=="on":
- caller=callerifnothasattr(caller,"account")elsecaller.account
- unmuted=channel.unmute(caller)
- ifunmuted:
- self.msg(_("You start listening to %s.")%channel)
- return
- self.msg(_("You were already listening to %s.")%channel)
- return
- ifmsg=="off":
- caller=callerifnothasattr(caller,"account")elsecaller.account
- muted=channel.mute(caller)
- ifmuted:
- self.msg(_("You stop listening to %s.")%channel)
- return
- self.msg(_("You were already not listening to %s.")%channel)
- return
- ifself.history_startisnotNone:
- # Try to view history
- log_file=channel.attributes.get("log_file",default="channel_%s.log"%channel.key)
-
- defsend_msg(lines):
- returnself.msg(
- "".join(line.split("[-]",1)[1]if"[-]"inlineelselineforlineinlines)
- )
-
- tail_log_file(log_file,self.history_start,20,callback=send_msg)
- else:
- caller=callerifnothasattr(caller,"account")elsecaller.account
- ifcallerinchannel.mutelist:
- self.msg(_("You currently have %s muted.")%channel)
- return
- channel.msg(msg,senders=self.caller,online=True)
-
-
[docs]defget_extra_info(self,caller,**kwargs):
- """
- Let users know that this command is for communicating on a channel.
-
- Args:
- caller (TypedObject): A Character or Account who has entered an ambiguous command.
-
- Returns:
- A string with identifying information to disambiguate the object, conventionally with a preceding space.
- """
- return_(" (channel)")
-
-
-
[docs]classChannelHandler(object):
- """
- The ChannelHandler manages all active in-game channels and
- dynamically creates channel commands for users so that they can
- just give the channel's key or alias to write to it. Whenever a
- new channel is created in the database, the update() method on
- this handler must be called to sync it with the database (this is
- done automatically if creating the channel with
- evennia.create_channel())
-
- """
-
-
[docs]defadd(self,channel):
- """
- Add an individual channel to the handler. This is called
- whenever a new channel is created.
-
- Args:
- channel (Channel): The channel to add.
-
- Notes:
- To remove a channel, simply delete the channel object and
- run self.update on the handler. This should usually be
- handled automatically by one of the deletion methos of
- the Channel itself.
-
- """
- global_CHANNEL_COMMAND_CLASS
- ifnot_CHANNEL_COMMAND_CLASS:
- _CHANNEL_COMMAND_CLASS=class_from_module(settings.CHANNEL_COMMAND_CLASS)
-
- # map the channel to a searchable command
- cmd=_CHANNEL_COMMAND_CLASS(
- key=channel.key.strip().lower(),
- aliases=channel.aliases.all(),
- locks="cmd:all();%s"%channel.locks,
- help_category="Channel names",
- obj=channel,
- is_channel=True,
- )
- # format the help entry
- key=channel.key
- cmd.__doc__=cmd.__doc__.format(
- channelkey=key,
- lower_channelkey=key.strip().lower(),
- channeldesc=channel.attributes.get("desc",default="").strip(),
- )
- self._cached_channel_cmds[channel]=cmd
- self._cached_channels[key]=channel
- self._cached_cmdsets={}
-
- add_channel=add# legacy alias
-
-
[docs]defremove(self,channel):
- """
- Remove channel from channelhandler. This will also delete it.
-
- Args:
- channel (Channel): Channel to remove/delete.
-
- """
- ifchannel.pk:
- channel.delete()
- self.update()
-
-
[docs]defupdate(self):
- """
- Updates the handler completely, including removing old removed
- Channel objects. This must be called after deleting a Channel.
-
- """
- global_CHANNELDB
- ifnot_CHANNELDB:
- fromevennia.comms.modelsimportChannelDBas_CHANNELDB
- self._cached_channel_cmds={}
- self._cached_cmdsets={}
- self._cached_channels={}
- forchannelin_CHANNELDB.objects.get_all_channels():
- self.add(channel)
-
-
[docs]defget(self,channelname=None):
- """
- Get a channel from the handler, or all channels
-
- Args:
- channelame (str, optional): Channel key, case insensitive.
- Returns
- channels (list): The matching channels in a list, or all
- channels in the handler.
-
- """
- ifchannelname:
- channel=self._cached_channels.get(channelname.lower(),None)
- return[channel]ifchannelelse[]
- returnlist(self._cached_channels.values())
-
-
[docs]defget_cmdset(self,source_object):
- """
- Retrieve cmdset for channels this source_object has
- access to send to.
-
- Args:
- source_object (Object): An object subscribing to one
- or more channels.
-
- Returns:
- cmdsets (list): The Channel-Cmdsets `source_object` has
- access to.
-
- """
- ifsource_objectinself._cached_cmdsets:
- returnself._cached_cmdsets[source_object]
- else:
- # create a new cmdset holding all viable channels
- chan_cmdset=None
- chan_cmds=[
- channelcmd
- forchannel,channelcmdinself._cached_channel_cmds.items()
- ifchannel.subscriptions.has(source_object)
- andchannelcmd.access(source_object,"send")
- ]
- ifchan_cmds:
- chan_cmdset=cmdset.CmdSet()
- chan_cmdset.key="ChannelCmdSet"
- chan_cmdset.priority=101
- chan_cmdset.duplicates=True
- forcmdinchan_cmds:
- chan_cmdset.add(cmd)
- self._cached_cmdsets[source_object]=chan_cmdset
- returnchan_cmdset
-
-
-# set up the singleton
-CHANNEL_HANDLER=class_from_module(settings.CHANNEL_HANDLER_CLASS)()
-CHANNELHANDLER=CHANNEL_HANDLER# legacy
-
-
-
-
\ No newline at end of file
diff --git a/docs/1.0-dev/_modules/evennia/comms/comms.html b/docs/1.0-dev/_modules/evennia/comms/comms.html
index 42a5a12c7c..df4abb84e6 100644
--- a/docs/1.0-dev/_modules/evennia/comms/comms.html
+++ b/docs/1.0-dev/_modules/evennia/comms/comms.html
@@ -54,18 +54,44 @@
fromevennia.utilsimportcreate,loggerfromevennia.utils.utilsimportmake_iter
-_CHANNEL_HANDLER=None
-
[docs]classDefaultChannel(ChannelDB,metaclass=TypeclassBase):""" This is the base class for all Channel Comms. Inherit from this to create different types of communication channels.
+ Class-level variables:
+ - `send_to_online_only` (bool, default True) - if set, will only try to
+ send to subscribers that are actually active. This is a useful optimization.
+ - `log_file` (str, default `"channel_{channelname}.log"`). This is the
+ log file to which the channel history will be saved. The `{channelname}` tag
+ will be replaced by the key of the Channel. If an Attribute 'log_file'
+ is set, this will be used instead. If this is None and no Attribute is found,
+ no history will be saved.
+ - `channel_prefix_string` (str, default `"[{channelname} ]"`) - this is used
+ as a simple template to get the channel prefix with `.channel_prefix()`.
+
"""objects=ChannelManager()
+ # channel configuration
+
+ # only send to characters/accounts who has an active session (this is a
+ # good optimization since people can still recover history separately).
+ send_to_online_only=True
+ # store log in log file. `channel_key tag will be replace with key of channel.
+ # Will use log_file Attribute first, if given
+ log_file="channel_{channelname}.log"
+ # which prefix to use when showing were a message is coming from. Set to
+ # None to disable and set this later.
+ channel_prefix_string="[{channelname}] "
+
+ # default nick-alias replacements (default using the 'channel' command)
+ channel_msg_nick_pattern=r"{alias}\s*?|{alias}\s+?(?P<arg1>.+?)"
+ channel_msg_nick_replacement="channel {channelname} = $1"
+
+
[docs]defat_first_save(self):""" Called by the typeclass system the very first time the channel
@@ -75,7 +101,6 @@
"""self.basetype_setup()self.at_channel_creation()
- self.attributes.add("log_file","channel_%s.log"%self.key)ifhasattr(self,"_createdict"):# this is only set if the channel was created# with the utils.create.create_channel function.
@@ -97,14 +122,11 @@
self.tags.batch_add(*cdict["tags"])
[docs]defbasetype_setup(self):
- # delayed import of the channelhandler
- global_CHANNEL_HANDLER
- ifnot_CHANNEL_HANDLER:
- fromevennia.comms.channelhandlerimportCHANNEL_HANDLERas_CHANNEL_HANDLER
- # register ourselves with the channelhandler.
- _CHANNEL_HANDLER.add(self)
+ self.locks.add("send:all();listen:all();control:perm(Admin)")
- self.locks.add("send:all();listen:all();control:perm(Admin)")
+ # make sure we don't have access to a same-named old channel's history.
+ log_file=self.get_log_filename()
+ logger.rotate_log_file(log_file,num_lines_to_append=0)
[docs]defget_log_filename(self):
+ """
+ File name to use for channel log.
+
+ Returns:
+ str: The filename to use (this is always assumed to be inside
+ settings.LOG_DIR)
+
+ """
+ ifnotself._log_file:
+ self._log_file=self.attributes.get(
+ "log_file",self.log_file.format(channelname=self.key.lower()))
+ returnself._log_file
+
+
[docs]defset_log_filename(self,filename):
+ """
+ Set a custom log filename.
+
+ Args:
+ filename (str): The filename to set. This is a path starting from
+ inside the settings.LOG_DIR location.
+
+ """
+ self.attributes.add("log_file",filename)
+
[docs]defhas_connection(self,subscriber):""" Checks so this account is actually listening
@@ -143,6 +192,10 @@
defmutelist(self):returnself.db.mute_listor[]
+ @property
+ defbanlist(self):
+ returnself.db.ban_listor[]
+
@propertydefwholist(self):subs=self.subscriptions.all()
@@ -171,6 +224,10 @@
**kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default).
+ Returns:
+ bool: True if muting was successful, False if we were already
+ muted.
+
"""mutelist=self.mutelistifsubscribernotinmutelist:
@@ -181,19 +238,67 @@
[docs]defunmute(self,subscriber,**kwargs):"""
- Removes an entity to the list of muted subscribers. A muted subscriber will no longer see channel messages,
- but may use channel commands.
+ Removes an entity from the list of muted subscribers. A muted subscriber
+ will no longer see channel messages, but may use channel commands. Args: subscriber (Object or Account): The subscriber to unmute. **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default).
+ Returns:
+ bool: True if unmuting was successful, False if we were already
+ unmuted.
+
"""mutelist=self.mutelistifsubscriberinmutelist:mutelist.remove(subscriber)
- self.db.mute_list=mutelist
+ returnTrue
+ returnFalse
+
+
[docs]defban(self,target,**kwargs):
+ """
+ Ban a given user from connecting to the channel. This will not stop
+ users already connected, so the user must be booted for this to take
+ effect.
+
+ Args:
+ target (Object or Account): The entity to unmute. This need not
+ be a subscriber.
+ **kwargs (dict): Arbitrary, optional arguments for users
+ overriding the call (unused by default).
+
+ Returns:
+ bool: True if banning was successful, False if target was already
+ banned.
+ """
+ banlist=self.banlist
+ iftargetnotinbanlist:
+ banlist.append(target)
+ self.db.ban_list=banlist
+ returnTrue
+ returnFalse
+
+
[docs]defunban(self,target,**kwargs):
+ """
+ Un-Ban a given user. This will not reconnect them - they will still
+ have to reconnect and set up aliases anew.
+
+ Args:
+ target (Object or Account): The entity to unmute. This need not
+ be a subscriber.
+ **kwargs (dict): Arbitrary, optional arguments for users
+ overriding the call (unused by default).
+
+ Returns:
+ bool: True if unbanning was successful, False if target was not
+ previously banned.
+ """
+ banlist=list(self.banlist)
+ iftargetinbanlist:
+ banlist=[bannedforbannedinbanlistifbanned!=target]
+ self.db.ban_list=banlistreturnTruereturnFalse
[docs]@classmethod
- defcreate(cls,key,account=None,*args,**kwargs):
+ defcreate(cls,key,creator=None,*args,**kwargs):""" Creates a basic Channel with default parameters, unless otherwise specified or extended.
@@ -295,7 +400,8 @@
Args: key (str): This must be unique.
- account (Account): Account to attribute this object to.
+ creator (Account or Object): Entity to associate with this channel
+ (used for tracking) Keyword Args: aliases (list of str): List of alternative (likely shorter) keynames.
@@ -323,8 +429,8 @@
# Record creator id and creation IPifip:obj.db.creator_ip=ip
- ifaccount:
- obj.db.creator_id=account.id
+ ifcreator:
+ obj.db.creator_id=creator.idexceptExceptionasexc:errors.append("An error occurred while creating this '%s' object."%key)
@@ -334,284 +440,189 @@
[docs]defdelete(self):"""
- Deletes channel while also cleaning up channelhandler.
+ Deletes channel. """self.attributes.clear()self.aliases.clear()
- super().delete()
- fromevennia.comms.channelhandlerimportCHANNELHANDLER
+ forsubscriberinself.subscriptions.all():
+ self.disconnect(subscriber)
+ super().delete()
- CHANNELHANDLER.update()
-
-
[docs]defmessage_transform(
- self,msgobj,emit=False,prefix=True,sender_strings=None,external=False,**kwargs
- ):
- """
- Generates the formatted string sent to listeners on a channel.
-
- Args:
- msgobj (Msg): Message object to send.
- emit (bool, optional): In emit mode the message is not associated
- with a specific sender name.
- prefix (bool, optional): Prefix `msg` with a text given by `self.channel_prefix`.
- sender_strings (list, optional): Used by bots etc, one string per external sender.
- external (bool, optional): If this is an external sender or not.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- ifsender_stringsorexternal:
- body=self.format_external(msgobj,sender_strings,emit=emit)
- else:
- body=self.format_message(msgobj,emit=emit)
- ifprefix:
- body="%s%s"%(self.channel_prefix(msgobj,emit=emit),body)
- msgobj.message=body
- returnmsgobj
-
-
[docs]defdistribute_message(self,msgobj,online=False,**kwargs):
- """
- Method for grabbing all listeners that a message should be
- sent to on this channel, and sending them a message.
-
- Args:
- msgobj (Msg or TempMsg): Message to distribute.
- online (bool): Only send to receivers who are actually online
- (not currently used):
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Notes:
- This is also where logging happens, if enabled.
-
- """
- # get all accounts or objects connected to this channel and send to them
- ifonline:
- subs=self.subscriptions.online()
- else:
- subs=self.subscriptions.all()
- forentityinsubs:
- # if the entity is muted, we don't send them a message
- ifentityinself.mutelist:
- continue
- try:
- # note our addition of the from_channel keyword here. This could be checked
- # by a custom account.msg() to treat channel-receives differently.
- entity.msg(
- msgobj.message,from_obj=msgobj.senders,options={"from_channel":self.id}
- )
- exceptAttributeErrorase:
- logger.log_trace("%s\nCannot send msg to '%s'."%(e,entity))
-
- ifmsgobj.keep_log:
- # log to file
- logger.log_file(
- msgobj.message,self.attributes.get("log_file")or"channel_%s.log"%self.key
- )
-
-
[docs]defmsg(
- self,
- msgobj,
- header=None,
- senders=None,
- sender_strings=None,
- keep_log=None,
- online=False,
- emit=False,
- external=False,
- ):
- """
- Send the given message to all accounts connected to channel. Note that
- no permission-checking is done here; it is assumed to have been
- done before calling this method. The optional keywords are not used if
- persistent is False.
-
- Args:
- msgobj (Msg, TempMsg or str): If a Msg/TempMsg, the remaining
- keywords will be ignored (since the Msg/TempMsg object already
- has all the data). If a string, this will either be sent as-is
- (if persistent=False) or it will be used together with `header`
- and `senders` keywords to create a Msg instance on the fly.
- header (str, optional): A header for building the message.
- senders (Object, Account or list, optional): Optional if persistent=False, used
- to build senders for the message.
- sender_strings (list, optional): Name strings of senders. Used for external
- connections where the sender is not an account or object.
- When this is defined, external will be assumed. The list will be
- filtered so each sender-string only occurs once.
- keep_log (bool or None, optional): This allows to temporarily change the logging status of
- this channel message. If `None`, the Channel's `keep_log` Attribute will
- be used. If `True` or `False`, that logging status will be used for this
- message only (note that for unlogged channels, a `True` value here will
- create a new log file only for this message).
- online (bool, optional) - If this is set true, only messages people who are
- online. Otherwise, messages all accounts connected. This can
- make things faster, but may not trigger listeners on accounts
- that are offline.
- emit (bool, optional) - Signals to the message formatter that this message is
- not to be directly associated with a name.
- external (bool, optional): Treat this message as being
- agnostic of its sender.
-
- Returns:
- success (bool): Returns `True` if message sending was
- successful, `False` otherwise.
-
- """
- senders=make_iter(senders)ifsenderselse[]
- ifisinstance(msgobj,str):
- # given msgobj is a string - convert to msgobject (always TempMsg)
- msgobj=TempMsg(senders=senders,header=header,message=msgobj,channels=[self])
- # we store the logging setting for use in distribute_message()
- msgobj.keep_log=keep_logifkeep_logisnotNoneelseself.db.keep_log
-
- # start the sending
- msgobj=self.pre_send_message(msgobj)
- ifnotmsgobj:
- returnFalse
- ifsender_strings:
- sender_strings=list(set(make_iter(sender_strings)))
- msgobj=self.message_transform(
- msgobj,emit=emit,sender_strings=sender_strings,external=external
- )
- self.distribute_message(msgobj,online=online)
- self.post_send_message(msgobj)
- returnTrue
-
-
[docs]deftempmsg(self,message,header=None,senders=None):
- """
- A wrapper for sending non-persistent messages.
-
- Args:
- message (str): Message to send.
- header (str, optional): Header of message to send.
- senders (Object or list, optional): Senders of message to send.
-
- """
- self.msg(message,senders=senders,header=header,keep_log=False)
[docs]defchannel_prefix(self):""" Hook method. How the channel should prefix itself for users.
- Args:
- msg (str, optional): Prefix text
- emit (bool, optional): Switches to emit mode, which usually
- means to not prefix the channel's info.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
Returns:
- prefix (str): The created channel prefix.
+ str: The channel prefix. """
- return""ifemitelse"[%s] "%self.key
[docs]defadd_user_channel_alias(self,user,alias,**kwargs):"""
- Hook method. Function used to format a list of sender names.
+ Add a personal user-alias for this channel to a given subscriber. Args:
- senders (list): Sender object names.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
+ user (Object or Account): The one to alias this channel.
+ alias (str): The desired alias.
- Returns:
- formatted_list (str): The list of names formatted appropriately.
+ Note:
+ This is tightly coupled to the default `channel` command. If you
+ change that, you need to change this as well.
+
+ We add two nicks - one is a plain `alias -> channel.key` that
+ users need to be able to reference this channel easily. The other
+ is a templated nick to easily be able to send messages to the
+ channel without needing to give the full `channel` command. The
+ structure of this nick is given by `self.channel_msg_nick_pattern`
+ and `self.channel_msg_nick_replacement`. By default it maps
+ `alias <msg> -> channel <channelname> = <msg>`, so that you can
+ for example just write `pub Hello` to send a message.
+
+ The alias created is `alias $1 -> channel channel = $1`, to allow
+ for sending to channel using the main channel command.
+
+ """
+ chan_key=self.key.lower()
+
+ # the message-pattern allows us to type the channel on its own without
+ # needing to use the `channel` command explicitly.
+ msg_nick_pattern=self.channel_msg_nick_pattern.format(alias=alias)
+ msg_nick_replacement=self.channel_msg_nick_replacement.format(channelname=chan_key)
+ user.nicks.add(msg_nick_pattern,msg_nick_replacement,category="inputline",
+ pattern_is_regex=True,**kwargs)
+
+ ifchan_key!=alias:
+ # this allows for using the alias for general channel lookups
+ user.nicks.add(alias,chan_key,category="channel",**kwargs)
+
+
[docs]@classmethod
+ defremove_user_channel_alias(cls,user,alias,**kwargs):
+ """
+ Remove a personal channel alias from a user.
+
+ Args:
+ user (Object or Account): The user to remove an alias from.
+ alias (str): The alias to remove.
+ **kwargs: Unused by default. Can be used to pass extra variables
+ into a custom implementation. Notes:
- This function exists separately so that external sources
- can use it to format source names in the same manner as
- normal object/account names.
+ The channel-alias actually consists of two aliases - one
+ channel-based one for searching channels with the alias and one
+ inputline one for doing the 'channelalias msg' - call.
+
+ This is a classmethod because it doesn't actually operate on the
+ channel instance.
+
+ It sits on the channel because the nick structure for this is
+ pretty complex and needs to be located in a central place (rather
+ on, say, the channel command). """
- ifnotsenders:
- return""
- return", ".join(senders)
[docs]defat_pre_msg(self,message,**kwargs):"""
- Hook method. Detects if the sender is posing, and modifies the
- message accordingly.
+ Called before the starting of sending the message to a receiver. This
+ is called before any hooks on the receiver itself. If this returns
+ None/False, the sending will be aborted. Args:
- msgobj (Msg or TempMsg): The message to analyze for a pose.
- sender_string (str): The name of the sender/poser.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
+ message (str): The message to send.
+ **kwargs (any): Keywords passed on from `.msg`. This includes
+ `senders`. Returns:
- string (str): A message that combines the `sender_string`
- component with `msg` in different ways depending on if a
- pose was performed or not (this must be analyzed by the
- hook).
+ str, False or None: Any custom changes made to the message. If
+ falsy, no message will be sent. """
- pose=False
- message=msgobj.message
- message_start=message.lstrip()
- ifmessage_start.startswith((":",";")):
- pose=True
- message=message[1:]
- ifnotmessage.startswith((":","'",",")):
- ifnotmessage.startswith(" "):
- message=" "+message
- ifpose:
- return"%s%s"%(sender_string,message)
+ returnmessage
+
+
[docs]defmsg(self,message,senders=None,bypass_mute=False,**kwargs):
+ """
+ Send message to channel, causing it to be distributed to all non-muted
+ subscribed users of that channel.
+
+ Args:
+ message (str): The message to send.
+ senders (Object, Account or list, optional): If not given, there is
+ no way to associate one or more senders with the message (like
+ a broadcast message or similar).
+ bypass_mute (bool, optional): If set, always send, regardless of
+ individual mute-state of subscriber. This can be used for
+ global announcements or warnings/alerts.
+ **kwargs (any): This will be passed on to all hooks. Use `no_prefix`
+ to exclude the channel prefix.
+
+ Notes:
+ The call hook calling sequence is:
+
+ - `msg = channel.at_pre_msg(message, **kwargs)` (aborts for all if return None)
+ - `msg = receiver.at_pre_channel_msg(msg, channel, **kwargs)` (aborts for receiver if return None)
+ - `receiver.at_channel_msg(msg, channel, **kwargs)`
+ - `receiver.at_post_channel_msg(msg, channel, **kwargs)``
+ Called after all receivers are processed:
+ - `channel.at_post_all_msg(message, **kwargs)`
+
+ (where the senders/bypass_mute are embedded into **kwargs for
+ later access in hooks)
+
+ """
+ senders=make_iter(senders)ifsenderselse[]
+ ifself.send_to_online_only:
+ receivers=self.subscriptions.online()else:
- return"%s: %s"%(sender_string,message)
[docs]defat_post_msg(self,message,**kwargs):"""
- Hook method. Used for formatting external messages. This is
- needed as a separate operation because the senders of external
- messages may not be in-game objects/accounts, and so cannot
- have things like custom user preferences.
+ This is called after sending to *all* valid recipients. It is normally
+ used for logging/channel history. Args:
- msgobj (Msg or TempMsg): The message to send.
- senders (list): Strings, one per sender.
- emit (bool, optional): A sender-agnostic message or not.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- transformed (str): A formatted string.
+ message (str): The message sent.
+ **kwargs (any): Keywords passed on from `msg`, including `senders`. """
- ifemitornotsenders:
- returnmsgobj.message
- senders=", ".join(senders)
- returnself.pose_transform(msgobj,senders)
-
-
[docs]defformat_message(self,msgobj,emit=False,**kwargs):
- """
- Hook method. Formats a message body for display.
-
- Args:
- msgobj (Msg or TempMsg): The message object to send.
- emit (bool, optional): The message is agnostic of senders.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- transformed (str): The formatted message.
-
- """
- # We don't want to count things like external sources as senders for
- # the purpose of constructing the message string.
- senders=[senderforsenderinmsgobj.sendersifhasattr(sender,"key")]
- ifnotsenders:
- emit=True
- ifemit:
- returnmsgobj.message
- else:
- senders=[sender.keyforsenderinmsgobj.senders]
- senders=", ".join(senders)
- returnself.pose_transform(msgobj,senders)
+ # save channel history to log file
+ log_file=self.get_log_filename()
+ iflog_file:
+ senders=",".join(sender.keyforsenderinkwargs.get("senders",[]))
+ senders=f"{senders}: "ifsenderselse""
+ message=f"{senders}{message}"
+ logger.log_file(message,log_file)
[docs]defpre_join_channel(self,joiner,**kwargs):"""
@@ -638,8 +649,13 @@
**kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default).
+ Notes:
+ By default this adds the needed channel nicks to the joiner.
+
"""
- pass
[docs]defpre_leave_channel(self,leaver,**kwargs):"""
@@ -667,36 +683,12 @@
overriding the call (unused by default). """
- pass
-
-
[docs]defpre_send_message(self,msg,**kwargs):
- """
- Hook method. Runs before a message is sent to the channel and
- should return the message object, after any transformations.
- If the message is to be discarded, return a false value.
-
- Args:
- msg (Msg or TempMsg): Message to send.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- Returns:
- result (Msg, TempMsg or bool): If False, abort send.
-
- """
- returnmsg
-
-
[docs]defpost_send_message(self,msg,**kwargs):
- """
- Hook method. Run after a message is sent to the channel.
-
- Args:
- msg (Msg or TempMsg): Message sent.
- **kwargs (dict): Arbitrary, optional arguments for users
- overriding the call (unused by default).
-
- """
- pass
# Used by Django Sites/Admin
- get_absolute_url=web_get_detail_url
+ get_absolute_url=web_get_detail_url
+
+ # TODO Evennia 1.0+ removed hooks. Remove in 1.1.
+
[docs]defmessage_transform(self,*args,**kwargs):
+ raiseRuntimeError("Channel.message_transform is no longer used in 1.0+. "
+ "Use Account/Object.at_pre_channel_msg instead.")
+
+
[docs]defdistribute_message(self,msgobj,online=False,**kwargs):
+ raiseRuntimeError("Channel.distribute_message is no longer used in 1.0+.")
+
+
[docs]defformat_senders(self,senders=None,**kwargs):
+ raiseRuntimeError("Channel.format_senders is no longer used in 1.0+. "
+ "Use Account/Object.at_pre_channel_msg instead.")
+
+
[docs]defpose_transform(self,msgobj,sender_string,**kwargs):
+ raiseRuntimeError("Channel.pose_transform is no longer used in 1.0+. "
+ "Use Account/Object.at_pre_channel_msg instead.")
+
+
[docs]defformat_external(self,msgobj,senders,emit=False,**kwargs):
+ raiseRuntimeError("Channel.format_external is no longer used in 1.0+. "
+ "Use Account/Object.at_pre_channel_msg instead.")
+
+
[docs]defformat_message(self,msgobj,emit=False,**kwargs):
+ raiseRuntimeError("Channel.format_message is no longer used in 1.0+. "
+ "Use Account/Object.at_pre_channel_msg instead.")
+
+
[docs]defpre_send_message(self,msg,**kwargs):
+ raiseRuntimeError("Channel.pre_send_message was renamed to Channel.at_pre_msg.")
+
+
[docs]defpost_send_message(self,msg,**kwargs):
+ raiseRuntimeError("Channel.post_send_message was renamed to Channel.at_post_msg.")
[docs]defget_messages_by_sender(self,sender):""" Get all messages sent by one entity - this could be either a account or an object Args: sender (Account or Object): The sender of the message.
- exclude_channel_messages (bool, optional): Only return messages
- not aimed at a channel (that is, private tells for example) Returns:
- messages (list): List of matching messages
+ QuerySet: Matching messages. Raises: CommError: For incorrect sender types. """obj,typ=identify_object(sender)
- ifexclude_channel_messages:
- # explicitly exclude channel recipients
- iftyp=="account":
- returnlist(
- self.filter(db_sender_accounts=obj,db_receivers_channels__isnull=True).exclude(
- db_hide_from_accounts=obj
- )
- )
- eliftyp=="object":
- returnlist(
- self.filter(db_sender_objects=obj,db_receivers_channels__isnull=True).exclude(
- db_hide_from_objects=obj
- )
- )
- else:
- raiseCommError
+ iftyp=="account":
+ returnself.filter(db_sender_accounts=obj).exclude(db_hide_from_accounts=obj)
+ eliftyp=="object":
+ returnself.filter(db_sender_objects=obj).exclude(db_hide_from_objects=obj)
+ eliftyp=="script":
+ returnself.filter(db_sender_scripts=obj)else:
- # get everything, channel or not
- iftyp=="account":
- returnlist(self.filter(db_sender_accounts=obj).exclude(db_hide_from_accounts=obj))
- eliftyp=="object":
- returnlist(self.filter(db_sender_objects=obj).exclude(db_hide_from_objects=obj))
- else:
- raiseCommError
+ raiseCommError
[docs]defget_messages_by_receiver(self,recipient):"""
@@ -251,7 +244,7 @@
recipient (Object, Account or Channel): The recipient of the messages to search for. Returns:
- messages (list): Matching messages.
+ Queryset: Matching messages. Raises: CommError: If the `recipient` is not of a valid type.
@@ -259,26 +252,14 @@
"""obj,typ=identify_object(recipient)iftyp=="account":
- returnlist(self.filter(db_receivers_accounts=obj).exclude(db_hide_from_accounts=obj))
+ returnself.filter(db_receivers_accounts=obj).exclude(db_hide_from_accounts=obj)eliftyp=="object":
- returnlist(self.filter(db_receivers_objects=obj).exclude(db_hide_from_objects=obj))
- eliftyp=="channel":
- returnlist(self.filter(db_receivers_channels=obj).exclude(db_hide_from_channels=obj))
+ returnself.filter(db_receivers_objects=obj).exclude(db_hide_from_objects=obj)
+ eliftyp=='script':
+ returnself.filter(db_receivers_scripts=obj)else:raiseCommError
-
[docs]defget_messages_by_channel(self,channel):
- """
- Get all persistent messages sent to one channel.
-
- Args:
- channel (Channel): The channel to find messages for.
-
- Returns:
- messages (list): Persistent Msg objects saved for this channel.
-
- """
- returnself.filter(db_receivers_channels=channel).exclude(db_hide_from_channels=channel)
[docs]defsearch_message(self,sender=None,receiver=None,freetext=None,dbref=None):"""
@@ -286,7 +267,7 @@
one of the arguments must be given to do a search. Args:
- sender (Object or Account, optional): Get messages sent by a particular account or object
+ sender (Object, Account or Script, optional): Get messages sent by a particular sender. receiver (Object, Account or Channel, optional): Get messages received by a certain account,object or channel freetext (str): Search for a text string in a message. NOTE:
@@ -297,40 +278,45 @@
always gives only one match. Returns:
- messages (list or Msg): A list of message matches or a single match if `dbref` was given.
+ Queryset: Message matches. """# unique msg idifdbref:
- msg=self.objects.filter(id=dbref)
- ifmsg:
- returnmsg[0]
+ returnself.objects.filter(id=dbref)# We use Q objects to gradually build up the query - this way we only# need to do one database lookup at the end rather than gradually# refining with multiple filter:s. Django Note: Q objects can be# combined with & and | (=AND,OR). ~ negates the queryset
- # filter by sender
+ # filter by sender (we need __pk to avoid an error with empty Q() objects)sender,styp=identify_object(sender)
+ ifsender:
+ spk=sender.pkifstyp=="account":
- sender_restrict=Q(db_sender_accounts=sender)&~Q(db_hide_from_accounts=sender)
+ sender_restrict=Q(db_sender_accounts__pk=spk)&~Q(db_hide_from_accounts__pk=spk)elifstyp=="object":
- sender_restrict=Q(db_sender_objects=sender)&~Q(db_hide_from_objects=sender)
+ sender_restrict=Q(db_sender_objects__pk=spk)&~Q(db_hide_from_objects__pk=spk)
+ elifstyp=='script':
+ sender_restrict=Q(db_sender_scripts__pk=spk)else:sender_restrict=Q()# filter by receiverreceiver,rtyp=identify_object(receiver)
+ ifreceiver:
+ rpk=receiver.pkifrtyp=="account":
- receiver_restrict=Q(db_receivers_accounts=receiver)&~Q(
- db_hide_from_accounts=receiver
- )
+ receiver_restrict=(
+ Q(db_receivers_accounts__pk=rpk)&~Q(db_hide_from_accounts__pk=rpk))elifrtyp=="object":
- receiver_restrict=Q(db_receivers_objects=receiver)&~Q(db_hide_from_objects=receiver)
+ receiver_restrict=Q(db_receivers_objects__pk=rpk)&~Q(db_hide_from_objects__pk=rpk)
+ elifrtyp=='script':
+ receiver_restrict=Q(db_receivers_scripts__pk=rpk)elifrtyp=="channel":
- receiver_restrict=Q(db_receivers_channels=receiver)&~Q(
- db_hide_from_channels=receiver
- )
+ raiseDeprecationWarning(
+ "Msg.objects.search don't accept channel recipients since "
+ "Channels no longer accepts Msg objects.")else:receiver_restrict=Q()# filter by full text
@@ -339,7 +325,7 @@
else:fulltext_restrict=Q()# execute the query
- returnlist(self.filter(sender_restrict&receiver_restrict&fulltext_restrict))
+ returnself.filter(sender_restrict&receiver_restrict&fulltext_restrict)# back-compatibility aliasmessage_search=search_message
diff --git a/docs/1.0-dev/_modules/evennia/comms/models.html b/docs/1.0-dev/_modules/evennia/comms/models.html
index 989ca64e23..073ff9a1b9 100644
--- a/docs/1.0-dev/_modules/evennia/comms/models.html
+++ b/docs/1.0-dev/_modules/evennia/comms/models.html
@@ -76,8 +76,6 @@
_SA=object.__setattr___DA=object.__delattr__
-_CHANNELHANDLER=None
-
# ------------------------------------------------------------#
@@ -201,6 +199,7 @@
db_hide_from_objects=models.ManyToManyField("objects.ObjectDB",related_name="hide_from_objects_set",blank=True)
+ # NOTE: deprecated in 1.0. Not used for channels anymoredb_hide_from_channels=models.ManyToManyField("ChannelDB",related_name="hide_from_channels_set",blank=True)
@@ -307,9 +306,8 @@
elifclsname=="ScriptDB":self.db_sender_accounts.remove(sender)
- # receivers property
- # @property
- def__receivers_get(self):
+ @property
+ defreceivers(self):""" Getter. Allows for value = self.receivers. Returns four lists of receivers: accounts, objects, scripts and channels.
@@ -321,8 +319,8 @@
+list(self.db_receivers_channels.all()))
- # @receivers.setter
- def__receivers_set(self,receivers):
+ @receivers.setter
+ defreceivers(self,receivers):""" Setter. Allows for self.receivers = value. This appends a new receiver to the message.
@@ -342,8 +340,8 @@
elifclsname=="ChannelDB":self.db_receivers_channels.add(receiver)
- # @receivers.deleter
- def__receivers_del(self):
+ @receivers.deleter
+ defreceivers(self):"Deleter. Clears all receivers"self.db_receivers_accounts.clear()self.db_receivers_objects.clear()
@@ -351,7 +349,6 @@
self.db_receivers_channels.clear()self.save()
- receivers=property(__receivers_get,__receivers_set,__receivers_del)
[docs]defremove_receiver(self,receivers):"""
@@ -645,9 +642,6 @@
no hooks will be called. """
- global_CHANNELHANDLER
- ifnot_CHANNELHANDLER:
- fromevennia.comms.channelhandlerimportCHANNEL_HANDLERas_CHANNELHANDLERforsubscriberinmake_iter(entity):ifsubscriber:clsname=subscriber.__dbclass__.__name__
@@ -656,7 +650,6 @@
self.obj.db_object_subscriptions.add(subscriber)elifclsname=="AccountDB":self.obj.db_account_subscriptions.add(subscriber)
- _CHANNELHANDLER._cached_cmdsets.pop(subscriber,None)self._recache()defremove(self,entity):
@@ -668,9 +661,6 @@
entities to un-subscribe from the channel. """
- global_CHANNELHANDLER
- ifnot_CHANNELHANDLER:
- fromevennia.comms.channelhandlerimportCHANNEL_HANDLERas_CHANNELHANDLERforsubscriberinmake_iter(entity):ifsubscriber:clsname=subscriber.__dbclass__.__name__
@@ -679,7 +669,6 @@
self.obj.db_account_subscriptions.remove(entity)elifclsname=="ObjectDB":self.obj.db_object_subscriptions.remove(entity)
- _CHANNELHANDLER._cached_cmdsets.pop(subscriber,None)self._recache()defall(self):
diff --git a/docs/1.0-dev/_modules/evennia/contrib/crafting/crafting.html b/docs/1.0-dev/_modules/evennia/contrib/crafting/crafting.html
index ee0a0ef1f8..a61ea8df5c 100644
--- a/docs/1.0-dev/_modules/evennia/contrib/crafting/crafting.html
+++ b/docs/1.0-dev/_modules/evennia/contrib/crafting/crafting.html
@@ -1000,7 +1000,6 @@
things in the current location, like a furnace, windmill or anvil. """
-
key="craft"locks="cmd:all()"help_category="General"
diff --git a/docs/1.0-dev/_modules/evennia/contrib/crafting/example_recipes.html b/docs/1.0-dev/_modules/evennia/contrib/crafting/example_recipes.html
index f8da613a56..739a5b117a 100644
--- a/docs/1.0-dev/_modules/evennia/contrib/crafting/example_recipes.html
+++ b/docs/1.0-dev/_modules/evennia/contrib/crafting/example_recipes.html
@@ -141,7 +141,7 @@
"You work and work but you are not happy with the result. You need to start over.")
- defdo_craft(self,**kwargs):
+ defcraft(self,**kwargs):""" Making a sword blade takes skill. Here we emulate this by introducing a random chance of failure (in a real game this could be a skill check
@@ -168,7 +168,7 @@
ifrandom.random()<0.8:# 80% chance of success. This will spawn the sword and show# success-message.
- returnsuper().do_craft(**kwargs)
+ returnsuper().craft(**kwargs)else:# fail and show failed messagereturnNone
diff --git a/docs/1.0-dev/_modules/evennia/contrib/crafting/tests.html b/docs/1.0-dev/_modules/evennia/contrib/crafting/tests.html
index 709ff4604b..375bcb5985 100644
--- a/docs/1.0-dev/_modules/evennia/contrib/crafting/tests.html
+++ b/docs/1.0-dev/_modules/evennia/contrib/crafting/tests.html
@@ -255,7 +255,7 @@
self.assertIsNotNone(self.tool1.pk)self.assertIsNotNone(self.tool2.pk)
[docs]deftest_seed__success(self):"""Test seed helper classmethod"""# needed for other dbs to pass seed
diff --git a/docs/1.0-dev/_modules/evennia/contrib/slow_exit.html b/docs/1.0-dev/_modules/evennia/contrib/slow_exit.html
index 0d9adfbeee..3fb5d70310 100644
--- a/docs/1.0-dev/_modules/evennia/contrib/slow_exit.html
+++ b/docs/1.0-dev/_modules/evennia/contrib/slow_exit.html
@@ -112,11 +112,11 @@
traversing_object.msg("You start moving %s at a %s."%(self.key,move_speed))# create a delayed movement
- deferred=utils.delay(move_delay,move_callback)
+ t=utils.delay(move_delay,move_callback)# we store the deferred on the character, this will allow us# to abort the movement. We must use an ndb here since# deferreds cannot be pickled.
- traversing_object.ndb.currently_moving=deferred
+"""
+The filehelp-system allows for defining help files outside of the game. These
+will be treated as non-command help entries and displayed in the same way as
+help entries created using the `sethelp` default command. After changing an
+entry on-disk you need to reload the server to have the change show in-game.
+
+An filehelp file is a regular python modules with dicts representing each help
+entry. If a list `HELP_ENTRY_DICTS` is found in the module, this should be a list of
+dicts. Otherwise *all* top-level dicts in the module will be assumed to be a
+help-entry dict.
+
+Each help-entry dict is on the form
+::
+
+ {'key': <str>,
+ 'category': <str>, # optional, otherwise settings.DEFAULT_HELP_CATEGORY
+ 'aliases': <list>, # optional
+ 'text': <str>}``
+
+where the ``category`` is optional and the ``text`` should be formatted on the
+same form as other help entry-texts and contain ``# subtopics`` as normal.
+
+New help-entry modules are added to the system by providing the python-path to
+the module to `settings.FILE_HELP_ENTRY_MODULES`. Note that if same-key entries are
+added, entries in latter modules will override that of earlier ones. Use
+``settings.DEFAULT_HELP_CATEGORY`` to customize what category is used if
+not set explicitly.
+
+An example of the contents of a module:
+::
+
+ help_entry1 = {
+ "key": "The Gods", # case-insensitive, can be searched by 'gods' as well
+ "aliases": ['pantheon', 'religion']
+ "category": "Lore",
+ "text": '''
+ The gods formed the world ...
+
+ # Subtopics
+
+ ## Pantheon
+
+ ...
+
+ ### God of love
+
+ ...
+
+ ### God of war
+
+ ...
+
+ '''
+ }
+
+
+ HELP_ENTRY_DICTS = [
+ help_entry1,
+ ...
+ ]
+
+----
+
+"""
+
+fromdataclassesimportdataclass
+fromdjango.confimportsettings
+fromevennia.utils.utilsimport(
+ variable_from_module,make_iter,all_from_module)
+fromevennia.utilsimportlogger
+
+_DEFAULT_HELP_CATEGORY=settings.DEFAULT_HELP_CATEGORY
+
+
+
[docs]@dataclass
+classFileHelpEntry:
+ """
+ Represents a help entry read from file. This mimics the api of the
+ database-bound HelpEntry so that they can be used interchangeably in the
+ help command.
+
+ """
+ key:str
+ aliases:list
+ help_category:str
+ entrytext:str
+
+ @property
+ defsearch_index_entry(self):
+ """
+ Property for easily retaining a search index entry for this object.
+
+ """
+ return{
+ "key":self.key,
+ "aliases":" ".join(self.aliases),
+ "category":self.help_category,
+ "tags":"",
+ "text":self.entrytext,
+ }
+
+
+
+
[docs]classFileHelpStorageHandler:
+ """
+ This reads and stores help entries for quick access. By default
+ it reads modules from `settings.FILE_HELP_ENTRY_MODULES`.
+
+ Note that this is not meant to any searching/lookup - that is all handled
+ by the help command.
+ """
+
+
[docs]defall(self,return_dict=False):
+ """
+ Get all help entries.
+
+ Args:
+ return_dict (bool): Return a dict ``{key: FileHelpEntry,...}``. Otherwise,
+ return a list of ``FileHelpEntry`.
+
+ Returns:
+ dict or list: Depending on the setting of ``return_dict``.
+
+ """
+ returnself.help_entries_dictifreturn_dictelseself.help_entries
+
+
+# singleton to hold the loaded help entries
+FILE_HELP_ENTRIES=FileHelpStorageHandler()
+
+"""
+Resources for indexing help entries and for splitting help entries into
+sub-categories.
+
+This is used primarily by the default `help` command.
+
+"""
+importre
+
+# these are words that Lunr normally ignores but which we want to find
+# since we use them (e.g. as command names).
+# Lunr's default word list is found here:
+# https://github.com/yeraydiazdiaz/lunr.py/blob/master/lunr/stop_word_filter.py
+_LUNR_STOP_WORD_FILTER_EXCEPTIONS=("about","might")
+
+_LUNR=None
+_LUNR_EXCEPTION=None
+
+_LUNR_GET_BUILDER=None
+_LUNR_BUILDER_PIPELINE=None
+
+_RE_HELP_SUBTOPICS_START=re.compile(
+ r"^\s*?#\s*?subtopics\s*?$",re.I+re.M)
+_RE_HELP_SUBTOPIC_SPLIT=re.compile(r"^\s*?(\#{2,6}\s*?\w+?[a-z0-9 \-\?!,\.]*?)$",re.M+re.I)
+_RE_HELP_SUBTOPIC_PARSE=re.compile(
+ r"^(?P<nesting>\#{2,6})\s*?(?P<name>.*?)$",re.I+re.M)
+
+MAX_SUBTOPIC_NESTING=5
+
+
+
[docs]defhelp_search_with_index(query,candidate_entries,suggestion_maxnum=5,fields=None):
+ """
+ Lunr-powered fast index search and suggestion wrapper. See https://lunrjs.com/.
+
+ Args:
+ query (str): The query to search for.
+ candidate_entries (list): This is the body of possible entities to search. Each
+ must have a property `.search_index_entry` that returns a dict with all
+ keys in the `fields` arg.
+ suggestion_maxnum (int): How many matches to allow at most in a multi-match.
+ fields (list, optional): A list of Lunr field mappings
+ ``{"field_name": str, "boost": int}``. See the Lunr documentation
+ for more details. The field name must exist in the dicts returned
+ by `.search_index_entry` of the candidates. If not given, a default setup
+ is used, prefering keys > aliases > category > tags.
+ Returns:
+ tuple: A tuple (matches, suggestions), each a list, where the `suggestion_maxnum` limits
+ how many suggestions are included.
+
+ """
+ global_LUNR,_LUNR_EXCEPTION,_LUNR_BUILDER_PIPELINE,_LUNR_GET_BUILDER
+ ifnot_LUNR:
+ # we have to delay-load lunr because it messes with logging if it's imported
+ # before twisted's logging has been set up
+ fromlunrimportlunras_LUNR
+ fromlunr.exceptionsimportQueryParseErroras_LUNR_EXCEPTION
+ fromlunrimportget_default_builderas_LUNR_GET_BUILDER
+ fromlunrimportstop_word_filter
+ fromlunr.stemmerimportstemmer
+ fromlunr.trimmerimporttrimmer
+
+ # pre-create a lunr index-builder pipeline where we've removed some of
+ # the stop-words from the default in lunr.
+
+ stop_words=stop_word_filter.WORDS
+
+ forignore_wordin_LUNR_STOP_WORD_FILTER_EXCEPTIONS:
+ try:
+ stop_words.remove(ignore_word)
+ exceptValueError:
+ pass
+
+ custom_stop_words_filter=stop_word_filter.generate_stop_word_filter(stop_words)
+ _LUNR_BUILDER_PIPELINE=(trimmer,custom_stop_words_filter,stemmer)
+
+
+ indx=[cnd.search_index_entryforcndincandidate_entries]
+ mapping={indx[ix]["key"]:candforix,candinenumerate(candidate_entries)}
+
+ ifnotfields:
+ fields=[
+ {"field_name":"key","boost":10},
+ {"field_name":"aliases","boost":9},
+ {"field_name":"category","boost":8},
+ {"field_name":"tags","boost":5},
+ ]
+
+ # build the search index
+ builder=_LUNR_GET_BUILDER()
+ builder.pipeline.reset()
+ builder.pipeline.add(*_LUNR_BUILDER_PIPELINE)
+
+ search_index=_LUNR(
+ ref="key",
+ fields=fields,
+ documents=indx,
+ builder=builder
+ )
+
+ try:
+ matches=search_index.search(query)[:suggestion_maxnum]
+ except_LUNR_EXCEPTION:
+ # this is a user-input problem
+ matches=[]
+
+ # matches (objs), suggestions (strs)
+ return(
+ [mapping[match["ref"]]formatchinmatches],
+ [str(match["ref"])formatchinmatches],# + f" (score {match['score']})") # good debug
+ )
+
+
+
[docs]defparse_entry_for_subcategories(entry):
+ """
+ Parse a command docstring for special sub-category blocks:
+
+ Args:
+ entry (str): A help entry to parse
+
+ Returns:
+ dict: The dict is a mapping that splits the entry into subcategories. This
+ will always hold a key `None` for the main help entry and
+ zero or more keys holding the subcategories. Each is itself
+ a dict with a key `None` for the main text of that subcategory
+ followed by any sub-sub-categories down to a max-depth of 5.
+
+ Example:
+ ::
+
+ '''
+ Main topic text
+
+ # SUBTOPICS
+
+ ## foo
+
+ A subcategory of the main entry, accessible as `help topic foo`
+ (or using /, like `help topic/foo`)
+
+ ## bar
+
+ Another subcategory, accessed as `help topic bar`
+ (or `help topic/bar`)
+
+ ### moo
+
+ A subcategory of bar, accessed as `help bar moo`
+ (or `help bar/moo`)
+
+ #### dum
+
+ A subcategory of moo, accessed `help bar moo dum`
+ (or `help bar/moo/dum`)
+
+ '''
+
+ This will result in this returned entry structure:
+ ::
+
+ {
+ None: "Main topic text":
+ "foo": {
+ None: "main topic/foo text"
+ },
+ "bar": {
+ None: "Main topic/bar text",
+ "moo": {
+ None: "topic/bar/moo text"
+ "dum": {
+ None: "topic/bar/moo/dum text"
+ }
+ }
+ }
+ }
+
+ """
+ topic,*subtopics=_RE_HELP_SUBTOPICS_START.split(entry,maxsplit=1)
+ structure={None:topic.strip('\n')}
+
+ ifsubtopics:
+ subtopics=subtopics[0]
+ else:
+ returnstructure
+
+ keypath=[]
+ current_nesting=0
+ subtopic=None
+
+ # from evennia import set_trace;set_trace()
+ forpartin_RE_HELP_SUBTOPIC_SPLIT.split(subtopics.strip()):
+
+ subtopic_match=_RE_HELP_SUBTOPIC_PARSE.match(part.strip())
+ ifsubtopic_match:
+ # a new sub(-sub..) category starts.
+ mdict=subtopic_match.groupdict()
+ subtopic=mdict['name'].lower().strip()
+ new_nesting=len(mdict['nesting'])-1
+
+ ifnew_nesting>MAX_SUBTOPIC_NESTING:
+ raiseRuntimeError(
+ f"Can have max {MAX_SUBTOPIC_NESTING} levels of nested help subtopics.")
+
+ nestdiff=new_nesting-current_nesting
+ ifnestdiff<0:
+ # jumping back up in nesting
+ for_inrange(abs(nestdiff)+1):
+ try:
+ keypath.pop()
+ exceptIndexError:
+ pass
+ elifnestdiff==0:
+ # don't add a deeper nesting but replace the current
+ try:
+ keypath.pop()
+ exceptIndexError:
+ pass
+ keypath.append(subtopic)
+ current_nesting=new_nesting
+ else:
+ # an entry belonging to a subtopic - find the nested location
+ dct=structure
+ ifnotkeypathandsubtopicisnotNone:
+ structure[subtopic]=part
+ else:
+ forkeyinkeypath:
+ ifkeyindct:
+ dct=dct[key]
+ else:
+ dct[key]={
+ None:part
+ }
+ returnstructure
+
+
+
\ No newline at end of file
diff --git a/docs/1.0-dev/_modules/evennia/objects/objects.html b/docs/1.0-dev/_modules/evennia/objects/objects.html
index d1db9b4e36..d00f9ae6a4 100644
--- a/docs/1.0-dev/_modules/evennia/objects/objects.html
+++ b/docs/1.0-dev/_modules/evennia/objects/objects.html
@@ -934,7 +934,7 @@
# Before the move, call eventual pre-commands.ifmove_hooks:try:
- ifnotself.at_before_move(destination):
+ ifnotself.at_before_move(destination,**kwargs):returnFalseexceptExceptionaserr:logerr(errtxt%"at_before_move()",err)
@@ -946,7 +946,7 @@
# Call hook on source locationifmove_hooksandsource_location:try:
- source_location.at_object_leave(self,destination)
+ source_location.at_object_leave(self,destination,**kwargs)exceptExceptionaserr:logerr(errtxt%"at_object_leave()",err)returnFalse
@@ -978,7 +978,7 @@
# Perform eventual extra commands on the receiving location# (the object has already arrived at this point)try:
- destination.at_object_receive(self,source_location)
+ destination.at_object_receive(self,source_location,**kwargs)exceptExceptionaserr:logerr(errtxt%"at_object_receive()",err)returnFalse
@@ -987,7 +987,7 @@
# (usually calling 'look')ifmove_hooks:try:
- self.at_after_move(source_location)
+ self.at_after_move(source_location,**kwargs)exceptExceptionaserr:logerr(errtxt%"at_after_move",err)returnFalse
diff --git a/docs/1.0-dev/_modules/evennia/prototypes/prototypes.html b/docs/1.0-dev/_modules/evennia/prototypes/prototypes.html
index ef29d57388..9b0b8b3b8c 100644
--- a/docs/1.0-dev/_modules/evennia/prototypes/prototypes.html
+++ b/docs/1.0-dev/_modules/evennia/prototypes/prototypes.html
@@ -60,6 +60,7 @@
fromevennia.utils.evmoreimportEvMorefromevennia.utils.utilsimport(all_from_module,
+ variable_from_module,make_iter,is_iter,dbid_to_obj,
@@ -200,11 +201,30 @@
# to remove a default prototype, override it with an empty dict.# internally we store as (key, desc, locks, tags, prototype_dict)prots=[]
- forvariable_name,protinall_from_module(mod).items():
- ifisinstance(prot,dict):
- if"prototype_key"notinprot:
- prot["prototype_key"]=variable_name.lower()
+
+ prototype_list=variable_from_module(mod,"PROTOTYPE_LIST")
+ ifprototype_list:
+ # found mod.PROTOTYPE_LIST - this should be a list of valid
+ # prototype dicts that must have 'prototype_key' set.
+ forprotinprototype_list:
+ ifnotisinstance(prot,dict):
+ logger.log_err(f"Prototype read from {mod}.PROTOTYPE_LIST "
+ f"is not a dict (skipping): {prot}")
+ continue
+ elif"prototype_key"notinprot:
+ logger.log_err(f"Prototype read from {mod}.PROTOTYPE_LIST "
+ f"is missing the 'prototype_key' (skipping): {prot}")
+ continueprots.append((prot["prototype_key"],homogenize_prototype(prot)))
+ else:
+ # load all global dicts in module as prototypes. If the prototype_key
+ # is not given, the variable name will be used.
+ forvariable_name,protinall_from_module(mod).items():
+ ifisinstance(prot,dict):
+ if"prototype_key"notinprot:
+ prot["prototype_key"]=variable_name.lower()
+ prots.append((prot["prototype_key"],homogenize_prototype(prot)))
+
# assign module path to each prototype_key for easy reference_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower():modforprototype_key,_inprots})# make sure the prototype contains all meta info
diff --git a/docs/1.0-dev/_modules/evennia/scripts/taskhandler.html b/docs/1.0-dev/_modules/evennia/scripts/taskhandler.html
index 85000da09e..5b051972df 100644
--- a/docs/1.0-dev/_modules/evennia/scripts/taskhandler.html
+++ b/docs/1.0-dev/_modules/evennia/scripts/taskhandler.html
@@ -47,7 +47,9 @@
fromdatetimeimportdatetime,timedeltafromtwisted.internetimportreactor
+frompickleimportPickleErrorfromtwisted.internet.taskimportdeferLater
+fromtwisted.internet.deferimportCancelledErrorasDefCancelledErrorfromevennia.server.modelsimportServerConfigfromevennia.utils.loggerimportlog_errfromevennia.utils.dbserializeimportdbserialize,dbunserialize
@@ -55,31 +57,213 @@
TASK_HANDLER=None
+
[docs]classTaskHandlerTask:
+ """An object to represent a single TaskHandler task.
+
+ Instance Attributes:
+ task_id (int): the global id for this task
+ deferred (deferred): a reference to this task's deferred
+ Property Attributes:
+ paused (bool): check if the deferred instance of a task has been paused.
+ called(self): A task attribute to check if the deferred instance of a task has been called.
+
+ Methods:
+ pause(): Pause the callback of a task.
+ unpause(): Process all callbacks made since pause() was called.
+ do_task(): Execute the task (call its callback).
+ call(): Call the callback of this task.
+ remove(): Remove a task without executing it.
+ cancel(): Stop a task from automatically executing.
+ active(): Check if a task is active (has not been called yet).
+ exists(): Check if a task exists.
+ get_id(): Returns the global id for this task. For use with
+
+ """
+
+
[docs]defget_deferred(self):
+ """Return the instance of the deferred the task id is using.
+
+ Returns:
+ bool or deferred: An instance of a deferred or False if there is no task with the id.
+ None is returned if there is no deferred affiliated with this id.
+
+ """
+ returnTASK_HANDLER.get_deferred(self.task_id)
+
+
[docs]defpause(self):
+ """Pause the callback of a task.
+ To resume use TaskHandlerTask.unpause
+ """
+ d=self.deferred
+ ifd:
+ d.pause()
+
+
[docs]defunpause(self):
+ """Unpause a task, run the task if it has passed delay time."""
+ d=self.deferred
+ ifd:
+ d.unpause()
+
+ @property
+ defpaused(self):
+ """A task attribute to check if the deferred instance of a task has been paused.
+
+ This exists to mock usage of a twisted deferred object.
+
+ Returns:
+ bool or None: True if the task was properly paused. None if the task does not have
+ a deferred instance.
+
+ """
+ d=self.deferred
+ ifd:
+ returnd.paused
+ else:
+ returnNone
+
+
[docs]defdo_task(self):
+ """Execute the task (call its callback).
+ If calling before timedelay, cancel the deferred instance affliated to this task.
+ Remove the task from the dictionary of current tasks on a successful
+ callback.
+
+ Returns:
+ bool or any: Set to `False` if the task does not exist in task
+ handler. Otherwise it will be the return of the task's callback.
+
+ """
+ returnTASK_HANDLER.do_task(self.task_id)
+
+
[docs]defcall(self):
+ """Call the callback of a task.
+ Leave the task unaffected otherwise.
+ This does not use the task's deferred instance.
+ The only requirement is that the task exist in task handler.
+
+ Returns:
+ bool or any: Set to `False` if the task does not exist in task
+ handler. Otherwise it will be the return of the task's callback.
+
+ """
+ returnTASK_HANDLER.call_task(self.task_id)
+
+
[docs]defremove(self):
+ """Remove a task without executing it.
+ Deletes the instance of the task's deferred.
+
+ Args:
+ task_id (int): an existing task ID.
+
+ Returns:
+ bool: True if the removal completed successfully.
+
+ """
+ returnTASK_HANDLER.remove(self.task_id)
+
+
[docs]defcancel(self):
+ """Stop a task from automatically executing.
+ This will not remove the task.
+
+ Returns:
+ bool: True if the cancel completed successfully.
+ False if the cancel did not complete successfully.
+
+ """
+ returnTASK_HANDLER.cancel(self.task_id)
+
+
[docs]defactive(self):
+ """Check if a task is active (has not been called yet).
+
+ Returns:
+ bool: True if a task is active (has not been called yet). False if
+ it is not (has been called) or if the task does not exist.
+
+ """
+ returnTASK_HANDLER.active(self.task_id)
+
+ @property
+ defcalled(self):
+ """
+ A task attribute to check if the deferred instance of a task has been called.
+
+ This exists to mock usage of a twisted deferred object.
+ It will not set to True if Task.call has been called. This only happens if
+ task's deferred instance calls the callback.
+
+ Returns:
+ bool: True if the deferred instance of this task has called the callback.
+ False if the deferred instnace of this task has not called the callback.
+
+ """
+ d=self.deferred
+ ifd:
+ returnd.called
+ else:
+ returnNone
+
+
[docs]defexists(self):
+ """Check if a task exists.
+ Most task handler methods check for existence for you.
+
+ Returns:
+ bool: True the task exists False if it does not.
+
+ """
+ returnTASK_HANDLER.exists(self.task_id)
+
+
[docs]defget_id(self):
+ """ Returns the global id for this task. For use with
+ `evennia.scripts.taskhandler.TASK_HANDLER`.
+
+ Returns:
+ task_id (int): global task id for this task.
+
+ """
+ returnself.task_id
+
+
[docs]classTaskHandler(object):
- """
- A light singleton wrapper allowing to access permanent tasks.
+ """A light singleton wrapper allowing to access permanent tasks. When `utils.delay` is called, the task handler is used to create
- the task. If `utils.delay` is called with `persistent=True`, the
- task handler stores the new task and saves.
+ the task.
- It's easier to access these tasks (should it be necessary) using
- `evennia.scripts.taskhandler.TASK_HANDLER`, which contains one
- instance of this class, and use its `add` and `remove` methods.
+ Task handler will automatically remove uncalled but canceled from task
+ handler. By default this will not occur until a canceled task
+ has been uncalled for 60 second after the time it should have been called.
+ To adjust this time use TASK_HANDLER.stale_timeout. If stale_timeout is 0
+ stale tasks will not be automatically removed.
+ This is not done on a timer. I is done as new tasks are added or the load method is called. """
+ self.to_save={}
+ self.clock=reactor
+ # number of seconds before an uncalled canceled task is removed from TaskHandler
+ self.stale_timeout=60
+ self._now=False# used in unit testing to manually set now time
[docs]defload(self):"""Load from the ServerConfig.
- Note:
- This should be automatically called when Evennia starts.
- It populates `self.tasks` according to the ServerConfig.
+ This should be automatically called when Evennia starts.
+ It populates `self.tasks` according to the ServerConfig. """to_save=False
@@ -100,16 +284,42 @@
continuecallback=getattr(obj,method)
- self.tasks[task_id]=(date,callback,args,kwargs)
+ self.tasks[task_id]=(date,callback,args,kwargs,True,None)
+ ifself.stale_timeout>0:# cleanup stale tasks.
+ self.clean_stale_tasks()ifto_save:self.save()
+
[docs]defclean_stale_tasks(self):
+ """remove uncalled but canceled from task handler.
+
+ By default this will not occur until a canceled task
+ has been uncalled for 60 second after the time it should have been called.
+ To adjust this time use TASK_HANDLER.stale_timeout.
+
+ """
+ clean_ids=[]
+ fortask_id,(date,callback,args,kwargs,persistent,_)inself.tasks.items():
+ ifnotself.active(task_id):
+ stale_date=date+timedelta(seconds=self.stale_timeout)
+ # if a now time is provided use it (intended for unit testing)
+ now=self._nowifself._nowelsedatetime.now()
+ # the task was canceled more than stale_timeout seconds ago
+ ifnow>stale_date:
+ clean_ids.append(task_id)
+ fortask_idinclean_ids:
+ self.remove(task_id)
+ returnTrue
+
[docs]defsave(self):"""Save the tasks in ServerConfig."""
- fortask_id,(date,callback,args,kwargs)inself.tasks.items():
+
+ fortask_id,(date,callback,args,kwargs,persistent,_)inself.tasks.items():iftask_idinself.to_save:continue
+ ifnotpersistent:
+ continueifgetattr(callback,"__self__",None):# `callback` is an instance method
@@ -120,52 +330,71 @@
# Check if callback can be pickled. args and kwargs have been checkedsafe_callback=None
+
+ self.to_save[task_id]=dbserialize((date,callback,args,kwargs))
+ ServerConfig.objects.conf("delayed_tasks",self.to_save)
+
+
[docs]defadd(self,timedelay,callback,*args,**kwargs):
+ """Add a new task.
+
+ If the persistent kwarg is truthy:
+ The callback, args and values for kwarg will be serialized. Type
+ and attribute errors during the serialization will be logged,
+ but will not throw exceptions.
+ For persistent tasks do not use memory references in the callback
+ function or arguments. After a restart those memory references are no
+ longer accurate.
+
+ Args:
+ timedelay (int or float): time in seconds before calling the callback.
+ callback (function or instance method): the callback itself
+ any (any): any additional positional arguments to send to the callback
+ *args: positional arguments to pass to callback.
+ **kwargs: keyword arguments to pass to callback.
+ - persistent (bool, optional): persist the task (stores it).
+ Persistent key and value is removed from kwargs it will
+ not be passed to callback.
+
+ Returns:
+ TaskHandlerTask: An object to represent a task.
+ Reference evennia.scripts.taskhandler.TaskHandlerTask for complete details.
+
+ """
+ # set the completion time
+ # Only used on persistent tasks after a restart
+ now=datetime.now()
+ delta=timedelta(seconds=timedelay)
+ comp_time=now+delta
+ # get an open task id
+ used_ids=list(self.tasks.keys())
+ task_id=1
+ whiletask_idinused_ids:
+ task_id+=1
+
+ # record the task to the tasks dictionary
+ persistent=kwargs.get("persistent",False)
+ if"persistent"inkwargs:
+ delkwargs["persistent"]
+ ifpersistent:
+ safe_args=[]
+ safe_kwargs={}
+
+ # an unsaveable callback should immediately aborttry:dbserialize(callback)
- except(TypeError,AttributeError):
+ except(TypeError,AttributeError,PickleError):raiseValueError("the specified callback {} cannot be pickled. ""It must be a top-level function in a module or an ""instance method.".format(callback))
- else:
- safe_callback=callback
-
- self.to_save[task_id]=dbserialize((date,safe_callback,args,kwargs))
- ServerConfig.objects.conf("delayed_tasks",self.to_save)
-
-
[docs]defadd(self,timedelay,callback,*args,**kwargs):
- """Add a new persistent task in the configuration.
-
- Args:
- timedelay (int or float): time in sedconds before calling the callback.
- callback (function or instance method): the callback itself
- any (any): any additional positional arguments to send to the callback
-
- Keyword Args:
- persistent (bool, optional): persist the task (store it).
- any (any): additional keyword arguments to send to the callback
-
- """
- persistent=kwargs.get("persistent",False)
- ifpersistent:
- delkwargs["persistent"]
- now=datetime.now()
- delta=timedelta(seconds=timedelay)
-
- # Choose a free task_id
- safe_args=[]
- safe_kwargs={}
- used_ids=list(self.tasks.keys())
- task_id=1
- whiletask_idinused_ids:
- task_id+=1
+ return# Check that args and kwargs contain picklable informationforarginargs:try:dbserialize(arg)
- except(TypeError,AttributeError):
+ except(TypeError,AttributeError,PickleError):log_err("The positional argument {} cannot be ""pickled and will not be present in the arguments "
@@ -177,7 +406,7 @@
forkey,valueinkwargs.items():try:dbserialize(value)
- except(TypeError,AttributeError):
+ except(TypeError,AttributeError,PickleError):log_err("The {} keyword argument {} cannot be ""pickled and will not be present in the arguments "
@@ -186,59 +415,219 @@
else:safe_kwargs[key]=value
- self.tasks[task_id]=(now+delta,callback,safe_args,safe_kwargs)
+ self.tasks[task_id]=(comp_time,callback,safe_args,safe_kwargs,persistent,None)self.save()
- callback=self.do_task
- args=[task_id]
- kwargs={}
+ else:# this is a non-persitent task
+ self.tasks[task_id]=(comp_time,callback,args,kwargs,persistent,None)
- returndeferLater(reactor,timedelay,callback,*args,**kwargs)
[docs]defremove(self,task_id):
- """Remove a persistent task without executing it.
+ # some tasks may complete before the deferred can be added
+ iftask_idinself.tasks:
+ task=self.tasks.get(task_id)
+ task=list(task)
+ task[4]=persistent
+ task[5]=d
+ self.tasks[task_id]=task
+ else:# the task already completed
+ returnFalse
+ ifself.stale_timeout>0:
+ self.clean_stale_tasks()
+ returnTaskHandlerTask(task_id)
+
+
[docs]defexists(self,task_id):
+ """Check if a task exists.
+ Most task handler methods check for existence for you. Args: task_id (int): an existing task ID.
- Note:
- A non-persistent task doesn't have a task_id, it is not stored
- in the TaskHandler.
+ Returns:
+ bool: True the task exists False if it does not. """
- delself.tasks[task_id]
+ iftask_idinself.tasks:
+ returnTrue
+ else:
+ returnFalse
+
+
[docs]defactive(self,task_id):
+ """Check if a task is active (has not been called yet).
+
+ Args:
+ task_id (int): an existing task ID.
+
+ Returns:
+ bool: True if a task is active (has not been called yet). False if
+ it is not (has been called) or if the task does not exist.
+
+ """
+ iftask_idinself.tasks:
+ # if the task has not been run, cancel it
+ deferred=self.get_deferred(task_id)
+ returnnot(deferredanddeferred.called)
+ else:
+ returnFalse
+
+
[docs]defcancel(self,task_id):
+ """Stop a task from automatically executing.
+ This will not remove the task.
+
+ Args:
+ task_id (int): an existing task ID.
+
+ Returns:
+ bool: True if the cancel completed successfully.
+ False if the cancel did not complete successfully.
+
+ """
+ iftask_idinself.tasks:
+ # if the task has not been run, cancel it
+ d=self.get_deferred(task_id)
+ ifd:# it is remotely possible for a task to not have a deferred
+ ifd.called:
+ returnFalse
+ else:# the callback has not been called yet.
+ d.cancel()
+ returnTrue
+ else:# this task has no deferred instance
+ returnFalse
+ else:
+ returnFalse
+
+
[docs]defremove(self,task_id):
+ """Remove a task without executing it.
+ Deletes the instance of the task's deferred.
+
+ Args:
+ task_id (int): an existing task ID.
+
+ Returns:
+ bool: True if the removal completed successfully.
+
+ """
+ d=None
+ # delete the task from the tasks dictionary
+ iftask_idinself.tasks:
+ # if the task has not been run, cancel it
+ self.cancel(task_id)
+ delself.tasks[task_id]# delete the task from the tasks dictionary
+ # remove the task from the persistent dictionary and ServerConfigiftask_idinself.to_save:delself.to_save[task_id]
+ self.save()# remove from ServerConfig.objects
+ # delete the instance of the deferred
+ ifd:
+ deld
+ returnTrue
- self.save()
+
[docs]defclear(self,save=True,cancel=True):
+ """clear all tasks.
+ By default tasks are canceled and removed from the database also.
+
+ Args:
+ save=True (bool): Should changes to persistent tasks be saved to database.
+ cancel=True (bool): Cancel scheduled tasks before removing it from task handler.
+
+ Returns:
+ True (bool): if the removal completed successfully.
+
+ """
+ ifself.tasks:
+ fortask_idinself.tasks.keys():
+ ifcancel:
+ self.cancel(task_id)
+ self.tasks={}
+ ifself.to_save:
+ self.to_save={}
+ ifsave:
+ self.save()
+ returnTrue
+
+
[docs]defcall_task(self,task_id):
+ """Call the callback of a task.
+ Leave the task unaffected otherwise.
+ This does not use the task's deferred instance.
+ The only requirement is that the task exist in task handler.
+
+ Args:
+ task_id (int): an existing task ID.
+
+ Returns:
+ bool or any: Set to `False` if the task does not exist in task
+ handler. Otherwise it will be the return of the task's callback.
+
+ """
+ iftask_idinself.tasks:
+ date,callback,args,kwargs,persistent,d=self.tasks.get(task_id)
+ else:# the task does not exist
+ returnFalse
+ returncallback(*args,**kwargs)
[docs]defdo_task(self,task_id):"""Execute the task (call its callback).
+ If calling before timedelay cancel the deferred instance affliated to this task.
+ Remove the task from the dictionary of current tasks on a successful
+ callback. Args: task_id (int): a valid task ID.
- Note:
- This will also remove it from the list of current tasks.
+ Returns:
+ bool or any: Set to `False` if the task does not exist in task
+ handler. Otherwise it will be the return of the task's callback. """
- date,callback,args,kwargs=self.tasks.pop(task_id)
- iftask_idinself.to_save:
- delself.to_save[task_id]
+ callback_return=False
+ iftask_idinself.tasks:
+ date,callback,args,kwargs,persistent,d=self.tasks.get(task_id)
+ else:# the task does not exist
+ returnFalse
+ ifd:# it is remotely possible for a task to not have a deferred
+ ifnotd.called:# the task's deferred has not been called yet
+ d.cancel()# cancel the automated callback
+ else:# this task has no deferred, and should not be called
+ returnFalse
+ callback_return=callback(*args,**kwargs)
+ self.remove(task_id)
+ returncallback_return
- self.save()
- callback(*args,**kwargs)
+
[docs]defget_deferred(self,task_id):
+ """
+ Return the instance of the deferred the task id is using.
+
+ Args:
+ task_id (int): a valid task ID.
+
+ Returns:
+ bool or deferred: An instance of a deferred or False if there is no task with the id.
+ None is returned if there is no deferred affiliated with this id.
+
+ """
+ iftask_idinself.tasks:
+ returnself.tasks[task_id][5]
+ else:
+ returnNone
[docs]defcreate_delays(self):"""Create the delayed tasks for the persistent tasks.
-
- Note:
- This method should be automatically called when Evennia starts.
+ This method should be automatically called when Evennia starts. """now=datetime.now()
- fortask_id,(date,callbac,args,kwargs)inself.tasks.items():
+ fortask_id,(date,callback,args,kwargs,_,_)inself.tasks.items():
+ self.tasks[task_id]=date,callback,args,kwargs,True,Noneseconds=max(0,(date-now).total_seconds())
- deferLater(reactor,seconds,self.do_task,task_id)
+ d=deferLater(self.clock,seconds,self.do_task,task_id)
+ d.addErrback(handle_error)
+ # some tasks may complete before the deferred can be added
+ ifself.tasks.get(task_id,False):
+ self.tasks[task_id]=date,callback,args,kwargs,True,d# Create the soft singleton
diff --git a/docs/1.0-dev/_modules/evennia/server/deprecations.html b/docs/1.0-dev/_modules/evennia/server/deprecations.html
index bbb979bb66..eaa8f30604 100644
--- a/docs/1.0-dev/_modules/evennia/server/deprecations.html
+++ b/docs/1.0-dev/_modules/evennia/server/deprecations.html
@@ -158,8 +158,12 @@
"settings.CYCLE_LOGFILES is unused and should be removed. ""Use PORTAL/SERVER_LOG_DAY_ROTATION and PORTAL/SERVER_LOG_MAX_SIZE ""to control log cycling."
- )
-
+ )
+ ifhasattr(settings,"CHANNEL_COMMAND_CLASS")orhasattr(settings,"CHANNEL_HANDLER_CLASS"):
+ raiseDeprecationWarning(
+ "settings.CHANNEL_HANDLER_CLASS and CHANNEL COMMAND_CLASS are "
+ "unused and should be removed. The ChannelHandler is no more; "
+ "channels are now handled by aliasing the default 'channel' command.")
[docs]defcheck_warnings(settings):"""
diff --git a/docs/1.0-dev/_modules/evennia/server/initial_setup.html b/docs/1.0-dev/_modules/evennia/server/initial_setup.html
index 1ebac43570..e16026429d 100644
--- a/docs/1.0-dev/_modules/evennia/server/initial_setup.html
+++ b/docs/1.0-dev/_modules/evennia/server/initial_setup.html
@@ -173,10 +173,9 @@
goduser=get_god_account()channel_mudinfo=settings.CHANNEL_MUDINFO
- ifnotchannel_mudinfo:
- raiseRuntimeError("settings.CHANNEL_MUDINFO must be defined.")
- channel=create.create_channel(**channel_mudinfo)
- channel.connect(goduser)
+ ifchannel_mudinfo:
+ channel=create.create_channel(**channel_mudinfo)
+ channel.connect(goduser)channel_connectinfo=settings.CHANNEL_CONNECTINFOifchannel_connectinfo:
diff --git a/docs/1.0-dev/_modules/evennia/server/inputfuncs.html b/docs/1.0-dev/_modules/evennia/server/inputfuncs.html
index 9d2f191ec7..0ca16f5371 100644
--- a/docs/1.0-dev/_modules/evennia/server/inputfuncs.html
+++ b/docs/1.0-dev/_modules/evennia/server/inputfuncs.html
@@ -120,11 +120,11 @@
puppet=session.puppetifpuppet:txt=puppet.nicks.nickreplace(
- txt,categories=("inputline","channel"),include_account=True
+ txt,categories=("inputline"),include_account=True)else:txt=session.account.nicks.nickreplace(
- txt,categories=("inputline","channel"),include_account=False
+ txt,categories=("inputline"),include_account=False)kwargs.pop("options",None)cmdhandler(session,txt,callertype="session",session=session,**kwargs)
diff --git a/docs/1.0-dev/_modules/evennia/server/portal/suppress_ga.html b/docs/1.0-dev/_modules/evennia/server/portal/suppress_ga.html
index b5f6a79eca..6a2de888c6 100644
--- a/docs/1.0-dev/_modules/evennia/server/portal/suppress_ga.html
+++ b/docs/1.0-dev/_modules/evennia/server/portal/suppress_ga.html
@@ -82,6 +82,9 @@
self.protocol=protocolself.protocol.protocol_flags["NOGOAHEAD"]=True
+ self.protocol.protocol_flags[
+ "NOPROMPTGOAHEAD"
+ ]=True# Used to send a GA after a prompt line only, set in TTYPE (per client)# tell the client that we prefer to suppress GA ...self.protocol.will(SUPPRESS_GA).addCallbacks(self.will_suppress_ga,self.wont_suppress_ga)
diff --git a/docs/1.0-dev/_modules/evennia/server/portal/telnet.html b/docs/1.0-dev/_modules/evennia/server/portal/telnet.html
index de470cb718..c73d69b015 100644
--- a/docs/1.0-dev/_modules/evennia/server/portal/telnet.html
+++ b/docs/1.0-dev/_modules/evennia/server/portal/telnet.html
@@ -484,7 +484,9 @@
prompt=mxp_parse(prompt)prompt=to_bytes(prompt,self)prompt=prompt.replace(IAC,IAC+IAC).replace(b"\n",b"\r\n")
- prompt+=IAC+GA
+ ifnotself.protocol_flags.get("NOPROMPTGOAHEAD",
+ self.protocol_flags.get("NOGOAHEAD",True)):
+ prompt+=IAC+GAself.transport.write(mccp_compress(self,prompt))else:ifechoisnotNone:
diff --git a/docs/1.0-dev/_modules/evennia/server/portal/tests.html b/docs/1.0-dev/_modules/evennia/server/portal/tests.html
index fde0f0412c..b2196885d4 100644
--- a/docs/1.0-dev/_modules/evennia/server/portal/tests.html
+++ b/docs/1.0-dev/_modules/evennia/server/portal/tests.html
@@ -273,7 +273,6 @@
self.transport.client=["localhost"]self.transport.setTcpKeepAlive=Mock()d=self.proto.makeConnection(self.transport)
-
# test suppress_gaself.assertTrue(self.proto.protocol_flags["NOGOAHEAD"])self.proto.dataReceived(IAC+DONT+SUPPRESS_GA)
@@ -288,13 +287,15 @@
self.assertEqual(self.proto.protocol_flags["SCREENHEIGHT"][0],45)self.assertEqual(self.proto.handshakes,6)# test ttype
- self.assertTrue(self.proto.protocol_flags["FORCEDENDLINE"])self.assertFalse(self.proto.protocol_flags["TTYPE"])self.assertTrue(self.proto.protocol_flags["ANSI"])self.proto.dataReceived(IAC+WILL+TTYPE)self.proto.dataReceived(b"".join([IAC,SB,TTYPE,IS,b"MUDLET",IAC,SE]))self.assertTrue(self.proto.protocol_flags["XTERM256"])self.assertEqual(self.proto.protocol_flags["CLIENTNAME"],"MUDLET")
+ self.assertTrue(self.proto.protocol_flags["FORCEDENDLINE"])
+ self.assertTrue(self.proto.protocol_flags["NOGOAHEAD"])
+ self.assertFalse(self.proto.protocol_flags["NOPROMPTGOAHEAD"])self.proto.dataReceived(b"".join([IAC,SB,TTYPE,IS,b"XTERM",IAC,SE]))self.proto.dataReceived(b"".join([IAC,SB,TTYPE,IS,b"MTTS 137",IAC,SE]))self.assertEqual(self.proto.handshakes,5)
diff --git a/docs/1.0-dev/_modules/evennia/server/portal/ttype.html b/docs/1.0-dev/_modules/evennia/server/portal/ttype.html
index 1ea4595cbd..27a74b4930 100644
--- a/docs/1.0-dev/_modules/evennia/server/portal/ttype.html
+++ b/docs/1.0-dev/_modules/evennia/server/portal/ttype.html
@@ -161,10 +161,11 @@
ifclientname.startswith("MUDLET"):# supports xterm256 stably since 1.1 (2010?)xterm256=clientname.split("MUDLET",1)[1].strip()>="1.1"
- self.protocol.protocol_flags["FORCEDENDLINE"]=False
-
- ifclientname.startswith("TINTIN++"):
- self.protocol.protocol_flags["FORCEDENDLINE"]=True
+ # Mudlet likes GA's on a prompt line for the prompt trigger to
+ # match, if it's not wanting NOGOAHEAD.
+ ifnotself.protocol.protocol_flags["NOGOAHEAD"]:
+ self.protocol.protocol_flags["NOGOAHEAD"]=True
+ self.protocol.protocol_flags["NOPROMPTGOAHEAD"]=Falseif(clientname.startswith("XTERM")
diff --git a/docs/1.0-dev/_modules/evennia/server/server.html b/docs/1.0-dev/_modules/evennia/server/server.html
index 231c759279..35be8093f7 100644
--- a/docs/1.0-dev/_modules/evennia/server/server.html
+++ b/docs/1.0-dev/_modules/evennia/server/server.html
@@ -78,7 +78,6 @@
fromevennia.utils.utilsimportget_evennia_version,mod_import,make_iterfromevennia.utilsimportlogger
-fromevennia.commsimportchannelhandlerfromevennia.server.sessionhandlerimportSESSIONSfromdjango.utils.translationimportgettextas_
@@ -185,12 +184,6 @@
if_MAINTENANCE_COUNT%5==0:# check cache size every 5 minutes_FLUSH_CACHE(_IDMAPPER_CACHE_MAXSIZE)
- if_MAINTENANCE_COUNT%60==0:
- # validate scripts every hour
- evennia.ScriptDB.objects.validate()
- if_MAINTENANCE_COUNT%61==0:
- # validate channels off-sync with scripts
- evennia.CHANNEL_HANDLER.update()if_MAINTENANCE_COUNT%(60*7)==0:# drop database connection every 7 hrs to avoid default timeouts on MySQL# (see https://github.com/evennia/evennia/issues/1376)
@@ -247,12 +240,6 @@
self.start_time=time.time()
- # initialize channelhandler
- try:
- channelhandler.CHANNELHANDLER.update()
- exceptOperationalError:
- print("channelhandler couldn't update - db not set up")
-
# wrap the SIGINT handler to make sure we empty the threadpool# even when we reload and we have long-running requests in queue.# this is necessary over using Twisted's signal handler.
@@ -589,11 +576,10 @@
god_account=AccountDB.objects.get(id=1)# mudinfomudinfo_chan=settings.CHANNEL_MUDINFO
- ifnotmudinfo_chan:
- raiseRuntimeError("settings.CHANNEL_MUDINFO must be defined.")
- ifnotChannelDB.objects.filter(db_key=mudinfo_chan["key"]):
- channel=create_channel(**mudinfo_chan)
- channel.connect(god_account)
+ ifmudinfo_chan:
+ ifnotChannelDB.objects.filter(db_key=mudinfo_chan["key"]):
+ channel=create_channel(**mudinfo_chan)
+ channel.connect(god_account)# connectinfoconnectinfo_chan=settings.CHANNEL_MUDINFOifconnectinfo_chan:
diff --git a/docs/1.0-dev/_modules/evennia/typeclasses/attributes.html b/docs/1.0-dev/_modules/evennia/typeclasses/attributes.html
index 6c742cc791..61d035e165 100644
--- a/docs/1.0-dev/_modules/evennia/typeclasses/attributes.html
+++ b/docs/1.0-dev/_modules/evennia/typeclasses/attributes.html
@@ -1037,7 +1037,8 @@
looked-after Attribute. default_access (bool, optional): If no `attrread` lock is set on object, this determines if the lock should then be passed or not.
- return_list (bool, optional):
+ return_list (bool, optional): Always return a list, also if there is only
+ one or zero matches found. Returns: result (any or list): One or more matches for keys and/or
@@ -1277,7 +1278,7 @@
_SA=object.__setattr__
-
[docs]definitialize_nick_templates(pattern,replacement,pattern_is_regex=False):""" Initialize the nick templates for matching and remapping a string. Args:
- in_template (str): The template to be used for nick recognition.
- out_template (str): The template to be used to replace the string
- matched by the in_template.
+ pattern (str): The pattern to be used for nick recognition. This will
+ be parsed for shell patterns into a regex, unless `pattern_is_regex`
+ is `True`, in which case it must be an already valid regex string. In
+ this case, instead of `$N`, numbered arguments must instead be given
+ as matching groups named as `argN`, such as `(?P<arg1>.+?)`.
+ replacement (str): The template to be used to replace the string
+ matched by the pattern. This can contain `$N` markers and is never
+ parsed into a regex.
+ pattern_is_regex (bool): If set, `pattern` is a full regex string
+ instead of containing shell patterns. Returns:
- regex (regex): Regex to match against strings
- template (str): Template with markers ``{arg1}, {arg2}``, etc for
- replacement using the standard .format method.
+ regex, template (str): Regex to match against strings and template
+ with markers ``{arg1}, {arg2}``, etc for replacement using the standard
+ `.format` method. Raises: evennia.typecalasses.attributes.NickTemplateInvalid: If the in/out template does not have a matching number of `$args`.
+ Examples:
+ - `pattern` (shell syntax): `"grin $1"`
+ - `pattern` (regex): `"grin (?P<arg1.+?>)"`
+ - `replacement`: `"emote gives a wicked grin to $1"`
+
"""
- # create the regex for in_template
- regex_string=fnmatch.translate(in_template)
- # we must account for a possible line break coming over the wire
+ # create the regex from the pattern
+ ifpattern_is_regex:
+ # Note that for a regex we can't validate in the way we do for the shell
+ # pattern, since you may have complex OR statements or optional arguments.
- # NOTE-PYTHON3: fnmatch.translate format changed since Python2
- regex_string=regex_string[:-2]+r"(?:[\n\r]*?)\Z"
+ # Explicit regex given from the onset - this already contains argN
+ # groups. we need to split out any | - separated parts so we can
+ # attach the line-break/ending extras all regexes require.
+ pattern_regex_string=r"|".join(
+ or_part+r"(?:[\n\r]*?)\Z"
+ foror_partin_RE_OR.split(pattern))
- # validate the templates
- regex_args=[match.group(2)formatchin_RE_NICK_ARG.finditer(regex_string)]
- temp_args=[match.group(2)formatchin_RE_NICK_TEMPLATE_ARG.finditer(out_template)]
- ifset(regex_args)!=set(temp_args):
- # We don't have the same $-tags in input/output.
- raiseNickTemplateInvalid
+ else:
+ # Shell pattern syntax - convert $N to argN groups
+ # for the shell pattern we make sure we have matching $N on both sides
+ pattern_args=[match.group(1)formatchin_RE_NICK_RAW_ARG.finditer(pattern)]
+ replacement_args=[
+ match.group(1)formatchin_RE_NICK_RAW_ARG.finditer(replacement)]
+ ifset(pattern_args)!=set(replacement_args):
+ # We don't have the same amount of argN/$N tags in input/output.
+ raiseNickTemplateInvalid("Nicks: Both in/out-templates must contain the same $N tags.")
- regex_string=_RE_NICK_SPACE.sub(r"\\s+",regex_string)
- regex_string=_RE_NICK_ARG.sub(lambdam:"(?P<arg%s>.+?)"%m.group(2),regex_string)
- template_string=_RE_NICK_TEMPLATE_ARG.sub(lambdam:"{arg%s}"%m.group(2),out_template)
+ # generate regex from shell pattern
+ pattern_regex_string=fnmatch.translate(pattern)
+ pattern_regex_string=_RE_NICK_SPACE.sub(r"\\s+",pattern_regex_string)
+ pattern_regex_string=_RE_NICK_ARG.sub(
+ lambdam:"(?P<arg%s>.+?)"%m.group(2),pattern_regex_string)
+ # we must account for a possible line break coming over the wire
+ pattern_regex_string=pattern_regex_string[:-2]+r"(?:[\n\r]*?)\Z"
- returnregex_string,template_string
+ # map the replacement to match the arg1 group-names, to make replacement easy
+ replacement_string=_RE_NICK_RAW_ARG.sub(lambdam:"{arg%s}"%m.group(2),replacement)
+
+ returnpattern_regex_string,replacement_string
[docs]defparse_nick_template(string,template_regex,outtemplate):
@@ -1388,17 +1418,21 @@
Parse a text using a template and map it to another template Args:
- string (str): The input string to processj
+ string (str): The input string to process template_regex (regex): A template regex created with initialize_nick_template. outtemplate (str): The template to which to map the matches produced by the template_regex. This should have $1, $2,
- etc to match the regex.
+ etc to match the template-regex. Un-found $N-markers (possible if
+ the regex has optional matching groups) are replaced with empty
+ strings. """match=template_regex.match(string)ifmatch:
- returnTrue,outtemplate.format(**match.groupdict())
+ matchdict={key:valueifvalueisnotNoneelse""
+ forkey,valueinmatch.groupdict().items()}
+ returnTrue,outtemplate.format(**matchdict)returnFalse,string
@@ -1447,6 +1481,9 @@
a string. kwargs (any, optional): These are passed on to `AttributeHandler.get`.
+ Returns:
+ str or tuple: The nick replacement string or nick tuple.
+
"""ifreturn_tupleor"return_obj"inkwargs:returnsuper().get(key=key,category=category,**kwargs)
@@ -1460,24 +1497,46 @@
)returnNone
[docs]defadd(self,pattern,replacement,category="inputline",pattern_is_regex=False,**kwargs):"""
- Add a new nick.
+ Add a new nick, a mapping pattern -> replacement. Args:
- key (str): A key (or template) for the nick to match for.
- replacement (str): The string (or template) to replace `key` with (the "nickname").
+ pattern (str): A pattern to match for. This will be parsed for
+ shell patterns using the `fnmatch` library and can contain
+ `$N`-markers to indicate the locations of arguments to catch. If
+ `pattern_is_regex=True`, this must instead be a valid regular
+ expression and the `$N`-markers must be named `argN` that matches
+ numbered regex groups (see examples).
+ replacement (str): The string (or template) to replace `key` with
+ (the "nickname"). This may contain `$N` markers to indicate where to
+ place the argument-matches category (str, optional): the category within which to retrieve the nick. The "inputline" means replacing data sent by the user.
- kwargs (any, optional): These are passed on to `AttributeHandler.get`.
+ pattern_is_regex (bool): If `True`, the `pattern` will be parsed as a
+ raw regex string. Instead of using `$N` markers in this string, one
+ then must mark numbered arguments as a named regex-groupd named `argN`.
+ For example, `(?P<arg1>.+?)` will match the behavior of using `$1`
+ in the shell pattern.
+ **kwargs (any, optional): These are passed on to `AttributeHandler.get`.
+
+ Notes:
+ For most cases, the shell-pattern is much shorter and easier. The
+ regex pattern form can be useful for more complex matchings though,
+ for example in order to add optional arguments, such as with
+ `(?P<argN>.*?)`.
+
+ Example:
+ - pattern (default shell syntax): `"gr $1 at $2"`
+ - pattern (with pattern_is_regex=True): `r"gr (?P<arg1>.+?) at (?P<arg2>.+?)"`
+ - replacement: `"emote With a flourish, $1 grins at $2."` """
- ifcategory=="channel":
- nick_regex,nick_template=initialize_nick_templates(key+" $1",replacement+" $1")
- else:
- nick_regex,nick_template=initialize_nick_templates(key,replacement)
- super().add(key,(nick_regex,nick_template,key,replacement),category=category,**kwargs)
[docs]defremove(self,key,category="inputline",**kwargs):"""
diff --git a/docs/1.0-dev/_modules/evennia/utils/create.html b/docs/1.0-dev/_modules/evennia/utils/create.html
index 76b506795e..066f149202 100644
--- a/docs/1.0-dev/_modules/evennia/utils/create.html
+++ b/docs/1.0-dev/_modules/evennia/utils/create.html
@@ -74,7 +74,6 @@
_AccountDB=None_to_object=None_ChannelDB=None
-_channelhandler=None# limit symbol import from API
@@ -403,22 +402,21 @@
[docs]defcreate_message(
- senderobj,message,channels=None,receivers=None,locks=None,tags=None,header=None
-):
+ senderobj,message,receivers=None,locks=None,tags=None,
+ header=None,**kwargs):""" Create a new communication Msg. Msgs represent a unit of database-persistent communication between entites. Args:
- senderobj (Object or Account): The entity sending the Msg.
+ senderobj (Object, Account, Script, str or list): The entity (or
+ entities) sending the Msg. If a `str`, this is the id-string
+ for an external sender type. message (str): Text with the message. Eventual headers, titles etc should all be included in this text string. Formatting will be retained.
- channels (Channel, key or list): A channel or a list of channels to
- send to. The channels may be actual channel objects or their
- unique key strings.
- receivers (Object, Account, str or list): An Account/Object to send
- to, or a list of them. May be Account objects or accountnames.
+ receivers (Object, Account or list): An Account/Object to send
+ to, or a list of them. locks (str): Lock definition string. tags (list): A list of tags or tuples `(tag, category)`. header (str): Mime-type or other optional information for the message
@@ -430,6 +428,12 @@
limit this as desired. """
+ if'channels'inkwargs:
+ raiseDeprecationWarning(
+ "create_message() does not accept 'channel' kwarg anymore "
+ "- channels no longer accept Msg objects."
+ )
+
global_Msgifnot_Msg:fromevennia.comms.modelsimportMsgas_Msg
@@ -441,8 +445,6 @@
forsenderinmake_iter(senderobj):new_message.senders=sendernew_message.header=header
- forchannelinmake_iter(channels):
- new_message.channels=channelforreceiverinmake_iter(receivers):new_message.receivers=receiveriflocks:
diff --git a/docs/1.0-dev/_modules/evennia/utils/evmenu.html b/docs/1.0-dev/_modules/evennia/utils/evmenu.html
index 88f3df7be8..4704d5055c 100644
--- a/docs/1.0-dev/_modules/evennia/utils/evmenu.html
+++ b/docs/1.0-dev/_modules/evennia/utils/evmenu.html
@@ -1673,10 +1673,12 @@
aliases=[_CMD_NOMATCH,"yes","no",'y','n','a','abort']arg_regex=r"^$"
- def_clean(self):
- delself.caller.ndb._yes_no_question
- self.caller.cmdset.remove(YesNoQuestionCmdSet)
-
+ def_clean(self,caller):
+ delcaller.ndb._yes_no_question
+ ifnotcaller.cmdset.has(YesNoQuestionCmdSet)andhasattr(caller,"account"):
+ caller.account.cmdset.remove(YesNoQuestionCmdSet)
+ else:
+ caller.cmdset.remove(YesNoQuestionCmdSet)
[docs]deffunc(self):"""This is called when user enters anything."""
@@ -1687,6 +1689,10 @@
yes_no_question=caller.account.ndb._yes_no_questioncaller=caller.account
+ ifnotyes_no_question:
+ self._clean(caller)
+ return
+
inp=self.cmdnameifinp==_CMD_NOINPUT:
@@ -1699,7 +1705,7 @@
ifinpin('a','abort')andyes_no_question.allow_abort:caller.msg("Aborted.")
- self._clean()
+ self._clean(caller)returncaller.ndb._yes_no_question.session=self.session
@@ -1719,12 +1725,13 @@
return# cleanup
- self._clean()
- exceptException:
+ self._clean(caller)
+ exceptExceptionaserr:# make sure to clean up cmdset if something goes wrongcaller.msg("|rError in ask_yes_no. Choice not confirmed (report to admin)|n")logger.log_trace("Error in ask_yes_no")
- self._clean()
[docs]defask_yes_no(caller,prompt="Yes or No {options}?",yes_action="Yes",no_action="No",
+ default=None,allow_abort=False,session=None,*args,**kwargs):""" A helper question for asking a simple yes/no question. This will cause the system to pause and wait for input from the player.
@@ -1762,8 +1769,9 @@
with `(caller, *args, **kwargs)` when the No-choice is made. If a string, this string will be echoed back to the caller. default (str optional): This is what the user will get if they just press the
- return key without giving any input. One of 'N', 'Y', 'A' or 'None'
- for no default. If 'A' is given, `allow_abort` is auto-set.
+ return key without giving any input. One of 'N', 'Y', 'A' or `None`
+ for no default (an explicit choice must be given). If 'A' (abort)
+ is given, `allow_abort` kwarg is ignored and assumed set. allow_abort (bool, optional): If set, the 'A(bort)' option is available (a third option meaning neither yes or no but just exits the prompt). session (Session, optional): This allows to specify the
@@ -1771,10 +1779,11 @@
is an Account in multisession modes greater than 2. The session is then updated by the command and is available (for example in callbacks) through `caller.ndb._yes_no_question.session`.
- *args, **kwargs: These are passed into the callables.
+ *args: Additional arguments passed on into callables.
+ **kwargs: Additional keyword args passed on into callables. Raises:
- RuntimeError, FooError: If default and allow_abort clashes.
+ RuntimeError, FooError: If default and `allow_abort` clashes. Example: ::
@@ -1821,6 +1830,7 @@
prompt=prompt.format(options=options)caller.ndb._yes_no_question=_Prompt()
+ caller.ndb._yes_no_question.prompt=promptcaller.ndb._yes_no_question.session=sessioncaller.ndb._yes_no_question.prompt=promptcaller.ndb._yes_no_question.default=default
diff --git a/docs/1.0-dev/_modules/evennia/utils/logger.html b/docs/1.0-dev/_modules/evennia/utils/logger.html
index 92311e81c6..50983e3509 100644
--- a/docs/1.0-dev/_modules/evennia/utils/logger.html
+++ b/docs/1.0-dev/_modules/evennia/utils/logger.html
@@ -399,12 +399,14 @@
_CHANNEL_LOG_NUM_TAIL_LINES=settings.CHANNEL_LOG_NUM_TAIL_LINESnum_lines_to_append=_CHANNEL_LOG_NUM_TAIL_LINES
-
[docs]defrotate(self,num_lines_to_append=None):""" Rotates our log file and appends some number of lines from the previous log to the start of the new one. """
- append_tail=self.num_lines_to_append>0
+ append_tail=(num_lines_to_append
+ ifnum_lines_to_appendisnotNone
+ elseself.num_lines_to_append)ifnotappend_tail:logfile.LogFile.rotate(self)return
@@ -514,6 +516,43 @@
deferToThread(callback,filehandle,msg).addErrback(errback)
+
[docs]deflog_file_exists(filename="game.log"):
+ """
+ Determine if a log-file already exists.
+
+ Args:
+ filename (str): The filename (within the log-dir).
+
+ Returns:
+ bool: If the log file exists or not.
+
+ """
+ global_LOGDIR
+ ifnot_LOGDIR:
+ fromdjango.confimportsettings
+ _LOGDIR=settings.LOG_DIR
+
+ filename=os.path.join(_LOGDIR,filename)
+ returnos.path.exists(filename)
+
+
+
[docs]defrotate_log_file(filename="game.log",num_lines_to_append=None):
+ """
+ Force-rotate a log-file, without
+
+ Args:
+ filename (str): The log file, located in settings.LOG_DIR.
+ num_lines_to_append (int, optional): Include N number of
+ lines from previous file in new one. If `None`, use default.
+ Set to 0 to include no lines.
+
+ """
+ iflog_file_exists(filename):
+ file_handle=_open_log_file(filename)
+ iffile_handle:
+ file_handle.rotate(num_lines_to_append=num_lines_to_append)
+
+
[docs]deftail_log_file(filename,offset,nlines,callback=None):""" Return the tail of the log file.
diff --git a/docs/1.0-dev/_modules/evennia/utils/optionclasses.html b/docs/1.0-dev/_modules/evennia/utils/optionclasses.html
index d2040d61cc..6ef26d3c27 100644
--- a/docs/1.0-dev/_modules/evennia/utils/optionclasses.html
+++ b/docs/1.0-dev/_modules/evennia/utils/optionclasses.html
@@ -48,7 +48,7 @@
fromevennia.utilsimportvalidatorfuncs
-
[docs]classBaseOption:""" Abstract Class to deal with encapsulating individual Options. An Option has a name/key, a description to display in relevant commands and menus, and a
diff --git a/docs/1.0-dev/_modules/evennia/utils/optionhandler.html b/docs/1.0-dev/_modules/evennia/utils/optionhandler.html
index 21f4f6aec1..5cfe5bfe89 100644
--- a/docs/1.0-dev/_modules/evennia/utils/optionhandler.html
+++ b/docs/1.0-dev/_modules/evennia/utils/optionhandler.html
@@ -47,7 +47,7 @@
_SA=object.__setattr__
-
[docs]classInMemorySaveHandler:""" Fallback SaveHandler, implementing a minimum of the required save mechanism and storing data in memory.
@@ -64,7 +64,7 @@
returnself.storage.get(key,default)
[docs]classOptionHandler:""" This is a generic Option handler. Retrieve options either as properties on this handler or by using the .get method.
@@ -99,6 +99,7 @@
A common one to pass would be AttributeHandler.get. save_kwargs (any): Optional extra kwargs to pass into `savefunc` above. load_kwargs (any): Optional extra kwargs to pass into `loadfunc` above.
+
Notes: Both loadfunc and savefunc must be specified. If only one is given, the other will be ignored and in-memory storage will be used.
diff --git a/docs/1.0-dev/_modules/evennia/utils/utils.html b/docs/1.0-dev/_modules/evennia/utils/utils.html
index 774ab09e4e..d916fd605c 100644
--- a/docs/1.0-dev/_modules/evennia/utils/utils.html
+++ b/docs/1.0-dev/_modules/evennia/utils/utils.html
@@ -210,16 +210,18 @@
returnto_str(text)
[docs]defdedent(text,baseline_index=None,indent=None):""" Safely clean all whitespace at the left of a paragraph. Args: text (str): The text to dedent.
- baseline_index (int or None, optional): Which row to use as a 'base'
+ baseline_index (int, optional): Which row to use as a 'base' for the indentation. Lines will be dedented to this level but no further. If None, indent so as to completely deindent the least indented text.
+ indent (int, optional): If given, force all lines to this indent.
+ This bypasses `baseline_index`. Returns: text (str): Dedented string.
@@ -232,7 +234,12 @@
"""ifnottext:return""
- ifbaseline_indexisNone:
+ ifindentisnotNone:
+ lines=text.split("\n")
+ ind=" "*indent
+ indline="\n"+ind
+ returnind+indline.join(line.strip()forlineinlines)
+ elifbaseline_indexisNone:returntextwrap.dedent(text)else:lines=text.split("\n")
@@ -1068,7 +1075,7 @@
[docs]defdelay(timedelay,callback,*args,**kwargs):"""
- Delay the return of a value.
+ Delay the calling of a callback (function). Args: timedelay (int or float): The delay in seconds
@@ -1076,24 +1083,35 @@
after `timedelay` seconds. *args: Will be used as arguments to callback Keyword Args:
- persistent (bool): Make the delay persistent over a reboot or reload.
- any: Any other keywords will be use as keyword arguments to callback.
+ persistent (bool, optional): Should make the delay persistent
+ over a reboot or reload. Defaults to False.
+ any (any): Will be used as keyword arguments to callback. Returns:
- deferred: Will fire with callback after `timedelay` seconds. Note that
- if `timedelay()` is used in the
- commandhandler callback chain, the callback chain can be
- defined directly in the command body and don't need to be
- specified here.
+ deferred or int: If ``persistent`` kwarg is `False`, return deferred
+ that will fire with callback after `timedelay` seconds. Note that
+ if `timedelay()` is used in the commandhandler callback chain, the
+ callback chain can be defined directly in the command body and
+ don't need to be specified here. Reference twisted.internet.defer.Deferred.
+ If persistent kwarg is set, return the task's ID as an integer. This is
+ intended for use with ``evennia.scripts.taskhandler.TASK_HANDLER``
+ `.do_task` and `.remove` methods. Notes: The task handler (`evennia.scripts.taskhandler.TASK_HANDLER`) will be called for persistent or non-persistent tasks. If persistent is set to True, the callback, its arguments
- and other keyword arguments will be saved in the database,
+ and other keyword arguments will be saved (serialized) in the database, assuming they can be. The callback will be executed even after a server restart/reload, taking into account the specified delay (and server down time).
+ Keep in mind that persistent tasks arguments and callback should not
+ use memory references.
+ If persistent is set to True the delay function will return an int
+ which is the task's id itended for use with TASK_HANDLER's do_task
+ and remove methods.
+
+ All task's whose time delays have passed will be called on server startup. """global_TASK_HANDLER
@@ -1370,18 +1388,19 @@
already imported module object (e.g. `models`) Returns:
- variables (dict): A dict of {variablename: variable} for all
+ dict: A dict of {variablename: variable} for all variables in the given module. Notes:
- Ignores modules and variable names starting with an underscore.
+ Ignores modules and variable names starting with an underscore, as well
+ as variables imported into the module from other modules. """mod=mod_import(module)ifnotmod:return{}# make sure to only return variables actually defined in this
- # module if available (try to avoid not imports)
+ # module if available (try to avoid imports)members=getmembers(mod,predicate=lambdaobj:getmodule(obj)in(mod,None))returndict((key,val)forkey,valinmembersifnotkey.startswith("_"))
[docs]defformat_grid(elements,width=78,sep=" ",verbatim_elements=None):""" This helper function makes a 'grid' output, where it distributes the given string-elements as evenly as possible to fill out the given width.
@@ -1888,84 +1907,118 @@
decorations in the grid, such as horizontal bars. Returns:
- gridstr: The grid as a list of ready-formatted rows. We return it
+ list: The grid as a list of ready-formatted rows. We return it like this to make it easier to insert decorations between rows, such as horizontal bars. """
+ def_minimal_rows(elements):
+ """
+ Minimalistic distribution with minimal spacing, good for single-line
+ grids but will look messy over many lines.
+ """
+ rows=[""]
+ forelementinelements:
+ rowlen=len(rows[-1])
+ elen=len(element)
+ ifrowlen+elen<=width:
+ rows[-1]+=element
+ else:
+ rows.append(element)
+ returnrows
+
+ def_weighted_rows(elements):
+ """
+ Dynamic-space, good for making even columns in a multi-line grid but
+ will look strange for a single line.
+ """
+
+ wls=[len(elem)foreleminelements]
+ wls_percentile=[wlforiw,wlinenumerate(wls)ifiwnotinverbatim_elements]
+
+ ifwls_percentile:
+ # get the nth percentile as a good representation of average width
+ averlen=int(percentile(sorted(wls_percentile),0.9))+2# include extra space
+ aver_per_row=width//averlen+1
+ else:
+ # no adjustable rows, just keep all as-is
+ aver_per_row=1
+
+ ifaver_per_row==1:
+ # one line per row, output directly since this is trivial
+ # we use rstrip here to remove extra spaces added by sep
+ return[
+ crop(element.rstrip(),width)+" "*max(0,width-len(element.rstrip()))
+ foriel,elementinenumerate(elements)
+ ]
+
+ indices=[averlen*indforindinrange(aver_per_row-1)]
+
+ rows=[]
+ ic=0
+ row=""
+ forie,elementinenumerate(elements):
+
+ wl=wls[ie]
+ lrow=len(row)
+ debug=row.replace(" ",".")
+
+ iflrow+wl>width:
+ # this slot extends outside grid, move to next line
+ row+=" "*(width-lrow)
+ rows.append(row)
+ ifwl>=width:
+ # remove sep if this fills the entire line
+ element=element.rstrip()
+ row=crop(element,width)
+ ic=0
+ elific>=aver_per_row-1:
+ # no more slots available on this line
+ row+=" "*max(0,(width-lrow))
+ rows.append(row)
+ row=crop(element,width)
+ ic=0
+ else:
+ try:
+ whilelrow>max(0,indices[ic]):
+ # slot too wide, extend into adjacent slot
+ ic+=1
+ row+=" "*max(0,indices[ic]-lrow)
+ exceptIndexError:
+ # we extended past edge of grid, crop or move to next line
+ ific==0:
+ row=crop(element,width)
+ else:
+ row+=" "*max(0,width-lrow)
+ rows.append(row)
+ ic=0
+ else:
+ # add a new slot
+ row+=element+" "*max(0,averlen-wl)
+ ic+=1
+
+ ifie>=nelements-1:
+ # last element, make sure to store
+ row+=" "*max(0,width-len(row))
+ rows.append(row)
+ returnrows
+
+ ifnotelements:
+ return[]ifnotverbatim_elements:verbatim_elements=[]nelements=len(elements)# add sep to all but the very last elementelements=[elements[ie]+sepforieinrange(nelements-1)]+[elements[-1]]
- wls=[len(elem)foreleminelements]
- wls_percentile=[wlforiw,wlinenumerate(wls)ifiwnotinverbatim_elements]
- # from pudb import debugger
- # debugger.Debugger().set_trace()
- # get the nth percentile as a good representation of average width
- averlen=int(percentile(sorted(wls_percentile),0.9))+2# include extra space
- aver_per_row=width//averlen+1
+ ifsum(len(element)forelementinelements)<=width:
+ # grid fits in one line
+ return_minimal_rows(elements)
+ else:
+ # full multi-line grid
+ return_weighted_rows(elements)
- ifaver_per_row==1:
- # one line per row, output directly since this is trivial
- # we use rstrip here to remove extra spaces added by sep
- return[
- crop(element.rstrip(),width)+" "*max(0,width-len(element.rstrip()))
- foriel,elementinenumerate(elements)
- ]
- indices=[averlen*indforindinrange(aver_per_row-1)]
-
- rows=[]
- ic=0
- row=""
- forie,elementinenumerate(elements):
-
- wl=wls[ie]
- lrow=len(row)
- debug=row.replace(" ",".")
-
- iflrow+wl>width:
- # this slot extends outside grid, move to next line
- row+=" "*(width-lrow)
- rows.append(row)
- ifwl>=width:
- # remove sep if this fills the entire line
- element=element.rstrip()
- row=crop(element,width)
- ic=0
- elific>=aver_per_row-1:
- # no more slots available on this line
- row+=" "*max(0,(width-lrow))
- rows.append(row)
- row=crop(element,width)
- ic=0
- else:
- try:
- whilelrow>max(0,indices[ic]):
- # slot too wide, extend into adjacent slot
- ic+=1
- row+=" "*max(0,indices[ic]-lrow)
- exceptIndexError:
- # we extended past edge of grid, crop or move to next line
- ific==0:
- row=crop(element,width)
- else:
- row+=" "*max(0,width-lrow)
- rows.append(row)
- ic=0
- else:
- # add a new slot
- row+=element+" "*max(0,averlen-wl)
- ic+=1
-
- ifie>=nelements-1:
- # last element, make sure to store
- row+=" "*max(0,width-len(row))
- rows.append(row)
-
- returnrows
diff --git a/docs/1.0-dev/_sources/Components/Channels.md.txt b/docs/1.0-dev/_sources/Components/Channels.md.txt
index 005eef01c3..5998880201 100644
--- a/docs/1.0-dev/_sources/Components/Channels.md.txt
+++ b/docs/1.0-dev/_sources/Components/Channels.md.txt
@@ -1,3 +1,379 @@
-# Channels
+# Channels
+
+In a multiplayer game, players often need other means of in-game communication
+than moving to the same room and use `say` or `emote`.
+
+_Channels_ allows Evennia's to act as a fancy chat program. When a player is
+connected to a channel, sending a message to it will automatically distribute
+it to every other subscriber.
+
+Channels can be used both for chats between [Accounts](./Accounts) and between
+[Objects](./Objects) (usually Characters). Chats could be both OOC
+(out-of-character) or IC (in-charcter) in nature. Some examples:
+
+- A support channel for contacting staff (OOC)
+- A general chat for discussing anything and foster community (OOC)
+- Admin channel for private staff discussions (OOC)
+- Private guild channels for planning and organization (IC/OOC depending on game)
+- Cyberpunk-style retro chat rooms (IC)
+- In-game radio channels (IC)
+- Group telephathy (IC)
+- Walkie talkies (IC)
+
+```versionchanged:: 1.0
+
+ Channel system changed to use a central 'channel' command and nicks instead of
+ auto-generated channel-commands and -cmdset. ChannelHandler was removed.
+
+```
+
+## Using channels in-game
+
+In the default command set, channels are all handled via the mighty
+[channel command](api:evennia.commands.default.comms.CmdChannel), `channel` (or
+`chan`). By default, this command will assume all entities dealing with
+channels are `Accounts`.
+
+### Viewing and joining channels
+
+ channel - shows your subscriptions
+ channel/all - shows all subs available to you
+ channel/who - shows who subscribes to this channel
+
+To join/unsub a channel do
+
+ channel/sub channelname
+ channel/unsub channelname
+
+If you temporarily don't want to hear the channel for a while (without actually
+unsubscribing), you can mute it:
+
+ channel/mute channelname
+ channel/unmute channelname
+
+### Chat on channels
+
+To speak on a channel, do
+
+ channel public Hello world!
+
+If the channel-name has spaces in it, you need to use a '`=`':
+
+ channel rest room = Hello world!
+
+Now, this is more to type than we'd like, so when you join a channel, the
+system automatically sets up an personal alias so you can do this instead:
+
+ public Hello world
+
+```warning::
+
+ This shortcut will not work if the channel-name has spaces in it.
+ So channels with long names should make sure to provide a one-word alias as
+ well.
+```
+
+Any user can make up their own channel aliases:
+
+ channel/alias public = foo;bar
+
+You can now just do
+
+ foo Hello world!
+ bar Hello again!
+
+And even remove the default one if they don't want to use it
+
+ channel/unalias public
+ public Hello
+
+But you can also use your alias with the `channel` command:
+
+ channel foo Hello world!
+
+> What happens when aliasing is that a [nick](./Nicks) is created that maps your
+> alias + argument onto calling the `channel` command. So when you enter `foo hello`,
+> what the server sees is actually `channel foo = hello`. The system is also
+> clever enough to know that whenever you search for channels, your channel-nicks
+> should also be considered so as to convert your input to an existing channel name.
+
+You can check if you missed channel conversations by viewing the channel's
+scrollback with
+
+ channel/history public
+
+This retrieves the last 20 lines of text (also from a time when you were
+offline). You can step further back by specifying how many lines back to start:
+
+ channel/history public = 30
+
+This again retrieve 20 lines, but starting 30 lines back (so you'll get lines
+30-50 counting backwards).
+
+
+### Channel administration
+
+To create/destroy a new channel you can do
+
+ channel/create channelname;alias;alias = description
+ channel/destroy channelname
+
+Aliases are optional but can be good for obvious shortcuts everyone may want to
+use. The description is used in channel-listings. You will automatically join a
+channel you created and will be controlling it. You can also use `channel/desc` to
+change the description on a channel you wnn later.
+
+If you control a channel you can also kick people off it:
+
+ channel/boot mychannel = annoyinguser123 : stop spamming!
+
+The last part is an optional reason to send to the user before they are booted.
+You can give a comma-separated list of channels to kick the same user from all
+those channels at once. The user will be unsubbed from the channel and all
+their aliases will be wiped. But they can still rejoin if they like.
+
+ channel/ban mychannel = annoyinguser123
+ channel/ban - view bans
+ channel/unban mychannel = annoyinguser123
+
+Banning adds the user to the channels blacklist. This means they will not be
+able to _rejoin_ if you boot them. You will need to run `channel/boot` to
+actually kick them out.
+
+See the [Channel command](api:evennia.commands.default.comms.CmdChannel) api
+docs (and in-game help) for more details.
+
+Admin-level users can also modify channel's [locks](./Locks):
+
+ channel/lock buildchannel = listen:all();send:perm(Builders)
+
+Channels use three lock-types by default:
+
+- `listen` - who may listen to the channel. Users without this access will not
+ even be able to join the channel and it will not appear in listings for them.
+- `send` - who may send to the channel.
+- `control` - this is assigned to you automatically when you create the channel. With
+ control over the channel you can edit it, boot users and do other management tasks.
+
+
+#### Restricting channel administration
+
+By default everyone can use the channel command ([evennia.commands.default.comms.CmdChannel](api:evennia.commands.default.comms.CmdChannel))
+to create channels and will then control the channels they created (to boot/ban
+people etc). If you as a developer does not want regular players to do this
+(perhaps you want only staff to be able to spawn new channels), you can
+override the `channel` command and change its `locks` property.
+
+The default `help` command has the following `locks` property:
+
+```python
+ locks = "cmd:not perm(channel_banned); admin:all(); manage:all(); changelocks: perm(Admin)"
+```
+
+This is a regular [lockstring](./Locks).
+
+- `cmd: pperm(channel_banned)` - The `cmd` locktype is the standard one used for all Commands.
+ an accessing object failing this will not even know that the command exists. The `pperm()` lockfunc
+ checks an on-account [Permission](Building Permissions) 'channel_banned' - and the `not` means
+ that if they _have_ that 'permission' they are cut off from using the `channel` command. You usually
+ don't need to change this lock.
+- `admin:all()` - this is a lock checked in the `channel` command itself. It controls access to the
+ `/boot`, `/ban` and `/unban` switches (by default letting everyone use them).
+- `manage:all()` - this controls access to the `/create`, `/destroy`, `/desc` switches.
+- `changelocks: perm(Admin)` - this controls access to the `/lock` and `/unlock` switches. By
+ default this is something only [Admins](Building Permissions) can change.
+
+> Note - while `admin:all()` and `manage:all()` will let everyone use these switches, users
+> will still only be able to admin or destroy channels they actually control!
+
+If you only want (say) Builders and higher to be able to create and admin
+channels you could override the `help` command and change the lockstring to:
+
+```python
+ # in for example mygame/commands/commands.py
+
+ from evennia import default_cmds
+
+ class MyCustomChannelCmd(default_cmds.CmdChannel):
+ locks = "cmd: not pperm(channel_banned);admin:perm(Builder);manage:perm(Builder);changelocks:perm(Admin)"
+
+```
+
+Add this custom command to your default cmdset and regular users wil now get an
+access-denied error when trying to use use these switches.
+
+## Allowing Characters to use Channels
+
+The default `channel` command ([evennia.commands.default.comms.CmdChannel](api:evennia.commands.default.comms.CmdChannel))
+sits in the `Account` [command set](./Command-Sets). It is set up such that it will
+always operate on `Accounts`, even if you were to add it to the
+`CharacterCmdSet`.
+
+It's a one-line change to make this command accept non-account callers. But for
+convenience we provide a version for Characters/Objects. Just import
+[evennia.commands.default.comms.CmdObjectChannel](api:evennia.commands.default.comms.CmdObjectChannel)
+and inherit from that instead.
+
+## Customizing channel output and behavior
+
+When distributing a message, the channel will call a series of hooks on itself
+and (more importantly) on each recipient. So you can customize things a lot by
+just modifying hooks on your normal Object/Account typeclasses.
+
+Internally, the message is sent with
+`channel.msg(message, senders=sender, bypass_mute=False, **kwargs)`, where
+`bypass_mute=True` means the message ignores muting (good for alerts or if you
+delete the channel etc) and `**kwargs` are any extra info you may want to pass
+to the hooks. The `senders` (it's always only one in the default implementation
+but could in principle be multiple) and `bypass_mute` are part of the `kwargs`
+below:
+
+ 1. `channel.at_pre_msg(message, **kwargs)`
+ 2. For each recipient:
+ - `message = recipient.at_pre_channel_msg(message, channel, **kwargs)` -
+ allows for the message to be tweaked per-receiver (for example coloring it depending
+ on the users' preferences). If this method returns `False/None`, that
+ recipient is skipped.
+ - `recipient.channel_msg(message, channel, **kwargs)` - actually sends to recipient.
+ - `recipient.at_post_channel_msg(message, channel, **kwargs)` - any post-receive effects.
+ 3. `channel.at_post_channel_msg(message, **kwargs)`
+
+Note that `Accounts` and `Objects` both have their have separate sets of hooks.
+So make sure you modify the set actually used by your subcribers (or both).
+Default channels all use `Account` subscribers.
+
+## Channels in code
+
+For most common changes, the default channel, the recipient hooks and possibly
+overriding the `channel` command will get you very far. But you can also tweak
+channels themselves.
+
+Channels are [Typeclassed](./Typeclasses) entities. This means they are
+persistent in the database, can have [attributes](./Attributes) and [Tags](./Tags)
+and can be easily extended.
+
+To change which channel typeclass Evennia uses for default commands, change
+`settings.BASE_CHANNEL_TYPECLASS`. The base command class is
+[`evennia.comms.comms.DefaultChannel`](api:evennia.comms.comms.DefaultChannel).
+There is an empty child class in `mygame/typeclasses/channels.py`, same
+as for other typelass-bases.
+
+In code you create a new channel with `evennia.create_channel` or
+`Channel.create`:
+
+```python
+ from evennia import create_channel, search_object
+ from typeclasses.channels import Channel
+
+ channel = create_channel("my channel", aliases=["mychan"], locks=..., typeclass=...)
+ # alternative
+ channel = Channel.create("my channel", aliases=["mychan"], locks=...)
+
+ # connect to it
+ me = search_object(key="Foo")[0]
+ channel.connect(me)
+
+ # send to it (this will trigger the channel_msg hooks described earlier)
+ channel.msg("Hello world!", senders=me)
+
+ # view subscriptions (the SubscriptionHandler handles all subs under the hood)
+ channel.subscriptions.has(me) # check we subbed
+ channel.subscriptions.all() # get all subs
+ channel.subscriptions.online() # get only subs currently online
+ channel.subscriptions.clear() # unsub all
+
+ # leave channel
+ channel.disconnect(me)
+
+ # permanently delete channel (will unsub everyone)
+ channel.delete()
+
+```
+
+The Channel's `.connect` method will accept both `Account` and `Object` subscribers
+and will handle them transparently.
+
+The channel has many more hooks, both hooks shared with all typeclasses as well
+as special ones related to muting/banning etc. See the channel class for
+details.
+
+## Channel logging
+
+```versionchanged:: 0.7
+
+ Channels changed from using Msg to TmpMsg and optional log files.
+```
+```versionchanged:: 1.0
+
+ Channels stopped supporting Msg and TmpMsg, using only log files.
+```
+
+The channel messages are not stored in the database. A channel is instead
+always logged to a regular text log-file
+`mygame/server/logs/channel_.log`. This is where `channels/history channelname`
+gets its data from. A channel's log will rotate when it grows too big, which
+thus also automatically limits the max amount of history a user can view with
+`/history`.
+
+The log file name is set on the channel class as the `log_file` property. This
+is a string that takes the formatting token `{channelname}` to be replaced with
+the (lower-case) name of the channel. By default the log is written to in the
+channel's `at_post_channel_msg` method.
+
+
+### Properties on Channels
+
+Channels have all the standard properties of a Typeclassed entity (`key`,
+`aliases`, `attributes`, `tags`, `locks` etc). This is not an exhaustive list;
+see the [Channel api docs](api:evennia.comms.comms.DefaultChannel) for details.
+
+- `send_to_online_only` - this class boolean defaults to `True` and is a
+ sensible optimization since people offline people will not see the message anyway.
+- `log_file` - this is a string that determines the name of the channel log file. Default
+ is `"channel_{channelname}.log"`. The log file will appear in `settings.LOG_DIR` (usually
+ `mygame/server/logs/`). You should usually not change this.
+- `channel_prefix_string` - this property is a string to easily change how
+ the channel is prefixed. It takes the `channelname` format key. Default is `"[{channelname}] "`
+ and produces output like `[public] ...``.
+- `subscriptions` - this is the [SubscriptionHandler](api:evennia.comms.comms#SubscriptionHandler), which
+ has methods `has`, `add`, `remove`, `all`, `clear` and also `online` (to get
+ only actually online channel-members).
+- `wholist`, `mutelist`, `banlist` are properties that return a list of subscribers,
+ as well as who are currently muted or banned.
+- `channel_msg_nick_pattern` - this is a regex pattern for performing the in-place nick
+ replacement (detect that `channelalias ` to `channel channelname = `.
+- `remove_user_channel_alias(user, alias, **kwargs)` - remove an alias. Note that this is
+ a class-method that will happily remove found channel-aliases from the user linked to _any_
+ channel, not only from the channel the method is called on.
+- `pre_join_channel(subscriber)` - if this returns `False`, connection will be refused.
+- `post_join_channel(subscriber)` - by default this sets up a users's channel-nicks/aliases.
+- `pre_leave_channel(subscriber)` - if this returns `False`, the user is not allowed to leave.
+- `post_leave_channel(subscriber)` - this will clean up any channel aliases/nicks of the user.
+- `delete` the standard typeclass-delete mechanism will also automatically un-subscribe all
+ subscribers (and thus wipe all their aliases).
-TODO: Channels are covered in [Communications](./Communications) right now.
\ No newline at end of file
diff --git a/docs/1.0-dev/_sources/Components/Communications.md.txt b/docs/1.0-dev/_sources/Components/Communications.md.txt
index f7c0130595..21570fa740 100644
--- a/docs/1.0-dev/_sources/Components/Communications.md.txt
+++ b/docs/1.0-dev/_sources/Components/Communications.md.txt
@@ -1,113 +1,8 @@
# Communications
+TODO: Remove this page?
-Apart from moving around in the game world and talking, players might need other forms of
-communication. This is offered by Evennia's `Comm` system. Stock evennia implements a 'MUX-like'
-system of channels, but there is nothing stopping you from changing things to better suit your
-taste.
-
-Comms rely on two main database objects - `Msg` and `Channel`. There is also the `TempMsg` which
-mimics the API of a `Msg` but has no connection to the database.
-
-## Msg
-
-The `Msg` object is the basic unit of communication in Evennia. A message works a little like an
-e-mail; it always has a sender (a [Account](./Accounts)) and one or more recipients. The recipients
-may be either other Accounts, or a *Channel* (see below). You can mix recipients to send the message
-to both Channels and Accounts if you like.
-
-Once created, a `Msg` is normally not changed. It is peristently saved in the database. This allows
-for comprehensive logging of communications. This could be useful for allowing senders/receivers to
-have 'mailboxes' with the messages they want to keep.
-
-### Properties defined on `Msg`
-
-- `senders` - this is a reference to one or many [Account](./Accounts) or [Objects](./Objects) (normally
-*Characters*) sending the message. This could also be an *External Connection* such as a message
-coming in over IRC/IMC2 (see below). There is usually only one sender, but the types can also be
-mixed in any combination.
-- `receivers` - a list of target [Accounts](./Accounts), [Objects](./Objects) (usually *Characters*) or
-*Channels* to send the message to. The types of receivers can be mixed in any combination.
-- `header` - this is a text field for storing a title or header for the message.
-- `message` - the actual text being sent.
-- `date_sent` - when message was sent (auto-created).
-- `locks` - a [lock definition](./Locks).
-- `hide_from` - this can optionally hold a list of objects, accounts or channels to hide this `Msg`
-from. This relationship is stored in the database primarily for optimization reasons, allowing for
-quickly post-filter out messages not intended for a given target. There is no in-game methods for
-setting this, it's intended to be done in code.
-
-You create new messages in code using `evennia.create_message` (or
-`evennia.utils.create.create_message.`)
-
-## TempMsg
-
-`evennia.comms.models` also has `TempMsg` which mimics the API of `Msg` but is not connected to the
-database. TempMsgs are used by Evennia for channel messages by default. They can be used for any
-system expecting a `Msg` but when you don't actually want to save anything.
-
-## Channels
-
-Channels are [Typeclassed](./Typeclasses) entities, which mean they can be easily extended and their
-functionality modified. To change which channel typeclass Evennia uses, change
-settings.BASE_CHANNEL_TYPECLASS.
-
-Channels act as generic distributors of messages. Think of them as "switch boards" redistributing
-`Msg` or `TempMsg` objects. Internally they hold a list of "listening" objects and any `Msg` (or
-`TempMsg`) sent to the channel will be distributed out to all channel listeners. Channels have
-[Locks](./Locks) to limit who may listen and/or send messages through them.
-
-The *sending* of text to a channel is handled by a dynamically created [Command](./Commands) that
-always have the same name as the channel. This is created for each channel by the global
-`ChannelHandler`. The Channel command is added to the Account's cmdset and normal command locks are
-used to determine which channels are possible to write to. When subscribing to a channel, you can
-then just write the channel name and the text to send.
-
-The default ChannelCommand (which can be customized by pointing `settings.CHANNEL_COMMAND_CLASS` to
-your own command), implements a few convenient features:
-
- - It only sends `TempMsg` objects. Instead of storing individual entries in the database it instead
-dumps channel output a file log in `server/logs/channel_.log`. This is mainly for
-practical reasons - we find one rarely need to query individual Msg objects at a later date. Just
-stupidly dumping the log to a file also means a lot less database overhead.
- - It adds a `/history` switch to view the 20 last messages in the channel. These are read from the
-end of the log file. One can also supply a line number to start further back in the file (but always
-20 entries at a time). It's used like this:
-
- > public/history
- > public/history 35
-
-
-There are two default channels created in stock Evennia - `MudInfo` and `Public`. `MudInfo`
-receives server-related messages meant for Admins whereas `Public` is open to everyone to chat on
-(all new accounts are automatically joined to it when logging in, it is useful for asking
-questions). The default channels are defined by the `DEFAULT_CHANNELS` list (see
-`evennia/settings_default.py` for more details).
-
-You create new channels with `evennia.create_channel` (or `evennia.utils.create.create_channel`).
-
-In code, messages are sent to a channel using the `msg` or `tempmsg` methods of channels:
-
- channel.msg(msgobj, header=None, senders=None, persistent=True)
-
-The argument `msgobj` can be either a string, a previously constructed `Msg` or a `TempMsg` - in the
-latter cases all the following keywords are ignored since the message objects already contains all
-this information. If `msgobj` is a string, the other keywords are used for creating a new `Msg` or
-`TempMsg` on the fly, depending on if `persistent` is set or not. By default, a `TempMsg` is emitted
-for channel communication (since the default ChannelCommand instead logs to a file).
-
-```python
- # assume we have a 'sender' object and a channel named 'mychan'
-
- # manually sending a message to a channel
- mychan.msg("Hello!", senders=[sender])
-```
-
-### Properties defined on `Channel`
-
-- `key` - main name for channel
-- `aliases` - alternative native names for channels
-- `desc` - optional description of channel (seen in listings)
-- `keep_log` (bool) - if the channel should store messages (default)
-- `locks` - A [lock definition](./Locks). Channels normally use the access_types `send, control` and
-`listen`.
\ No newline at end of file
+- [Channels](./Channels) - are used for implementing in-game chat rooms.
+- [Msg](./Msg)-objects are used for storing messages in the database (email-like)
+ and is a building block for implementing other game systems. It's used by the
+ `page` command by default.
diff --git a/docs/1.0-dev/_sources/Components/Help-System.md.txt b/docs/1.0-dev/_sources/Components/Help-System.md.txt
index 9914da6447..537a692d45 100644
--- a/docs/1.0-dev/_sources/Components/Help-System.md.txt
+++ b/docs/1.0-dev/_sources/Components/Help-System.md.txt
@@ -1,37 +1,164 @@
# Help System
-
-An important part of Evennia is the online help system. This allows the players and staff alike to
-learn how to use the game's commands as well as other information pertinent to the game. The help
-system has many different aspects, from the normal editing of help entries from inside the game, to
-auto-generated help entries during code development using the *auto-help system*.
-
-## Viewing the help database
-
-The main command is `help`:
-
- help [searchstring]
-
-This will show a list of help entries, ordered after categories. You will find two sections,
-*Command help entries* and *Other help entries* (initially you will only have the first one). You
-can use help to get more info about an entry; you can also give partial matches to get suggestions.
-If you give category names you will only be shown the topics in that category.
+Evennia has an extensive help system covering both command-help and regular
+free-form help documentation. It supports subtopics and if failing to find a
+match it will provide suggestsions, first from alternative topics and then by
+finding mentions of the search term in help entries.
-## Command Auto-help system
+```
+------------------------------------------------------------------------------
+Help for The theatre (aliases: the hub, curtains)
-A common item that requires help entries are in-game commands. Keeping these entries up-to-date with
-the actual source code functionality can be a chore. Evennia's commands are therefore auto-
-documenting straight from the sources through its *auto-help system*. Only commands that you and
-your character can actually currently use are picked up by the auto-help system. That means an admin
-will see a considerably larger amount of help topics than a normal player when using the default
-`help` command.
+The theatre is at the centre of the city, both literally and figuratively ...
+(A lot more text about it follows ...)
-The auto-help system uses the `__doc__` strings of your command classes and formats this to a nice-
-looking help entry. This makes for a very easy way to keep the help updated - just document your
-commands well and updating the help file is just a `@reload` away. There is no need to manually
-create and maintain help database entries for commands; as long as you keep the docstrings updated
-your help will be dynamically updated for you as well.
+Subtopics:
+ theatre/lore
+ theatre/layout
+ theatre/dramatis personae
+------------------------------------------------------------------------------
+```
+```
+------------------------------------------------------------------------------
+No help found
+
+There is no help topic matching 'evennia'.
+... But matches where found within the help texts of the suggestions below.
+
+Suggestions:
+ grapevine2chan, about, irc2chan
+-----------------------------------------------------------------------------
+```
+
+## Using the help system from in-game
+
+The help system is accessed in-game by use of the `help` command:
+
+ help
+
+Sub-topics are accessed as `help //...`.
+
+Creating a new help entry from in-game is done with
+
+ sethelp [;aliases] [,category] [,lockstring] =
+
+For example
+
+ sethelp The Gods;pantheon, Lore = In the beginning all was dark ...
+
+Use the `/edit` switch to open the EvEditor for more convenient in-game writing
+(but note that devs can also create help entries outside the game using their
+regular code editor, see below).
+
+## Sources of help entries
+
+Evennia collects help entries from three sources:
+
+- _Auto-generated command help_ - this is literally the doc-strings of the [Command classes](./Commands).
+ The idea is that the command docs are easier to maintain and keep up-to-date if
+ the developer can change them at the same time as they do the code.
+- _Database-stored help entries_ - These are created in-game (using the default `sethelp` command
+ as exemplified in the previous section).
+- _File-stored help entries_ - These are created outside the game, as dicts in
+ normal Python modules. They allows developers to write and maintain their help files using
+ a proper text editor.
+
+### The Help Entry
+
+All help entries (no matter the source) have the following properties:
+
+- `key` - This is the main topic-name. For Commands, this is literally the command's `key`.
+- `aliases` - Alternate names for the help entry. This can be useful if the main name is hard to remember.
+- `help_category` - The general grouping of the entry. This is optional. If not given it will use the
+ default category given by `settings.COMMAND_DEFAULT_HELP_CATEGORY` for Commands and `settings.DEFAULT_HELP_CATEGORY`
+ for file+db help entries.
+- `locks` - This defines who may read this entry. The locktype checked by the `help` command is `view`. In the
+ case of Commands, it's more common that the `cmd` lock fails - in that case the command is not loaded
+ into the help parser at all.
+- `tags` - This is not used by default, but could be used to further organize help entries.
+- `text` - The actual help entry text. This will be dedented and stripped of
+ extra space at beginning and end.
+
+
+A `text` that scrolls off the screen will automatically be paginated by
+the [EvMore](./EvMore) pager (you can control this with
+`settings.HELP_MORE_ENABLED=False`). If you use EvMore and want to control
+exactly where the pager should break the page, mark the break with the control
+character `\f`.
+
+
+#### Subtopics
+
+```versionadded:: 1.0
+```
+
+Rather than making a very long help entry, the `text` may also be broken up
+into _subtopics_. A list of the next level of subtopics are shown below the
+main help text and allows the user to read more about some particular detail
+that wouldn't fit in the main text.
+
+Subtopics use a markup slightly similar to markdown headings. The top level
+heading must be named `# subtopics` (non case-sensitive) and the following
+headers must be sub-headings to this (so `## subtopic name` etc). All headings
+are non-case sensitive (the help command will format them). The topics can be
+nested at most to a depth of 5 (which is probably too many levels already). The
+parser uses fuzzy matching to find the subtopic, so one does not have to type
+it all out exactly.
+
+Below is an example of a `text` with sub topics.
+
+
+```
+The theatre is the heart of the city, here you can find ...
+(This is the main help text, what you get with `help theatre`)
+
+# subtopics
+
+## lore
+
+The theatre holds many mysterious things...
+(`help theatre/lore`)
+
+### the grand opening
+
+The grand opening is the name for a mysterious event where ghosts appeared ...
+(`this is a subsub-topic to lore, accessible as `help theatre/lore/grand` or
+any other partial match).
+
+### the Phantom
+
+Deep under the theatre, rumors has it a monster hides ...
+(another subsubtopic, accessible as `help theatre/lore/phantom`)
+
+## layout
+
+The theatre is a two-story building situated at ...
+(`help theatre/layout`)
+
+## dramatis personae
+
+There are many interesting people prowling the halls of the theatre ...
+(`help theatre/dramatis` or `help theathre/drama` or `help theatre/personae` would work)
+
+### Primadonna Ada
+
+Everyone knows the primadonna! She is ...
+(A subtopic under dramatis personae, accessible as `help theatre/drama/ada` etc)
+
+### The gatekeeper
+
+He always keeps an eye on the door and ...
+(`help theatre/drama/gate`)
+
+```
+
+### Command Auto-help system
+
+The auto-help system uses the `__doc__` strings of your command classes and
+formats this to a nice- looking help entry. This makes for a very easy way to
+keep the help updated - just document your commands well and updating the help
+file is just a `reload` away.
Example (from a module with command definitions):
@@ -64,7 +191,8 @@ structure shown above.
You should also supply the `help_category` class property if you can; this helps to group help
entries together for people to more easily find them. See the `help` command in-game to see the
-default categories. If you don't specify the category, "General" is assumed.
+default categories. If you don't specify the category, `settings.COMMAND_DEFAULT_HELP_CATEGORY`
+(default is "General") is used.
If you don't want your command to be picked up by the auto-help system at all (like if you want to
write its docs manually using the info in the next section or you use a [cmdset](./Command-Sets) that
@@ -72,51 +200,127 @@ has its own help functionality) you can explicitly set `auto_help` class propert
command definition.
Alternatively, you can keep the advantages of *auto-help* in commands, but control the display of
-command helps. You can do so by overriding the command's `get_help()` method. By default, this
+command helps. You can do so by overriding the command's `get_help(caller, cmdset)` method. By default, this
method will return the class docstring. You could modify it to add custom behavior: the text
returned by this method will be displayed to the character asking for help in this command.
-## Database help entries
+### Database-help entries
-These are all help entries not involving commands (this is handled automatically by the [Command
-Auto-help system](Help-System#command-auto-help-system)). Non-automatic help entries describe how
-your particular game is played - its rules, world descriptions and so on.
-
-A help entry consists of four parts:
-
-- The *topic*. This is the name of the help entry. This is what players search for when they are
-looking for help. The topic can contain spaces and also partial matches will be found.
-- The *help category*. Examples are *Administration*, *Building*, *Comms* or *General*. This is an
-overall grouping of similar help topics, used by the engine to give a better overview.
-- The *text* - the help text itself, of any length.
-- locks - a [lock definition](./Locks). This can be used to limit access to this help entry, maybe
-because it's staff-only or otherwise meant to be restricted. Help commands check for `access_type`s
-`view` and `edit`. An example of a lock string would be `view:perm(Builders)`.
-
-You can create new help entries in code by using `evennia.create_help_entry()`.
+These are most commonly created in-game using the `sethelp` command. If you need to create one
+manually, you can do so with `evennia.create_help_entry()`:
```python
+
from evennia import create_help_entry
entry = create_help_entry("emote",
"Emoting is important because ...",
category="Roleplaying", locks="view:all()")
```
-From inside the game those with the right permissions can use the `@sethelp` command to add and
-modify help entries.
+The entity being created is a [evennia.help.models.HelpEntry](api:evennia.help.models.HelpEntry)
+object. This is _not_ a [Typeclassed](./Typeclasses) entity and is not meant to
+be modified to any great degree. It holds the properties listed earlier. The
+text is stored in a field `entrytext`. It does not provide a `get_help` method
+like commands, stores and returns the `entrytext` directly.
- > @sethelp/add emote = The emote command is ...
+You can search for `HelpEntry` objects using `evennia.search_help` but note
+that this will not return the two other types of help entries.
-Using `@sethelp` you can add, delete and append text to existing entries. By default new entries
-will go in the *General* help category. You can change this using a different form of the `@sethelp`
-command:
- > @sethelp/add emote, Roleplaying = Emoting is important because ...
+### File-help entries
-If the category *Roleplaying* did not already exist, it is created and will appear in the help
-index.
+```versionadded:: 1.0
+```
-You can, finally, define a lock for the help entry by following the category with a [lock
-definition](Locks):
+File-help entries are created by the game development team outside of the game. The
+help entries are defined in normal Python modules (`.py` file ending) containing
+a `dict` to represent each entry. They require a server `reload` before any changes
+apply.
- > @sethelp/add emote, Roleplaying, view:all() = Emoting is ...
\ No newline at end of file
+- Evennia will look through all modules given by `settings.FILE_HELP_ENTRY_MODULES`. This
+ should be a list of python-paths for Evennia to import.
+- If this module contains a top-level variable `HELP_ENTRY_DICTS`, this will be imported
+ and must be a `list` of help-entry dicts.
+- If no `HELP_ENTRY_DICTS` list is found, _every_ top-level variable in the
+ module that is a `dict` will be read as a help entry. The variable-names will
+ be ignored in this case.
+
+If you add multiple modules to be read, same-keyed help entries added later in the list
+will override coming before.
+
+Each entry dict must define keys to match that needed by all help entries.
+Here's an example of a help module:
+
+```python
+
+# in a module pointed to by settings.FILE_HELP_ENTRY_MODULES
+
+HELP_ENTRY_DICTS = [
+ {
+ "key": "The Gods", # case-insensitive, can be searched by 'gods' too
+ "aliases": ['pantheon', 'religion']
+ "category": "Lore",
+ "text": '''
+ The gods formed the world ...
+
+ # Subtopics
+
+ ## Pantheon
+
+ The pantheon consists of 40 gods that ...
+
+ ### God of love
+
+ The most prominent god is ...
+
+ ### God of war
+
+ Also known as 'the angry god', this god is known to ...
+
+ '''
+ },
+ {
+ "key": "The mortals",
+
+ }
+]
+
+```
+
+The help entry text will be dedented and will retain paragraphs. You should try
+to keep your strings a reasonable width (it will look better). Just reload the
+server and the file-based help entries will be available to view.
+
+
+## Customizing the look of the help system
+
+This is done almost exclusively by overriding the `help` command
+[evennia.commands.default.help.CmdHelp](api:evennia.commands.default.help#CmdHelp).
+
+Since the available commands may vary from moment to moment, `help` is
+responsible for collating the three sources of help-entries (commands/db/file)
+together and search through them on the fly. It also does all the formatting of
+the output.
+
+To make it easier to tweak the look, the parts of the code that changes the
+visual presentation has been broken out into separate methods `format_help_entry` and
+`format_help_index` - override these in your version of `help` to change the display
+as you please. See the api link above for details.
+
+
+## Technical notes
+
+Since it needs to search so different types of data, the help system has to
+collect all possibilities in memory before searching through the entire set. It
+uses the [Lunr](https://github.com/yeraydiazdiaz/lunr.py) search engine to
+search through the main bulk of help entries. Lunr is a mature engine used for
+web-pages and produces much more sensible results than previous solutions.
+
+Once the main entry has been found, subtopics are then searched with
+simple `==`, `startswith` and `in` matching (there are so relatively few of them
+at that point).
+
+```versionchanged:: 1.0
+ Replaced the bag-of-words algorithm with lunr.
+
+```
diff --git a/docs/1.0-dev/_sources/Components/Msg.md.txt b/docs/1.0-dev/_sources/Components/Msg.md.txt
new file mode 100644
index 0000000000..9e69584dc1
--- /dev/null
+++ b/docs/1.0-dev/_sources/Components/Msg.md.txt
@@ -0,0 +1,91 @@
+# Msg
+
+The [Msg](api:evennia.comms.models.Msg) object represents a database-saved
+piece of communication. Think of it as a discrete piece of email - it contains
+a message, some metadata and will always have a sender and one or more
+recipients.
+
+Once created, a Msg is normally not changed. It is persitently saved in the
+database. This allows for comprehensive logging of communications. Here are some
+good uses for `Msg` objects:
+
+- page/tells (the `page` command is how Evennia uses them out of the box)
+- messages in a bulletin board
+- game-wide email stored in 'mailboxes'.
+
+
+```important::
+
+ A `Msg` does not have any in-game representation. So if you want to use them
+ to represent in-game mail/letters, the physical letters would never be
+ visible in a room (possible to steal, spy on etc) unless you make your
+ spy-system access the Msgs directly (or go to the trouble of spawning an
+ actual in-game letter-object based on the Msg)
+
+
+```
+
+```versionchanged:: 1.0
+ Channels dropped Msg-support. Now only used in `page` command by default.
+```
+
+## Msg in code
+
+The Msg is intended to be used exclusively in code, to build other game systems. It is _not_
+a [Typeclassed](./Typeclasses) entity, which means it cannot (easily) be overridden. It
+doesn't support Attributes (but it _does_ support [Tags](./Tags)). It tries to be lean
+and small since a new one is created for every message.
+
+You create a new message with `evennia.create_message`:
+
+```python
+ from evennia import create_message
+ message = create_message(senders, message, receivers,
+ locks=..., tags=..., header=...)
+```
+
+You can search for `Msg` objects in various ways:
+
+
+```python
+ from evennia import search_message, Msg
+
+ # args are optional. Only a single sender/receiver should be passed
+ messages = search_message(sender=..., receiver=..., freetext=..., dbref=...)
+
+ # get all messages for a given sender/receiver
+ messages = Msg.objects.get_msg_by_sender(sender)
+ messages = Msg.objects.get_msg_by_receiver(recipient)
+
+```
+
+### Properties on Msg
+
+- `senders` - there must always be at least one sender. This is one of [Account](./Accounts), [Object](./Objects), [Script](./Scripts)
+ or _external_ - which is a string uniquely identifying the sender. The latter can be used by
+ a sender-system that doesn't fit into Evennia's normal typeclass-system.
+ While most systems expect a single sender, it's possible to have any number of them.
+- `receivers` - these are the ones to see the Msg. These are again one of
+ [Account](./Accounts), [Object](./Objects) or [Script](./Scripts). It's in principle possible to have
+ zero receivers but most usages of Msg expects one or more.
+- `header` - this is an optional text field that can contain meta-information about the message. For
+ an email-like system it would be the subject line. This can be independently searched, making
+ this a powerful place for quickly finding messages.
+- `message` - the actual text being sent.
+- `date_sent` - this is auto-set to the time the Msg was created (and thus presumably sent).
+- `locks` - the Evennia [lock handler](./Locks). Use with `locks.add()` etc and check locks with `msg.access()`
+ like for all other lockable entities. This can be used to limit access to the contents
+ of the Msg. The default lock-type to check is `'read'`.
+- `hide_from` - this is an optional list of [Accounts](./Accounts) or [Objects](./Objects) that
+ will not see this Msg. This relationship is available mainly for optimization
+ reasons since it allows quick filtering of messages not intended for a given
+ target.
+
+
+## TempMsg
+
+[evennia.comms.models.TempMsg](api:evennia.comms.models.TempMsg) is an object
+that implements the same API as the regular `Msg`, but which has no database
+component (and thus cannot be searched). It's meant to plugged into systems
+expecting a `Msg` but where you just want to process the message without saving
+it.
diff --git a/docs/1.0-dev/_sources/Contribs/Crafting.md.txt b/docs/1.0-dev/_sources/Contribs/Crafting.md.txt
index b3d909564d..7b2950e23f 100644
--- a/docs/1.0-dev/_sources/Contribs/Crafting.md.txt
+++ b/docs/1.0-dev/_sources/Contribs/Crafting.md.txt
@@ -1,39 +1,39 @@
-# Crafting system contrib
+# Crafting system contrib
_Contrib by Griatch 2020_
```versionadded:: 1.0
```
-This contrib implements a full Crafting system that can be expanded and modified to fit your game.
+This contrib implements a full Crafting system that can be expanded and modified to fit your game.
-- See the [evennia/contrib/crafting/crafting.py API](api:evennia.contrib.crafting.crafting) for installation
+- See the [evennia/contrib/crafting/crafting.py API](api:evennia.contrib.crafting.crafting) for installation
instructrions.
-- See the [sword example](api:evennia.contrib.crafting.example_recipes) for an example of how to design
+- See the [sword example](api:evennia.contrib.crafting.example_recipes) for an example of how to design
a crafting tree for crafting a sword from base elements.
-From in-game it uses the new `craft` command:
+From in-game it uses the new `craft` command:
```bash
> craft bread from flour, eggs, salt, water, yeast using oven, roller
> craft bandage from cloth using scissors
```
-The syntax is `craft [from ,...][ using ,...]`.
+The syntax is `craft [from ,...][ using ,...]`.
-The above example uses the `bread` *recipe* and requires `flour`, `eggs`, `salt`, `water` and `yeast` objects
-to be in your inventory. These will be consumed as part of crafting (baking) the bread.
+The above example uses the `bread` *recipe* and requires `flour`, `eggs`, `salt`, `water` and `yeast` objects
+to be in your inventory. These will be consumed as part of crafting (baking) the bread.
-The `oven` and `roller` are "tools" that can be either in your inventory or in your current location (you are not carrying an oven
-around with you after all). Tools are *not* consumed in the crafting. If the added ingredients/tools matches
+The `oven` and `roller` are "tools" that can be either in your inventory or in your current location (you are not carrying an oven
+around with you after all). Tools are *not* consumed in the crafting. If the added ingredients/tools matches
the requirements of the recipe, a new `bread` object will appear in the crafter's inventory.
-If you wanted, you could also picture recipes without any consumables:
+If you wanted, you could also picture recipes without any consumables:
```
> craft fireball using wand, spellbook
```
-With a little creativity, the 'recipe' concept could be adopted to all sorts of things, like puzzles or
+With a little creativity, the 'recipe' concept could be adopted to all sorts of things, like puzzles or
magic systems.
In code, you can craft using the `evennia.contrib.crafting.crafting.craft` function:
@@ -46,22 +46,22 @@ result = craft(caller, "recipename", *inputs)
```
Here, `caller` is the one doing the crafting and `*inputs` is any combination of consumables and/or tool
Objects. The system will identify which is which by the [Tags](../Components/Tags) on them (see below)
-The `result` is always a list.
+The `result` is always a list.
## Adding new recipes
A *recipe* is a class inheriting from `evennia.contrib.crafting.crafting.CraftingRecipe`. This class
implements the most common form of crafting - that using in-game objects. Each recipe is a separate class
-which gets initialized with the consumables/tools you provide.
+which gets initialized with the consumables/tools you provide.
-For the `craft` command to find your custom recipes, you need to tell Evennia where they are. Add a new
+For the `craft` command to find your custom recipes, you need to tell Evennia where they are. Add a new
line to your `mygame/server/conf/settings.py` file, with a list to any new modules with recipe classes.
```python
CRAFT_RECIPE_MODULES = ["world.myrecipes"]
```
-(You need to reload after adding this). All global-level classes in these modules (whose names don't start
+(You need to reload after adding this). All global-level classes in these modules (whose names don't start
with underscore) are considered by the system as viable recipes.
Here we assume you created `mygame/world/myrecipes.py` to match the above example setting:
@@ -80,23 +80,23 @@ class WoodenPuppetRecipe(CraftingRecipe):
{"key": "A carved wooden doll",
"typeclass": "typeclasses.objects.decorations.Toys",
"desc": "A small carved doll"}
- ]
+ ]
```
-This specifies which tags to look for in the inputs. It defines a [Prototype](../Components/Prototypes)
-for the recipe to use to spawn the result on the fly (a recipe could spawn more than one result if needed).
-Instead of specifying the full prototype-dict, you could also just provide a list of `prototype_key`s to
+This specifies which tags to look for in the inputs. It defines a [Prototype](../Components/Prototypes)
+for the recipe to use to spawn the result on the fly (a recipe could spawn more than one result if needed).
+Instead of specifying the full prototype-dict, you could also just provide a list of `prototype_key`s to
existing prototypes you have.
-After reloading the server, this recipe would now be available to use. To try it we should
-create materials and tools to insert into the recipe.
+After reloading the server, this recipe would now be available to use. To try it we should
+create materials and tools to insert into the recipe.
-The recipe analyzes inputs, looking for [Tags](../Components/Tags) with specific tag-categories.
-The tag-category used can be set per-recipe using the (`.consumable_tag_category` and
-`.tool_tag_category` respectively). The defaults are `crafting_material` and `crafting_tool`. For
-the puppet we need one object with the `wood` tag and another with the `knife` tag:
+The recipe analyzes inputs, looking for [Tags](../Components/Tags) with specific tag-categories.
+The tag-category used can be set per-recipe using the (`.consumable_tag_category` and
+`.tool_tag_category` respectively). The defaults are `crafting_material` and `crafting_tool`. For
+the puppet we need one object with the `wood` tag and another with the `knife` tag:
```python
from evennia import create_object
@@ -105,9 +105,9 @@ knife = create_object(key="Hobby knife", tags=[("knife", "crafting_tool")])
wood = create_object(key="Piece of wood", tags[("wood", "crafting_material")])
```
-Note that the objects can have any name, all that matters is the tag/tag-category. This means if a
-"bayonet" also had the "knife" crafting tag, it could also be used to carve a puppet. This is also
-potentially interesting for use in puzzles and to allow users to experiment and find alternatives to
+Note that the objects can have any name, all that matters is the tag/tag-category. This means if a
+"bayonet" also had the "knife" crafting tag, it could also be used to carve a puppet. This is also
+potentially interesting for use in puzzles and to allow users to experiment and find alternatives to
know ingredients.
By the way, there is also a simple shortcut for doing this:
@@ -116,15 +116,15 @@ By the way, there is also a simple shortcut for doing this:
tools, consumables = WoodenPuppetRecipe.seed()
```
-The `seed` class-method will create simple dummy objects that fulfills the recipe's requirements. This
+The `seed` class-method will create simple dummy objects that fulfills the recipe's requirements. This
is great for testing.
-Assuming these objects were put in our inventory, we could now craft using the in-game command:
+Assuming these objects were put in our inventory, we could now craft using the in-game command:
```bash
> craft wooden puppet from wood using hobby knife
```
-In code we would do
+In code we would do
```python
from evennia.contrub.crafting.crafting import craft
@@ -132,7 +132,7 @@ puppet = craft(crafter, "wooden puppet", knife, wood)
```
In the call to `craft`, the order of `knife` and `wood` doesn't matter - the recipe will sort out which
-is which based on their tags.
+is which based on their tags.
## Deeper customization of recipes
@@ -147,36 +147,36 @@ recipe = MyRecipe(crafter, *(tools + consumables))
result = recipe.craft()
```
-This is useful for testing and allows you to use the class directly without adding it to a module
-in `settings.CRAFTING_RECIPE_MODULES`.
+This is useful for testing and allows you to use the class directly without adding it to a module
+in `settings.CRAFTING_RECIPE_MODULES`.
-Even without modifying more than the class properties, there are a lot of options to set on
-the `CraftingRecipe` class. Easiest is to refer to the
+Even without modifying more than the class properties, there are a lot of options to set on
+the `CraftingRecipe` class. Easiest is to refer to the
[CraftingRecipe api documentation](evennia.contrib.crafting.crafting.html#evennia.contrib.crafting.crafting.CraftingRecipe).
-For example, you can customize the validation-error messages, decide if the ingredients have
+For example, you can customize the validation-error messages, decide if the ingredients have
to be exactly right, if a failure still consumes the ingredients or not, and much more.
For even more control you can override hooks in your own class:
-- `pre_craft` - this should handle input validation and store its data in `.validated_consumables` and
+- `pre_craft` - this should handle input validation and store its data in `.validated_consumables` and
`validated_tools` respectively. On error, this reports the error to the crafter and raises the
`CraftingValidationError`.
-- `do_craft` - this will only be called if `pre_craft` finished without an exception. This should
- return the result of the crafting, by spawnging the prototypes. Or the empty list if crafting
- fails for some reason. This is the place to add skill-checks or random chance if you need it
- for your game.
-- `post_craft` - this receives the result from `do_craft` and handles error messages and also deletes
+- `craft` - this will only be called if `pre_craft` finished without an exception. This should
+ return the result of the crafting, by spawnging the prototypes. Or the empty list if crafting
+ fails for some reason. This is the place to add skill-checks or random chance if you need it
+ for your game.
+- `post_craft` - this receives the result from `craft` and handles error messages and also deletes
any consumables as needed. It may also modify the result before returning it.
-- `msg` - this is a wrapper for `self.crafter.msg` and should be used to send messages to the
+- `msg` - this is a wrapper for `self.crafter.msg` and should be used to send messages to the
crafter. Centralizing this means you can also easily modify the sending style in one place later.
-The class constructor (and the `craft` access function) takes optional `**kwargs`. These are passed
+The class constructor (and the `craft` access function) takes optional `**kwargs`. These are passed
into each crafting hook. These are unused by default but could be used to customize things per-call.
### Skilled crafters
-What the crafting system does not have out of the box is a 'skill' system - the notion of being able
-to fail the craft if you are not skilled enough. Just how skills work is game-dependent, so to add
+What the crafting system does not have out of the box is a 'skill' system - the notion of being able
+to fail the craft if you are not skilled enough. Just how skills work is game-dependent, so to add
this you need to make your own recipe parent class and have your recipes inherit from this.
@@ -189,7 +189,7 @@ class SkillRecipe(CraftingRecipe):
difficulty = 20
- def do_craft(self, **kwargs):
+ def craft(self, **kwargs):
"""The input is ok. Determine if crafting succeeds"""
# this is set at initialization
@@ -201,15 +201,15 @@ class SkillRecipe(CraftingRecipe):
# roll for success:
if randint(1, 100) <= (crafting_skill - self.difficulty):
# all is good, craft away
- return super().do_craft()
+ return super().craft()
else:
self.msg("You are not good enough to craft this. Better luck next time!")
return []
```
-In this example we introduce a `.difficulty` for the recipe and makes a 'dice roll' to see
-if we succed. We would of course make this a lot more immersive and detailed in a full game. In
-principle you could customize each recipe just the way you want it, but you could also inherit from
-a central parent like this to cut down on work.
+In this example we introduce a `.difficulty` for the recipe and makes a 'dice roll' to see
+if we succed. We would of course make this a lot more immersive and detailed in a full game. In
+principle you could customize each recipe just the way you want it, but you could also inherit from
+a central parent like this to cut down on work.
The [sword recipe example module](api:evennia.contrib.crafting.example_recipes) also shows an example
of a random skill-check being implemented in a parent and then inherited for multiple use.
@@ -218,5 +218,5 @@ of a random skill-check being implemented in a parent and then inherited for mul
If you want to build something even more custom (maybe using different input types of validation logic)
you could also look at the `CraftingRecipe` parent class `CraftingRecipeBase`.
-It implements just the minimum needed to be a recipe and for big changes you may be better off starting
+It implements just the minimum needed to be a recipe and for big changes you may be better off starting
from this rather than the more opinionated `CraftingRecipe`.
diff --git a/docs/1.0-dev/_sources/Howto/Customize-channels.md.txt b/docs/1.0-dev/_sources/Howto/Customize-channels.md.txt
deleted file mode 100644
index 313df706a5..0000000000
--- a/docs/1.0-dev/_sources/Howto/Customize-channels.md.txt
+++ /dev/null
@@ -1,484 +0,0 @@
-# Customize channels
-
-
-# Channel commands in Evennia
-
-By default, Evennia's default channel commands are inspired by MUX. They all
-begin with "c" followed by the action to perform (like "ccreate" or "cdesc").
-If this default seems strange to you compared to other Evennia commands that
-rely on switches, you might want to check this tutorial out.
-
-This tutorial will also give you insight into the workings of the channel system.
-So it may be useful even if you don't plan to make the exact changes shown here.
-
-## What we will try to do
-
-Our mission: change the default channel commands to have a different syntax.
-
-This tutorial will do the following changes:
-
-- Remove all the default commands to handle channels.
-- Add a `+` and `-` command to join and leave a channel. So, assuming there is
-a `public` channel on your game (most often the case), you could type `+public`
-to join it and `-public` to leave it.
-- Group the commands to manipulate channels under the channel name, after a
-switch. For instance, instead of writing `cdesc public = My public channel`,
- you would write `public/desc My public channel`.
-
-
-> I listed removing the default Evennia commands as a first step in the
-> process. Actually, we'll move it at the very bottom of the list, since we
-> still want to use them, we might get it wrong and rely on Evennia commands
-> for a while longer.
-
-## A command to join, another to leave
-
-We'll do the most simple task at first: create two commands, one to join a
-channel, one to leave.
-
-> Why not have them as switches? `public/join` and `public/leave` for instance?
-
-For security reasons, I will hide channels to which the caller is not
-connected. It means that if the caller is not connected to the "public"
-channel, he won't be able to use the "public" command. This is somewhat
-standard: if we create an administrator-only channel, we don't want players to
-try (or even know) the channel command. Again, you could design it a different
-way should you want to.
-
-First create a file named `comms.py` in your `commands` package. It's
-a rather logical place, since we'll write different commands to handle
-communication.
-
-Okay, let's add the first command to join a channel:
-
-```python
-# in commands/comms.py
-from evennia.utils.search import search_channel
-from commands.command import Command
-
-class CmdConnect(Command):
- """
- Connect to a channel.
- """
-
- key = "+"
- help_category = "Comms"
- locks = "cmd:not pperm(channel_banned)"
- auto_help = False
-
- def func(self):
- """Implement the command"""
- caller = self.caller
- args = self.args
- if not args:
- self.msg("Which channel do you want to connect to?")
- return
-
- channelname = self.args
- channel = search_channel(channelname)
- if not channel:
- return
-
- # Check permissions
- if not channel.access(caller, 'listen'):
- self.msg("%s: You are not allowed to listen to this channel." % channel.key)
- return
-
- # If not connected to the channel, try to connect
- if not channel.has_connection(caller):
- if not channel.connect(caller):
- self.msg("%s: You are not allowed to join this channel." % channel.key)
- return
- else:
- self.msg("You now are connected to the %s channel. " % channel.key.lower())
- else:
- self.msg("You already are connected to the %s channel. " % channel.key.lower())
-```
-
-Okay, let's review this code, but if you're used to Evennia commands, it shouldn't be too strange:
-
-1. We import `search_channel`. This is a little helper function that we will use to search for
-channels by name and aliases, found in `evennia.utils.search`. It's just more convenient.
-2. Our class `CmdConnect` contains the body of our command to join a channel.
-3. Notice the key of this command is simply `"+"`. When you enter `+something` in the game, it will
-try to find a command key `+something`. Failing that, it will look at other potential matches.
-Evennia is smart enough to understand that when we type `+something`, `+` is the command key and
-`something` is the command argument. This will, of course, fail if you have a command beginning by
-`+` conflicting with the `CmdConnect` key.
-4. We have altered some class attributes, like `auto_help`. If you want to know what they do and
-why they have changed here, you can check the [documentation on commands](../Components/Commands).
-5. In the command body, we begin by extracting the channel name. Remember that this name should be
-in the command arguments (that is, in `self.args`). Following the same example, if a player enters
-`+something`, `self.args` should contain `"something"`. We use `search_channel` to see if this
-channel exists.
-6. We then check the access level of the channel, to see if the caller can listen to it (not
-necessarily use it to speak, mind you, just listen to others speak, as these are two different locks
-on Evennia).
-7. Finally, we connect the caller if he's not already connected to the channel. We use the
-channel's `connect` method to do this. Pretty straightforward eh?
-
-Now we'll add a command to leave a channel. It's almost the same, turned upside down:
-
-```python
-class CmdDisconnect(Command):
- """
- Disconnect from a channel.
- """
-
- key = "-"
- help_category = "Comms"
- locks = "cmd:not pperm(channel_banned)"
- auto_help = False
-
- def func(self):
- """Implement the command"""
- caller = self.caller
- args = self.args
- if not args:
- self.msg("Which channel do you want to disconnect from?")
- return
-
- channelname = self.args
- channel = search_channel(channelname)
- if not channel:
- return
-
- # If connected to the channel, try to disconnect
- if channel.has_connection(caller):
- if not channel.disconnect(caller):
- self.msg("%s: You are not allowed to disconnect from this channel." % channel.key)
- return
- else:
- self.msg("You stop listening to the %s channel. " % channel.key.lower())
- else:
- self.msg("You are not connected to the %s channel. " % channel.key.lower())
-```
-
-So far, you shouldn't have trouble following what this command does: it's
-pretty much the same as the `CmdConnect` class in logic, though it accomplishes
-the opposite. If you are connected to the channel `public` you could
-disconnect from it using `-public`. Remember, you can use channel aliases too
-(`+pub` and `-pub` will also work, assuming you have the alias `pub` on the
- `public` channel).
-
-It's time to test this code, and to do so, you will need to add these two
-commands. Here is a good time to say it: by default, Evennia connects accounts
-to channels. Some other games (usually with a higher multisession mode) will
-want to connect characters instead of accounts, so that several characters in
-the same account can be connected to various channels. You can definitely add
-these commands either in the `AccountCmdSet` or `CharacterCmdSet`, the caller
-will be different and the command will add or remove accounts of characters.
-If you decide to install these commands on the `CharacterCmdSet`, you might
-have to disconnect your superuser account (account #1) from the channel before
-joining it with your characters, as Evennia tends to subscribe all accounts
-automatically if you don't tell it otherwise.
-
-So here's an example of how to add these commands into your `AccountCmdSet`.
-Edit the file `commands/default_cmdsets.py` to change a few things:
-
-```python
-# In commands/default_cmdsets.py
-from evennia import default_cmds
-from commands.comms import CmdConnect, CmdDisconnect
-
-
-# ... Skip to the AccountCmdSet class ...
-
-class AccountCmdSet(default_cmds.AccountCmdSet):
- """
- This is the cmdset available to the Account at all times. It is
- combined with the `CharacterCmdSet` when the Account puppets a
- Character. It holds game-account-specific commands, channel
- commands, etc.
- """
- key = "DefaultAccount"
-
- def at_cmdset_creation(self):
- """
- Populates the cmdset
- """
- super().at_cmdset_creation()
-
- # Channel commands
- self.add(CmdConnect())
- self.add(CmdDisconnect())
-```
-
-Save, reload your game, and you should be able to use `+public` and `-public`
-now!
-
-## A generic channel command with switches
-
-It's time to dive a little deeper into channel processing. What happens in
-Evennia when a player enters `public Hello everybody!`?
-
-Like exits, channels are a particular command that Evennia automatically
-creates and attaches to individual channels. So when you enter `public
-message` in your game, Evennia calls the `public` command.
-
-> But I didn't add any public command...
-
-Evennia will just create these commands automatically based on the existing
-channels. The base command is the command we'll need to edit.
-
-> Why edit it? It works just fine to talk.
-
-Unfortunately, if we want to add switches to our channel names, we'll have to
-edit this command. It's not too hard, however, we'll just start writing a
-standard command with minor twitches.
-
-### Some additional imports
-
-You'll need to add a line of import in your `commands/comms.py` file. We'll
-see why this import is important when diving in the command itself:
-
-```python
-from evennia.comms.models import ChannelDB
-```
-
-### The class layout
-
-```python
-# In commands/comms.py
-class ChannelCommand(Command):
- """
- {channelkey} channel
-
- {channeldesc}
-
- Usage:
- {lower_channelkey}
- {lower_channelkey}/history [start]
- {lower_channelkey}/me
- {lower_channelkey}/who
-
- Switch:
- history: View 20 previous messages, either from the end or
- from number of messages from the end.
- me: Perform an emote on this channel.
- who: View who is connected to this channel.
-
- Example:
- {lower_channelkey} Hello World!
- {lower_channelkey}/history
- {lower_channelkey}/history 30
- {lower_channelkey}/me grins.
- {lower_channelkey}/who
- """
- # note that channeldesc and lower_channelkey will be filled
- # automatically by ChannelHandler
-
- # this flag is what identifies this cmd as a channel cmd
- # and branches off to the system send-to-channel command
- # (which is customizable by admin)
- is_channel = True
- key = "general"
- help_category = "Channel Names"
- obj = None
- arg_regex = ""
-```
-
-There are some differences here compared to most common commands.
-
-- There is something disconcerting in the class docstring. Some information is
-between curly braces. This is a format-style which is only used for channel
-commands. `{channelkey}` will be replaced by the actual channel key (like
- public). `{channeldesc}` will be replaced by the channel description (like
- "public channel"). And `{lower_channelkey}`.
-- We have set `is_channel` to `True` in the command class variables. You
-shouldn't worry too much about that: it just tells Evennia this is a special
-command just for channels.
-- `key` is a bit misleading because it will be replaced eventually. So we
-could set it to virtually anything.
-- The `obj` class variable is another one we won't detail right now.
-- `arg_regex` is important: the default `arg_regex` in the channel command will
-forbid to use switches (a slash just after the channel name is not allowed).
-That's why we enforce it here, we allow any syntax.
-
-> What will become of this command?
-
-Well, when we'll be through with it, and once we'll add it as the default
-command to handle channels, Evennia will create one per existing channel. For
-instance, the public channel will receive one command of this class, with `key`
-set to `public` and `aliases` set to the channel aliases (like `['pub']`).
-
-> Can I see it work?
-
-Not just yet, there's still a lot of code needed.
-
-Okay we have the command structure but it's rather empty.
-
-### The parse method
-
-The `parse` method is called before `func` in every command. Its job is to
-parse arguments and in our case, we will analyze switches here.
-
-```python
-# ...
- def parse(self):
- """
- Simple parser
- """
- # channel-handler sends channame:msg here.
- channelname, msg = self.args.split(":", 1)
- self.switch = None
- if msg.startswith("/"):
- try:
- switch, msg = msg[1:].split(" ", 1)
- except ValueError:
- switch = msg[1:]
- msg = ""
-
- self.switch = switch.lower().strip()
-
- self.args = (channelname.strip(), msg.strip())
-```
-
-Reading the comments we see that the channel handler will send the command in a
-strange way: a string with the channel name, a colon and the actual message
-entered by the player. So if the player enters "public hello", the command
-`args` will contain `"public:hello"`. You can look at the way the channel name
-and message are parsed, this can be used in a lot of different commands.
-
-Next we check if there's any switch, that is, if the message starts with a
-slash. This would be the case if a player entered `public/me jumps up and
-down`, for instance. If there is a switch, we save it in `self.switch`. We
-alter `self.args` at the end to contain a tuple with two values: the channel
-name, and the message (if a switch was used, notice that the switch will be
- stored in `self.switch`, not in the second element of `self.args`).
-
-### The command func
-
-Finally, let's see the `func` method in the command class. It will have to
-handle switches and also the raw message to send if no switch was used.
-
-
-```python
-# ...
- def func(self):
- """
- Create a new message and send it to channel, using
- the already formatted input.
- """
- channelkey, msg = self.args
- caller = self.caller
- channel = ChannelDB.objects.get_channel(channelkey)
-
- # Check that the channel exists
- if not channel:
- self.msg(_("Channel '%s' not found.") % channelkey)
- return
-
- # Check that the caller is connected
- if not channel.has_connection(caller):
- string = "You are not connected to channel '%s'."
- self.msg(string % channelkey)
- return
-
- # Check that the caller has send access
- if not channel.access(caller, 'send'):
- string = "You are not permitted to send to channel '%s'."
- self.msg(string % channelkey)
- return
-
- # Handle the various switches
- if self.switch == "me":
- if not msg:
- self.msg("What do you want to do on this channel?")
- else:
- msg = "{} {}".format(caller.key, msg)
- channel.msg(msg, online=True)
- elif self.switch:
- self.msg("{}: Invalid switch {}.".format(channel.key, self.switch))
- elif not msg:
- self.msg("Say what?")
- else:
- if caller in channel.mutelist:
- self.msg("You currently have %s muted." % channel)
- return
- channel.msg(msg, senders=self.caller, online=True)
-```
-
-- First of all, we try to get the channel object from the channel name we have
-in the `self.args` tuple. We use `ChannelDB.objects.get_channel` this time
-because we know the channel name isn't an alias (that was part of the deal,
- `channelname` in the `parse` method contains a command key).
-- We check that the channel does exist.
-- We then check that the caller is connected to the channel. Remember, if the
-caller isn't connected, we shouldn't allow him to use this command (that
- includes the switches on channels).
-- We then check that the caller has access to the channel's `send` lock. This
-time, we make sure the caller can send messages to the channel, no matter what
-operation he's trying to perform.
-- Finally we handle switches. We try only one switch: `me`. This switch would
-be used if a player entered `public/me jumps up and down` (to do a channel
- emote).
-- We handle the case where the switch is unknown and where there's no switch
-(the player simply wants to talk on this channel).
-
-The good news: The code is not too complicated by itself. The bad news is that
-this is just an abridged version of the code. If you want to handle all the
-switches mentioned in the command help, you will have more code to write. This
-is left as an exercise.
-
-### End of class
-
-It's almost done, but we need to add a method in this command class that isn't
-often used. I won't detail it's usage too much, just know that Evennia will use
-it and will get angry if you don't add it. So at the end of your class, just
-add:
-
-```python
-# ...
- def get_extra_info(self, caller, **kwargs):
- """
- Let users know that this command is for communicating on a channel.
-
- Args:
- caller (TypedObject): A Character or Account who has entered an ambiguous command.
-
- Returns:
- A string with identifying information to disambiguate the object, conventionally with a
-preceding space.
- """
- return " (channel)"
-```
-
-### Adding this channel command
-
-Contrary to most Evennia commands, we won't add our `ChannelCommand` to a
-`CmdSet`. Instead we need to tell Evennia that it should use the command we
-just created instead of its default channel-command.
-
-In your `server/conf/settings.py` file, add a new setting:
-
-```python
-# Channel options
-CHANNEL_COMMAND_CLASS = "commands.comms.ChannelCommand"
-```
-
-Then you can reload your game. Try to type `public hello` and `public/me jumps
-up and down`. Don't forget to enter `help public` to see if your command has
-truly been added.
-
-## Conclusion and full code
-
-That was some adventure! And there's still things to do! But hopefully, this
-tutorial will have helped you in designing your own channel system. Here are a
-few things to do:
-
-- Add more switches to handle various actions, like changing the description of
-a channel for instance, or listing the connected participants.
-- Remove the default Evennia commands to handle channels.
-- Alter the behavior of the channel system so it better aligns with what you
-want to do.
-
-As a special bonus, you can find a full, working example of a communication
-system similar to the one I've shown you: this is a working example, it
-integrates all switches and does ever some extra checking, but it's also very
-close from the code I've provided here. Notice, however, that this resource is
-external to Evennia and not maintained by anyone but the original author of
-this article.
-
-[Read the full example on Github](https://github.com/vincent-
-lg/avenew/blob/master/commands/comms.py)
\ No newline at end of file
diff --git a/docs/1.0-dev/_sources/Howto/Howto-Overview.md.txt b/docs/1.0-dev/_sources/Howto/Howto-Overview.md.txt
index 3741b67683..6c404f7fba 100644
--- a/docs/1.0-dev/_sources/Howto/Howto-Overview.md.txt
+++ b/docs/1.0-dev/_sources/Howto/Howto-Overview.md.txt
@@ -66,7 +66,6 @@ in mind for your own game, this will give you a good start.
## Howto's
- [Giving Exits a default error](./Default-Exit-Errors)
-- [Customize Channel output](./Customize-channels)
- [Add a command prompt](./Command-Prompt)
- [Don't allow spamming commands](./Command-Cooldown)
- [Commands that take time](./Command-Duration)
diff --git a/docs/1.0-dev/_sources/api/evennia.comms.channelhandler.rst.txt b/docs/1.0-dev/_sources/api/evennia.comms.channelhandler.rst.txt
deleted file mode 100644
index 0d0561cef3..0000000000
--- a/docs/1.0-dev/_sources/api/evennia.comms.channelhandler.rst.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-evennia.comms.channelhandler
-===================================
-
-.. automodule:: evennia.comms.channelhandler
- :members:
- :undoc-members:
- :show-inheritance:
diff --git a/docs/1.0-dev/_sources/api/evennia.comms.rst.txt b/docs/1.0-dev/_sources/api/evennia.comms.rst.txt
index df024da752..0ae919dbd3 100644
--- a/docs/1.0-dev/_sources/api/evennia.comms.rst.txt
+++ b/docs/1.0-dev/_sources/api/evennia.comms.rst.txt
@@ -12,7 +12,6 @@ evennia.comms
:maxdepth: 6
evennia.comms.admin
- evennia.comms.channelhandler
evennia.comms.comms
evennia.comms.managers
evennia.comms.models
diff --git a/docs/1.0-dev/_sources/api/evennia.help.filehelp.rst.txt b/docs/1.0-dev/_sources/api/evennia.help.filehelp.rst.txt
new file mode 100644
index 0000000000..1947254237
--- /dev/null
+++ b/docs/1.0-dev/_sources/api/evennia.help.filehelp.rst.txt
@@ -0,0 +1,7 @@
+evennia.help.filehelp
+============================
+
+.. automodule:: evennia.help.filehelp
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/1.0-dev/_sources/api/evennia.help.rst.txt b/docs/1.0-dev/_sources/api/evennia.help.rst.txt
index cc9b4ac68c..6c737f8390 100644
--- a/docs/1.0-dev/_sources/api/evennia.help.rst.txt
+++ b/docs/1.0-dev/_sources/api/evennia.help.rst.txt
@@ -12,5 +12,7 @@ evennia.help
:maxdepth: 6
evennia.help.admin
+ evennia.help.filehelp
evennia.help.manager
evennia.help.models
+ evennia.help.utils
diff --git a/docs/1.0-dev/_sources/api/evennia.help.utils.rst.txt b/docs/1.0-dev/_sources/api/evennia.help.utils.rst.txt
new file mode 100644
index 0000000000..74425cec91
--- /dev/null
+++ b/docs/1.0-dev/_sources/api/evennia.help.utils.rst.txt
@@ -0,0 +1,7 @@
+evennia.help.utils
+=========================
+
+.. automodule:: evennia.help.utils
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/1.0-dev/_sources/toc.md.txt b/docs/1.0-dev/_sources/toc.md.txt
index fa1a27bec0..2f721e1866 100644
--- a/docs/1.0-dev/_sources/toc.md.txt
+++ b/docs/1.0-dev/_sources/toc.md.txt
@@ -34,6 +34,7 @@
- [Components/Inputfuncs](Components/Inputfuncs)
- [Components/Locks](Components/Locks)
- [Components/MonitorHandler](Components/MonitorHandler)
+- [Components/Msg](Components/Msg)
- [Components/Nicks](Components/Nicks)
- [Components/Objects](Components/Objects)
- [Components/Outputfuncs](Components/Outputfuncs)
@@ -90,7 +91,6 @@
- [Howto/Command Duration](Howto/Command-Duration)
- [Howto/Command Prompt](Howto/Command-Prompt)
- [Howto/Coordinates](Howto/Coordinates)
-- [Howto/Customize channels](Howto/Customize-channels)
- [Howto/Default Exit Errors](Howto/Default-Exit-Errors)
- [Howto/Evennia for Diku Users](Howto/Evennia-for-Diku-Users)
- [Howto/Evennia for MUSH Users](Howto/Evennia-for-MUSH-Users)
diff --git a/docs/1.0-dev/api/evennia-api.html b/docs/1.0-dev/api/evennia-api.html
index af25bcf417..433d00e7f5 100644
--- a/docs/1.0-dev/api/evennia-api.html
+++ b/docs/1.0-dev/api/evennia-api.html
@@ -80,7 +80,6 @@
Called by the Channel just before passing a message into channel_msg.
+This allows for tweak messages per-user and also to abort the
+receive on the receiver-level.
+
+
Parameters
+
+
message (str) – The message sent to the channel.
+
channel (Channel) – The sending channel.
+
senders (list, optional) – Accounts or Objects acting as senders.
+For most normal messages, there is only a single sender. If
+there are no senders, this may be a broadcasting message.
+
**kwargs – These are additional keywords passed into channel_msg.
+If no_prefix=True or emit=True are passed, the channel
+prefix will not be added (**[channelname]: ** by default)
+
+
+
Returns
+
str or None –
+
+
Allows for customizing the message for this recipient.
If returning None (or False) message-receiving is aborted.
+The returning string will be passed into self.channel_msg.
+
+
+
+
+
+
Notes
+
This support posing/emotes by starting channel-send with : or ;.
This performs the actions of receiving a message to an un-muted
+channel.
+
+
Parameters
+
+
message (str) – The message sent to the channel.
+
channel (Channel) – The sending channel.
+
senders (list, optional) – Accounts or Objects acting as senders.
+For most normal messages, there is only a single sender. If
+there are no senders, this may be a broadcasting message or
+similar.
+
**kwargs – These are additional keywords originally passed into
+Channel.msg.
+
+
+
+
Notes
+
Before this, Channel.at_pre_channel_msg will fire, which offers a way
+to customize the message for the receiver on the channel-level.
Called by self.channel_msg after message was received.
+
+
Parameters
+
+
message (str) – The message sent to the channel.
+
channel (Channel) – The sending channel.
+
senders (list, optional) – Accounts or Objects acting as senders.
+For most normal messages, there is only a single sender. If
+there are no senders, this may be a broadcasting message.
+
**kwargs – These are additional keywords passed into channel_msg.
diff --git a/docs/1.0-dev/api/evennia.commands.cmdhandler.html b/docs/1.0-dev/api/evennia.commands.cmdhandler.html
index 859d441399..8b38dd755c 100644
--- a/docs/1.0-dev/api/evennia.commands.cmdhandler.html
+++ b/docs/1.0-dev/api/evennia.commands.cmdhandler.html
@@ -45,15 +45,11 @@ command line. The processing of a command works as follows:
The calling object (caller) is analyzed based on its callertype.
Cmdsets are gathered from different sources:
-- channels: all available channel names are auto-created into a cmdset, to allow
+- object cmdsets: all objects at caller’s location are scanned for non-empty
-
for giving the channel name and have the following immediately
-sent to the channel. The sending is performed by the CMD_CHANNEL
-system command.
+
cmdsets. This includes cmdsets on exits.
-
object cmdsets: all objects at caller’s location are scanned for non-empty
-cmdsets. This includes cmdsets on exits.
caller: the caller is searched for its own currently active cmdset.
account: lastly the cmdsets defined on caller.account are added.
@@ -67,19 +63,10 @@ input string for possible command matches.
cmdset, or fallback to error message. Exit.
If no match was found -> check for CMD_NOMATCH in current cmdset or
fallback to error message. Exit.
-
A single match was found. If this is a channel-command (i.e. the
-ommand name is that of a channel), –> check for CMD_CHANNEL in
-current cmdset or use channelhandler default. Exit.
At this point we have found a normal command. We assign useful variables to it that
will be available to the command coder at run-time.
-
-
-
We have a unique cmdobject, primed for use. Call all hooks:
-
-
-
at_pre_cmd(), cmdobj.parse(), cmdobj.func() and finally at_post_cmd().
-
-
+
We have a unique cmdobject, primed for use. Call all hooks:
+at_pre_cmd(), cmdobj.parse(), cmdobj.func() and finally at_post_cmd().
Return deferred that will fire with the return from cmdobj.func() (unused by default).
class evennia.commands.command.Command(**kwargs)[source]¶
Bases: object
-
Base command
+
(you may see this if a child command had no help text defined)
Usage:
command [args]
@@ -436,7 +436,7 @@ detailing the contents of the table.
-search_index_entry = {'aliases': '', 'category': 'general', 'key': 'command', 'tags': '', 'text': '\n Base command\n\n Usage:\n command [args]\n\n This is the base command class. Inherit from this\n to create new commands.\n\n The cmdhandler makes the following variables available to the\n command methods (so you can always assume them to be there):\n self.caller - the game object calling the command\n self.cmdstring - the command name used to trigger this command (allows\n you to know which alias was used, for example)\n cmd.args - everything supplied to the command following the cmdstring\n (this is usually what is parsed in self.parse())\n cmd.cmdset - the cmdset from which this command was matched (useful only\n seldomly, notably for help-type commands, to create dynamic\n help entries and lists)\n cmd.obj - the object on which this command is defined. If a default command,\n this is usually the same as caller.\n cmd.rawstring - the full raw string input, including any args and no parsing.\n\n The following class properties can/should be defined on your child class:\n\n key - identifier for command (e.g. "look")\n aliases - (optional) list of aliases (e.g. ["l", "loo"])\n locks - lock string (default is "cmd:all()")\n help_category - how to organize this help entry in help system\n (default is "General")\n auto_help - defaults to True. Allows for turning off auto-help generation\n arg_regex - (optional) raw string regex defining how the argument part of\n the command should look in order to match for this command\n (e.g. must it be a space between cmdname and arg?)\n auto_help_display_key - (optional) if given, this replaces the string shown\n in the auto-help listing. This is particularly useful for system-commands\n whose actual key is not really meaningful.\n\n (Note that if auto_help is on, this initial string is also used by the\n system to create the help entry for the command, so it\'s a good idea to\n format it similar to this one). This behavior can be changed by\n overriding the method \'get_help\' of a command: by default, this\n method returns cmd.__doc__ (that is, this very docstring, or\n the docstring of your command). You can, however, extend or\n replace this without disabling auto_help.\n '}¶
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'command', 'tags': '', 'text': '\n ## Base command\n\n (you may see this if a child command had no help text defined)\n\n Usage:\n command [args]\n\n This is the base command class. Inherit from this\n to create new commands.\n\n The cmdhandler makes the following variables available to the\n command methods (so you can always assume them to be there):\n self.caller - the game object calling the command\n self.cmdstring - the command name used to trigger this command (allows\n you to know which alias was used, for example)\n cmd.args - everything supplied to the command following the cmdstring\n (this is usually what is parsed in self.parse())\n cmd.cmdset - the cmdset from which this command was matched (useful only\n seldomly, notably for help-type commands, to create dynamic\n help entries and lists)\n cmd.obj - the object on which this command is defined. If a default command,\n this is usually the same as caller.\n cmd.rawstring - the full raw string input, including any args and no parsing.\n\n The following class properties can/should be defined on your child class:\n\n key - identifier for command (e.g. "look")\n aliases - (optional) list of aliases (e.g. ["l", "loo"])\n locks - lock string (default is "cmd:all()")\n help_category - how to organize this help entry in help system\n (default is "General")\n auto_help - defaults to True. Allows for turning off auto-help generation\n arg_regex - (optional) raw string regex defining how the argument part of\n the command should look in order to match for this command\n (e.g. must it be a space between cmdname and arg?)\n auto_help_display_key - (optional) if given, this replaces the string shown\n in the auto-help listing. This is particularly useful for system-commands\n whose actual key is not really meaningful.\n\n (Note that if auto_help is on, this initial string is also used by the\n system to create the help entry for the command, so it\'s a good idea to\n format it similar to this one). This behavior can be changed by\n overriding the method \'get_help\' of a command: by default, this\n method returns cmd.__doc__ (that is, this very docstring, or\n the docstring of your command). You can, however, extend or\n replace this without disabling auto_help.\n '}¶
diff --git a/docs/1.0-dev/api/evennia.commands.default.admin.html b/docs/1.0-dev/api/evennia.commands.default.admin.html
index febf1ed115..ac7499b0de 100644
--- a/docs/1.0-dev/api/evennia.commands.default.admin.html
+++ b/docs/1.0-dev/api/evennia.commands.default.admin.html
@@ -254,7 +254,7 @@ to accounts respectively.
-search_index_entry = {'aliases': 'pemit remit', 'category': 'admin', 'key': 'emit', 'tags': '', 'text': '\n admin command for emitting message to multiple objects\n\n Usage:\n emit[/switches] [<obj>, <obj>, ... =] <message>\n remit [<obj>, <obj>, ... =] <message>\n pemit [<obj>, <obj>, ... =] <message>\n\n Switches:\n room - limit emits to rooms only (default)\n accounts - limit emits to accounts only\n contents - send to the contents of matched objects too\n\n Emits a message to the selected objects or to\n your immediate surroundings. If the object is a room,\n send to its contents. remit and pemit are just\n limited forms of emit, for sending to rooms and\n to accounts respectively.\n '}¶
+search_index_entry = {'aliases': 'remit pemit', 'category': 'admin', 'key': 'emit', 'tags': '', 'text': '\n admin command for emitting message to multiple objects\n\n Usage:\n emit[/switches] [<obj>, <obj>, ... =] <message>\n remit [<obj>, <obj>, ... =] <message>\n pemit [<obj>, <obj>, ... =] <message>\n\n Switches:\n room - limit emits to rooms only (default)\n accounts - limit emits to accounts only\n contents - send to the contents of matched objects too\n\n Emits a message to the selected objects or to\n your immediate surroundings. If the object is a room,\n send to its contents. remit and pemit are just\n limited forms of emit, for sending to rooms and\n to accounts respectively.\n '}¶
diff --git a/docs/1.0-dev/api/evennia.commands.default.building.html b/docs/1.0-dev/api/evennia.commands.default.building.html
index ce2ae344b2..8b0589f44b 100644
--- a/docs/1.0-dev/api/evennia.commands.default.building.html
+++ b/docs/1.0-dev/api/evennia.commands.default.building.html
@@ -529,7 +529,7 @@ You can specify the /force switch to bypass this confirmation.
@@ -570,7 +570,7 @@ You can specify the /force switch to bypass this confirmation.
-search_index_entry = {'aliases': 'del delete', 'category': 'building', 'key': 'destroy', 'tags': '', 'text': '\n permanently delete objects\n\n Usage:\n destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...]\n\n Switches:\n override - The destroy command will usually avoid accidentally\n destroying account objects. This switch overrides this safety.\n force - destroy without confirmation.\n Examples:\n destroy house, roof, door, 44-78\n destroy 5-10, flower, 45\n destroy/force north\n\n Destroys one or many objects. If dbrefs are used, a range to delete can be\n given, e.g. 4-10. Also the end points will be deleted. This command\n displays a confirmation before destroying, to make sure of your choice.\n You can specify the /force switch to bypass this confirmation.\n '}¶
+search_index_entry = {'aliases': 'delete del', 'category': 'building', 'key': 'destroy', 'tags': '', 'text': '\n permanently delete objects\n\n Usage:\n destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...]\n\n Switches:\n override - The destroy command will usually avoid accidentally\n destroying account objects. This switch overrides this safety.\n force - destroy without confirmation.\n Examples:\n destroy house, roof, door, 44-78\n destroy 5-10, flower, 45\n destroy/force north\n\n Destroys one or many objects. If dbrefs are used, a range to delete can be\n given, e.g. 4-10. Also the end points will be deleted. This command\n displays a confirmation before destroying, to make sure of your choice.\n You can specify the /force switch to bypass this confirmation.\n '}¶
-search_index_entry = {'aliases': 'parent swap update type', 'category': 'building', 'key': 'typeclass', 'tags': '', 'text': "\n set or change an object's typeclass\n\n Usage:\n typeclass[/switch] <object> [= typeclass.path]\n typeclass/prototype <object> = prototype_key\n\n typeclass/list/show [typeclass.path]\n swap - this is a shorthand for using /force/reset flags.\n update - this is a shorthand for using the /force/reload flag.\n\n Switch:\n show, examine - display the current typeclass of object (default) or, if\n given a typeclass path, show the docstring of that typeclass.\n update - *only* re-run at_object_creation on this object\n meaning locks or other properties set later may remain.\n reset - clean out *all* the attributes and properties on the\n object - basically making this a new clean object.\n force - change to the typeclass also if the object\n already has a typeclass of the same name.\n list - show available typeclasses. Only typeclasses in modules actually\n imported or used from somewhere in the code will show up here\n (those typeclasses are still available if you know the path)\n prototype - clean and overwrite the object with the specified\n prototype key - effectively making a whole new object.\n\n Example:\n type button = examples.red_button.RedButton\n type/prototype button=a red button\n\n If the typeclass_path is not given, the current object's typeclass is\n assumed.\n\n View or set an object's typeclass. If setting, the creation hooks of the\n new typeclass will be run on the object. If you have clashing properties on\n the old class, use /reset. By default you are protected from changing to a\n typeclass of the same name as the one you already have - use /force to\n override this protection.\n\n The given typeclass must be identified by its location using python\n dot-notation pointing to the correct module and class. If no typeclass is\n given (or a wrong typeclass is given). Errors in the path or new typeclass\n will lead to the old typeclass being kept. The location of the typeclass\n module is searched from the default typeclass directory, as defined in the\n server settings.\n\n "}¶
+search_index_entry = {'aliases': 'parent type swap update', 'category': 'building', 'key': 'typeclass', 'tags': '', 'text': "\n set or change an object's typeclass\n\n Usage:\n typeclass[/switch] <object> [= typeclass.path]\n typeclass/prototype <object> = prototype_key\n\n typeclass/list/show [typeclass.path]\n swap - this is a shorthand for using /force/reset flags.\n update - this is a shorthand for using the /force/reload flag.\n\n Switch:\n show, examine - display the current typeclass of object (default) or, if\n given a typeclass path, show the docstring of that typeclass.\n update - *only* re-run at_object_creation on this object\n meaning locks or other properties set later may remain.\n reset - clean out *all* the attributes and properties on the\n object - basically making this a new clean object.\n force - change to the typeclass also if the object\n already has a typeclass of the same name.\n list - show available typeclasses. Only typeclasses in modules actually\n imported or used from somewhere in the code will show up here\n (those typeclasses are still available if you know the path)\n prototype - clean and overwrite the object with the specified\n prototype key - effectively making a whole new object.\n\n Example:\n type button = examples.red_button.RedButton\n type/prototype button=a red button\n\n If the typeclass_path is not given, the current object's typeclass is\n assumed.\n\n View or set an object's typeclass. If setting, the creation hooks of the\n new typeclass will be run on the object. If you have clashing properties on\n the old class, use /reset. By default you are protected from changing to a\n typeclass of the same name as the one you already have - use /force to\n override this protection.\n\n The given typeclass must be identified by its location using python\n dot-notation pointing to the correct module and class. If no typeclass is\n given (or a wrong typeclass is given). Errors in the path or new typeclass\n will lead to the old typeclass being kept. The location of the typeclass\n module is searched from the default typeclass directory, as defined in the\n server settings.\n\n "}¶
@@ -1452,7 +1452,7 @@ If object is not specified, the current location is examined.
@@ -1549,7 +1549,7 @@ non-persistent data stored on object
-search_index_entry = {'aliases': 'ex exam', 'category': 'building', 'key': 'examine', 'tags': '', 'text': '\n get detailed information about an object\n\n Usage:\n examine [<object>[/attrname]]\n examine [*<account>[/attrname]]\n\n Switch:\n account - examine an Account (same as adding *)\n object - examine an Object (useful when OOC)\n\n The examine command shows detailed game info about an\n object and optionally a specific attribute on it.\n If object is not specified, the current location is examined.\n\n Append a * before the search string to examine an account.\n\n '}¶
+search_index_entry = {'aliases': 'exam ex', 'category': 'building', 'key': 'examine', 'tags': '', 'text': '\n get detailed information about an object\n\n Usage:\n examine [<object>[/attrname]]\n examine [*<account>[/attrname]]\n\n Switch:\n account - examine an Account (same as adding *)\n object - examine an Object (useful when OOC)\n\n The examine command shows detailed game info about an\n object and optionally a specific attribute on it.\n If object is not specified, the current location is examined.\n\n Append a * before the search string to examine an account.\n\n '}¶
Comm commands are OOC commands and intended to be made available to
-the Account at all times (they go into the AccountCmdSet). So we
-make sure to homogenize self.caller to always be the account object
-for easy handling.
Note that this will not work if the alias has a space in it. So the
+‘warrior guild’ alias must be used with the channel command:
+
+
channel warrior guild = Hello
+
+
Channel-aliases can be removed one at a time, using the ‘/unalias’ switch.
+
Usage: channel/who channelname
+
List the channel’s subscribers. Shows who are currently offline or are
+muting the channel. Subscribers who are ‘muting’ will not see messages sent
+to the channel (use channel/mute to mute a channel).
+
Usage: channel/history channel [= index]
+
This will display the last |c20|n lines of channel history. By supplying an
+index number, you will step that many lines back before viewing those 20 lines.
+
For example:
+
+
channel/history public = 35
+
+
will go back 35 lines and show the previous 20 lines from that point (so
+lines -35 to -55).
+
+
Usage: channel/sub channel [=alias[;alias;…]]
channel/unsub channel
+
+
+
This subscribes you to a channel and optionally assigns personal shortcuts
+for you to use to send to that channel (see aliases). When you unsub, all
+your personal aliases will also be removed.
+
+
Usage: channel/mute channelname
channel/unmute channelname
+
+
+
Muting silences all output from the channel without actually
+un-subscribing. Other channel members will see that you are muted in the /who
+list. Sending a message to the channel will automatically unmute you.
Creates a new channel (or destroys one you control). You will automatically
+join the channel you create and everyone will be kicked and loose all aliases
+to a destroyed channel.
+
+
Usage: channel/lock channelname = lockstring
channel/unlock channelname = lockstring
+
+
+
Note: this is an admin command.
+
A lockstring is on the form locktype:lockfunc(). Channels understand three
+locktypes:
+
+
listen - who may listen or join the channel.
+send - who may send messages to the channel
+control - who controls the channel. This is usually the one creating
+
+
the channel.
+
+
+
Common lockfuncs are all() and perm(). To make a channel everyone can
+listen to but only builders can talk on, use this:
Booting will kick a named subscriber from channel(s) temporarily. The
+‘reason’ will be passed to the booted user. Unless the /quiet switch is
+used, the channel will also be informed of the action. A booted user is
+still able to re-connect, but they’ll have to set up their aliases again.
+
Banning will blacklist a user from (re)joining the provided channels. It
+will then proceed to boot them from those channels if they were connected.
+The ‘reason’ and /quiet works the same as for booting.
+
Example
+
boot mychannel1 = EvilUser : Kicking you to cool down a bit.
+ban mychannel1,mychannel2= EvilUser : Was banned for spamming.
Helper function for searching for a single channel with some error
+handling.
+
+
Parameters
+
+
channelname (str) – Name, alias #dbref or partial name/alias to search
+for.
+
exact (bool, optional) – If an exact or fuzzy-match of the name should be done.
+Note that even for a fuzzy match, an exactly given, unique channel name
+will always be returned.
+
handle_errors (bool) – If true, use self.msg to report errors if
+there are non/multiple matches. If so, the return will always be
+a single match or None.
+
+
+
Returns
+
object, list or None –
+
+
If handle_errors is True, this is either a found Channel
or None. Otherwise it’s a list of zero, one or more channels found.
+
+
+
+
+
+
Notes
+
The ‘listen’ and ‘control’ accesses are checked before returning.
Add a new alias (nick) for the user to use with this channel.
+
+
Parameters
+
+
channel (Channel) – The channel to alias.
+
alias (str) – The personal alias to use for this channel.
+
**kwargs – If given, passed into nicks.add.
+
+
+
+
+
Note
+
We add two nicks - one is a plain alias -> channel.key that
+we need to be able to reference this channel easily. The other
+is a templated nick to easily be able to send messages to the
+channel without needing to give the full channel command. The
+structure of this nick is given by self.channel_msg_pattern
+and self.channel_msg_nick_replacement. By default it maps
+alias <msg> -> channel <channelname> = <msg>, so that you can
+for example just write pub Hello to send a message.
+
The alias created is alias $1 -> channel channel = $1, to allow
+for sending to channel using the main channel command.
Create a new channel. Its name must not previously exist
+(users can alias as needed). Will also connect to the
+new channel.
+
+
Parameters
+
+
name (str) – The new channel name/key.
+
description (str) – This is used in listings.
+
aliases (list) – A list of strings - alternative aliases for the channel
+(not to be confused with per-user aliases; these are available for
+everyone).
Destroy an existing channel. Access should be checked before
+calling this function.
+
+
Parameters
+
+
channel (Channel) – The channel to alias.
+
message (str, optional) – Final message to send onto the channel
+before destroying it. If not given, a default message is
+used. Set to the empty string for no message.
Un-Ban a user from a channel. This will not reconnect them
+to the channel, just allow them to connect again (assuming
+they have the suitable ‘listen’ lock like everyone else).
Show a list of online people is subscribing to a channel. This will check
+the ‘control’ permission of caller to determine if only online users
+should be returned or everyone.
+
+
Parameters
+
channel (Channel) – The channel to operate on.
+
+
Returns
+
list –
+
+
A list of prepared strings, with name + markers for if they are
+search_index_entry = {'aliases': 'chan channels', 'category': 'comms', 'key': 'channel', 'tags': '', 'text': "\n Use and manage in-game channels.\n\n Usage:\n channel channelname <msg>\n channel channel name = <msg>\n channel (show all subscription)\n channel/all (show available channels)\n channel/alias channelname = alias[;alias...]\n channel/unalias alias\n channel/who channelname\n channel/history channelname [= index]\n channel/sub channelname [= alias[;alias...]]\n channel/unsub channelname[,channelname, ...]\n channel/mute channelname[,channelname,...]\n channel/unmute channelname[,channelname,...]\n\n channel/create channelname[;alias;alias[:typeclass]] [= description]\n channel/destroy channelname [= reason]\n channel/desc channelname = description\n channel/lock channelname = lockstring\n channel/unlock channelname = lockstring\n channel/ban channelname (list bans)\n channel/ban[/quiet] channelname[, channelname, ...] = subscribername [: reason]\n channel/unban[/quiet] channelname[, channelname, ...] = subscribername\n channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]\n\n # subtopics\n\n ## sending\n\n Usage: channel channelname msg\n channel channel name = msg (with space in channel name)\n\n This sends a message to the channel. Note that you will rarely use this\n command like this; instead you can use the alias\n\n channelname <msg>\n channelalias <msg>\n\n For example\n\n public Hello World\n pub Hello World\n\n (this shortcut doesn't work for aliases containing spaces)\n\n See channel/alias for help on setting channel aliases.\n\n ## alias and unalias\n\n Usage: channel/alias channel = alias[;alias[;alias...]]\n channel/unalias alias\n channel - this will list your subs and aliases to each channel\n\n Set one or more personal aliases for referencing a channel. For example:\n\n channel/alias warrior's guild = warrior;wguild;warchannel;warrior guild\n\n You can now send to the channel using all of these:\n\n warrior's guild Hello\n warrior Hello\n wguild Hello\n warchannel Hello\n\n Note that this will not work if the alias has a space in it. So the\n 'warrior guild' alias must be used with the `channel` command:\n\n channel warrior guild = Hello\n\n Channel-aliases can be removed one at a time, using the '/unalias' switch.\n\n ## who\n\n Usage: channel/who channelname\n\n List the channel's subscribers. Shows who are currently offline or are\n muting the channel. Subscribers who are 'muting' will not see messages sent\n to the channel (use channel/mute to mute a channel).\n\n ## history\n\n Usage: channel/history channel [= index]\n\n This will display the last |c20|n lines of channel history. By supplying an\n index number, you will step that many lines back before viewing those 20 lines.\n\n For example:\n\n channel/history public = 35\n\n will go back 35 lines and show the previous 20 lines from that point (so\n lines -35 to -55).\n\n ## sub and unsub\n\n Usage: channel/sub channel [=alias[;alias;...]]\n channel/unsub channel\n\n This subscribes you to a channel and optionally assigns personal shortcuts\n for you to use to send to that channel (see aliases). When you unsub, all\n your personal aliases will also be removed.\n\n ## mute and unmute\n\n Usage: channel/mute channelname\n channel/unmute channelname\n\n Muting silences all output from the channel without actually\n un-subscribing. Other channel members will see that you are muted in the /who\n list. Sending a message to the channel will automatically unmute you.\n\n ## create and destroy\n\n Usage: channel/create channelname[;alias;alias[:typeclass]] [= description]\n channel/destroy channelname [= reason]\n\n Creates a new channel (or destroys one you control). You will automatically\n join the channel you create and everyone will be kicked and loose all aliases\n to a destroyed channel.\n\n ## lock and unlock\n\n Usage: channel/lock channelname = lockstring\n channel/unlock channelname = lockstring\n\n Note: this is an admin command.\n\n A lockstring is on the form locktype:lockfunc(). Channels understand three\n locktypes:\n listen - who may listen or join the channel.\n send - who may send messages to the channel\n control - who controls the channel. This is usually the one creating\n the channel.\n\n Common lockfuncs are all() and perm(). To make a channel everyone can\n listen to but only builders can talk on, use this:\n\n listen:all()\n send: perm(Builders)\n\n ## boot and ban\n\n Usage:\n channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason]\n channel/ban channelname[, channelname, ...] = subscribername [: reason]\n channel/unban channelname[, channelname, ...] = subscribername\n channel/unban channelname\n channel/ban channelname (list bans)\n\n Booting will kick a named subscriber from channel(s) temporarily. The\n 'reason' will be passed to the booted user. Unless the /quiet switch is\n used, the channel will also be informed of the action. A booted user is\n still able to re-connect, but they'll have to set up their aliases again.\n\n Banning will blacklist a user from (re)joining the provided channels. It\n will then proceed to boot them from those channels if they were connected.\n The 'reason' and `/quiet` works the same as for booting.\n\n Example:\n boot mychannel1 = EvilUser : Kicking you to cool down a bit.\n ban mychannel1,mychannel2= EvilUser : Was banned for spamming.\n\n "}¶
+
+
+
+
class evennia.commands.default.comms.CmdAddCom(**kwargs)[source]¶
@@ -95,7 +775,7 @@ aliases to an already joined channel.
-search_index_entry = {'aliases': 'aliaschan chanalias', 'category': 'comms', 'key': 'addcom', 'tags': '', 'text': '\n add a channel alias and/or subscribe to a channel\n\n Usage:\n addcom [alias=] <channel>\n\n Joins a given channel. If alias is given, this will allow you to\n refer to the channel by this alias rather than the full channel\n name. Subsequent calls of this command can be used to add multiple\n aliases to an already joined channel.\n '}¶
+search_index_entry = {'aliases': 'aliaschan chanalias', 'category': 'comms', 'key': 'addcom', 'tags': '', 'text': '\n Add a channel alias and/or subscribe to a channel\n\n Usage:\n addcom [alias=] <channel>\n\n Joins a given channel. If alias is given, this will allow you to\n refer to the channel by this alias rather than the full channel\n name. Subsequent calls of this command can be used to add multiple\n aliases to an already joined channel.\n '}¶
@@ -103,7 +783,7 @@ aliases to an already joined channel.
class evennia.commands.default.comms.CmdDelCom(**kwargs)[source]¶
Lists all channels available to you, whether you listen to them or not.
-Use ‘comlist’ to only view your current channel subscriptions.
-Use addcom/delcom to join and leave channels
-search_index_entry = {'aliases': 'clist chanlist comlist all channels channellist', 'category': 'comms', 'key': 'channels', 'tags': '', 'text': "\n list all channels available to you\n\n Usage:\n channels\n clist\n comlist\n\n Lists all channels available to you, whether you listen to them or not.\n Use 'comlist' to only view your current channel subscriptions.\n Use addcom/delcom to join and leave channels\n "}¶
-
-
-
-
class evennia.commands.default.comms.CmdCdestroy(**kwargs)[source]¶
sendername - attach the sender’s name before the message
-quiet - don’t echo the message back to sender
-
-
-
Allows the user to broadcast a message over a channel as long as
-they control it. It does not show the user’s name unless they
-provide the /sendername switch.
-lock_storage = 'cmd: not pperm(channel_banned) and pperm(Player)'¶
-
-
-
-
-search_index_entry = {'aliases': 'cmsg', 'category': 'comms', 'key': 'cemit', 'tags': '', 'text': "\n send an admin message to a channel you control\n\n Usage:\n cemit[/switches] <channel> = <message>\n\n Switches:\n sendername - attach the sender's name before the message\n quiet - don't echo the message back to sender\n\n Allows the user to broadcast a message over a channel as long as\n they control it. It does not show the user's name unless they\n provide the /sendername switch.\n\n "}¶
-
-
-
-
class evennia.commands.default.comms.CmdCWho(**kwargs)[source]¶
last - shows who you last messaged
list - show your last <number> of tells/pages (default)
-
Send a message to target user (if online). If no
-argument is given, you will get a list of your latest messages.
+
Send a message to target user (if online). If no argument is given, you
+will get a list of your latest messages. The equal sign is needed for
+multiple targets or if sending to target with space in the name.
@@ -719,7 +1281,7 @@ argument is given, you will get a list of your latest messages.
-search_index_entry = {'aliases': 'tell', 'category': 'comms', 'key': 'page', 'tags': '', 'text': "\n send a private message to another account\n\n Usage:\n page[/switches] [<account>,<account>,... = <message>]\n tell ''\n page <number>\n\n Switch:\n last - shows who you last messaged\n list - show your last <number> of tells/pages (default)\n\n Send a message to target user (if online). If no\n argument is given, you will get a list of your latest messages.\n "}¶
+search_index_entry = {'aliases': 'tell', 'category': 'comms', 'key': 'page', 'tags': '', 'text': "\n send a private message to another account\n\n Usage:\n page <account> <message>\n page[/switches] [<account>,<account>,... = <message>]\n tell ''\n page <number>\n\n Switches:\n last - shows who you last messaged\n list - show your last <number> of tells/pages (default)\n\n Send a message to target user (if online). If no argument is given, you\n will get a list of your latest messages. The equal sign is needed for\n multiple targets or if sending to target with space in the name.\n\n "}¶
@@ -761,7 +1323,7 @@ The bot will relay everything said in the evennia channel to the
IRC channel and vice versa. The bot will automatically connect at
server start, so this command need only be given once. The
/disconnect switch will permanently delete the bot. To only
-temporarily deactivate it, use the |wservices|n command instead.
+temporarily deactivate it, use the |wservices|n command instead.
Provide an optional bot class path to use a custom bot.
@@ -812,7 +1374,7 @@ Provide an optional bot class path to use a custom bot.
If not given arguments, will return a list of all bots (like
@@ -857,7 +1419,7 @@ messages sent to either channel will be lost.
-search_index_entry = {'aliases': '', 'category': 'comms', 'key': 'ircstatus', 'tags': '', 'text': "\n Check and reboot IRC bot.\n\n Usage:\n ircstatus [#dbref ping||nicklist||reconnect]\n\n If not given arguments, will return a list of all bots (like\n irc2chan/list). The 'ping' argument will ping the IRC network to\n see if the connection is still responsive. The 'nicklist' argument\n (aliases are 'who' and 'users') will return a list of users on the\n remote IRC channel. Finally, 'reconnect' will force the client to\n disconnect and reconnect again. This may be a last resort if the\n client has silently lost connection (this may happen if the remote\n network experience network issues). During the reconnection\n messages sent to either channel will be lost.\n\n "}¶
+search_index_entry = {'aliases': '', 'category': 'comms', 'key': 'ircstatus', 'tags': '', 'text': "\n Check and reboot IRC bot.\n\n Usage:\n ircstatus [#dbref ping | nicklist | reconnect]\n\n If not given arguments, will return a list of all bots (like\n irc2chan/list). The 'ping' argument will ping the IRC network to\n see if the connection is still responsive. The 'nicklist' argument\n (aliases are 'who' and 'users') will return a list of users on the\n remote IRC channel. Finally, 'reconnect' will force the client to\n disconnect and reconnect again. This may be a last resort if the\n client has silently lost connection (this may happen if the remote\n network experience network issues). During the reconnection\n messages sent to either channel will be lost.\n\n "}¶
diff --git a/docs/1.0-dev/api/evennia.commands.default.general.html b/docs/1.0-dev/api/evennia.commands.default.general.html
index 18277eacb3..6b8433334c 100644
--- a/docs/1.0-dev/api/evennia.commands.default.general.html
+++ b/docs/1.0-dev/api/evennia.commands.default.general.html
@@ -205,7 +205,7 @@ for everyone to use, you need build privileges and the alias command.
@@ -237,7 +237,7 @@ for everyone to use, you need build privileges and the alias command.
-search_index_entry = {'aliases': 'nicks nickname', 'category': 'general', 'key': 'nick', 'tags': '', 'text': '\n define a personal alias/nick by defining a string to\n match and replace it with another on the fly\n\n Usage:\n nick[/switches] <string> [= [replacement_string]]\n nick[/switches] <template> = <replacement_template>\n nick/delete <string> or number\n nicks\n\n Switches:\n inputline - replace on the inputline (default)\n object - replace on object-lookup\n account - replace on account-lookup\n list - show all defined aliases (also "nicks" works)\n delete - remove nick by index in /list\n clearall - clear all nicks\n\n Examples:\n nick hi = say Hello, I\'m Sarah!\n nick/object tom = the tall man\n nick build $1 $2 = create/drop $1;$2\n nick tell $1 $2=page $1=$2\n nick tm?$1=page tallman=$1\n nick tm\\=$1=page tallman=$1\n\n A \'nick\' is a personal string replacement. Use $1, $2, ... to catch arguments.\n Put the last $-marker without an ending space to catch all remaining text. You\n can also use unix-glob matching for the left-hand side <string>:\n\n * - matches everything\n ? - matches 0 or 1 single characters\n [abcd] - matches these chars in any order\n [!abcd] - matches everything not among these chars\n \\= - escape literal \'=\' you want in your <string>\n\n Note that no objects are actually renamed or changed by this command - your nicks\n are only available to you. If you want to permanently add keywords to an object\n for everyone to use, you need build privileges and the alias command.\n\n '}¶
+search_index_entry = {'aliases': 'nickname nicks', 'category': 'general', 'key': 'nick', 'tags': '', 'text': '\n define a personal alias/nick by defining a string to\n match and replace it with another on the fly\n\n Usage:\n nick[/switches] <string> [= [replacement_string]]\n nick[/switches] <template> = <replacement_template>\n nick/delete <string> or number\n nicks\n\n Switches:\n inputline - replace on the inputline (default)\n object - replace on object-lookup\n account - replace on account-lookup\n list - show all defined aliases (also "nicks" works)\n delete - remove nick by index in /list\n clearall - clear all nicks\n\n Examples:\n nick hi = say Hello, I\'m Sarah!\n nick/object tom = the tall man\n nick build $1 $2 = create/drop $1;$2\n nick tell $1 $2=page $1=$2\n nick tm?$1=page tallman=$1\n nick tm\\=$1=page tallman=$1\n\n A \'nick\' is a personal string replacement. Use $1, $2, ... to catch arguments.\n Put the last $-marker without an ending space to catch all remaining text. You\n can also use unix-glob matching for the left-hand side <string>:\n\n * - matches everything\n ? - matches 0 or 1 single characters\n [abcd] - matches these chars in any order\n [!abcd] - matches everything not among these chars\n \\= - escape literal \'=\' you want in your <string>\n\n Note that no objects are actually renamed or changed by this command - your nicks\n are only available to you. If you want to permanently add keywords to an object\n for everyone to use, you need build privileges and the alias command.\n\n '}¶
The help command. The basic idea is that help texts for commands
-are best written by those that write the commands - the admins. So
-command-help is all auto-loaded and searched from the current command
-set. The normal, database-tied help system is used for collaborative
-creation of other help topics such as RP help or game-world aides.
+
The help command. The basic idea is that help texts for commands are best
+written by those that write the commands - the developers. So command-help is
+all auto-loaded and searched from the current command set. The normal,
+database-tied help system is used for collaborative creation of other help
+topics such as RP help or game-world aides. Help entries can also be created
+outside the game in modules given by **settings.FILE_HELP_ENTRY_MODULES**.
class evennia.commands.default.help.CmdHelp(**kwargs)[source]¶
Output a category-ordered g for displaying the main help, grouped by
+category.
+
+
Parameters
+
+
cmd_help_dict (dict) – A dict {“category”: [topic, topic, …]} for
+command-based help.
+
db_help_dict (dict) – A dict {“category”: [topic, topic], …]} for
+database-based help.
+
title_lone_category (bool, optional) – If a lone category should
+be titled with the category name or not. While pointless in a
+general index, the title should probably show when explicitly
+listing the category itself.
+
+
+
Returns
+
str – The help index organized into a grid.
+
+
+
The input are the
pre-loaded help files for commands and database-helpfiles
respectively. You can override this method to return a
custom display of the list of commands and topics.
@@ -182,6 +225,13 @@ False: the command shouldn’t appear in the table.
input is a string containing the command or topic to match.
+
The allowed syntax is
+
help<topic>[/<subtopic>[/<subtopic>[/...]]]
+
+
+
The database/command query is always for <topic>, and any subtopics
+is then parsed from there. If a <topic> has spaces in it, it is
+always matched before assuming the space begins a subtopic.
@@ -202,7 +252,7 @@ False: the command shouldn’t appear in the table.
-search_index_entry = {'aliases': '?', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n View help or a list of topics\n\n Usage:\n help <topic or command>\n help list\n help all\n\n This will search for help on commands and other\n topics related to the game.\n '}¶
+search_index_entry = {'aliases': '?', 'category': 'general', 'key': 'help', 'tags': '', 'text': "\n Get help.\n\n Usage:\n help\n help <topic, command or category>\n help <topic>/<subtopic>\n help <topic>/<subtopic>/<subsubtopic> ...\n\n Use the 'help' command alone to see an index of all help topics, organized\n by category.eSome big topics may offer additional sub-topics.\n\n "}¶
@@ -223,14 +273,43 @@ delete - remove help topic.
Examples
-
sethelp throw = This throws something at …
+
sethelp lore = In the beginning was …
sethelp/append pickpocketing,Thievery = This steals …
sethelp/replace pickpocketing, ,attr(is_thief) = This steals …
sethelp/edit thievery
-
This command manipulates the help database. A help entry can be created,
-appended/merged to and deleted. If you don’t assign a category, the
-“General” category will be used. If no lockstring is specified, default
-is to let everyone read the help file.
+
If not assigning a category, the settings.DEFAULT_HELP_CATEGORY category
+will be used. If no lockstring is specified, everyone will be able to read
+the help entry. Sub-topics are embedded in the help text.
+
Note that this cannot modify command-help entries - these are modified
+in-code, outside the game.
+
Subtopics helps to break up a long help entry into sub-sections. Users can
+access subtopics with |whelp topic/subtopic/…|n Subtopics are created and
+stored together with the main topic.
+
To start adding subtopics, add the text ‘# SUBTOPICS’ on a new line at the
+end of your help text. After this you can now add any number of subtopics,
+each starting with ‘## <subtopic-name>’ on a line, followed by the
+help-text of that subtopic.
+Use ‘### <subsub-name>’ to add a sub-subtopic and so on. Max depth is 5. A
+subtopic’s title is case-insensitive and can consist of multiple words -
+the user will be able to enter a partial match to access it.
+
For example:
+
+
Main help text for <topic>
+
+
# SUBTOPICS
+
+
## about
+
+
Text for the ‘<topic>/about’ subtopic’
+
+
### more about-info
+
+
Text for the ‘<topic>/about/more about-info sub-subtopic
@@ -269,7 +348,7 @@ is to let everyone read the help file.
-search_index_entry = {'aliases': '', 'category': 'building', 'key': 'sethelp', 'tags': '', 'text': '\n Edit the help database.\n\n Usage:\n help[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text>]\n\n Switches:\n edit - open a line editor to edit the topic\'s help text.\n replace - overwrite existing help topic.\n append - add text to the end of existing topic with a newline between.\n extend - as append, but don\'t add a newline.\n delete - remove help topic.\n\n Examples:\n sethelp throw = This throws something at ...\n sethelp/append pickpocketing,Thievery = This steals ...\n sethelp/replace pickpocketing, ,attr(is_thief) = This steals ...\n sethelp/edit thievery\n\n This command manipulates the help database. A help entry can be created,\n appended/merged to and deleted. If you don\'t assign a category, the\n "General" category will be used. If no lockstring is specified, default\n is to let everyone read the help file.\n\n '}¶
+search_index_entry = {'aliases': '', 'category': 'building', 'key': 'sethelp', 'tags': '', 'text': "\n Edit the help database.\n\n Usage:\n help[/switches] <topic>[[;alias;alias][,category[,locks]] [= <text>]\n\n Switches:\n edit - open a line editor to edit the topic's help text.\n replace - overwrite existing help topic.\n append - add text to the end of existing topic with a newline between.\n extend - as append, but don't add a newline.\n delete - remove help topic.\n\n Examples:\n sethelp lore = In the beginning was ...\n sethelp/append pickpocketing,Thievery = This steals ...\n sethelp/replace pickpocketing, ,attr(is_thief) = This steals ...\n sethelp/edit thievery\n\n If not assigning a category, the `settings.DEFAULT_HELP_CATEGORY` category\n will be used. If no lockstring is specified, everyone will be able to read\n the help entry. Sub-topics are embedded in the help text.\n\n Note that this cannot modify command-help entries - these are modified\n in-code, outside the game.\n\n # SUBTOPICS\n\n ## Adding subtopics\n\n Subtopics helps to break up a long help entry into sub-sections. Users can\n access subtopics with |whelp topic/subtopic/...|n Subtopics are created and\n stored together with the main topic.\n\n To start adding subtopics, add the text '# SUBTOPICS' on a new line at the\n end of your help text. After this you can now add any number of subtopics,\n each starting with '## <subtopic-name>' on a line, followed by the\n help-text of that subtopic.\n Use '### <subsub-name>' to add a sub-subtopic and so on. Max depth is 5. A\n subtopic's title is case-insensitive and can consist of multiple words -\n the user will be able to enter a partial match to access it.\n\n For example:\n\n | Main help text for <topic>\n |\n | # SUBTOPICS\n |\n | ## about\n |\n | Text for the '<topic>/about' subtopic'\n |\n | ### more about-info\n |\n | Text for the '<topic>/about/more about-info sub-subtopic\n |\n | ## extra\n |\n | Text for the '<topic>/extra' subtopic\n\n "}¶
diff --git a/docs/1.0-dev/api/evennia.commands.default.syscommands.html b/docs/1.0-dev/api/evennia.commands.default.syscommands.html
index d2b7a31198..4439f0edd6 100644
--- a/docs/1.0-dev/api/evennia.commands.default.syscommands.html
+++ b/docs/1.0-dev/api/evennia.commands.default.syscommands.html
@@ -192,125 +192,6 @@ the raw_cmdname is the cmdname unmodified by eventual prefix-st
-
This method is called by the cmdhandler once the command name
-has been identified. It creates a new set of member variables
-that can be later accessed from self.func() (see below)
-
The following variables are available for our use when entering this
-method (from the command definition, and assigned on the fly by the
-cmdhandler):
-
-
self.key - the name of this command (‘look’)
-self.aliases - the aliases of this cmd (‘l’)
-self.permissions - permission string for this command
-self.help_category - overall category of command
-
self.caller - the object calling this command
-self.cmdstring - the actual command name used to call this
-
-
-
(this allows you to know which alias was used,
for example)
-
-
-
-
self.args - the raw input; everything following self.cmdstring.
-self.cmdset - the cmdset from which this command was picked. Not
-
-
often used (useful for commands like ‘help’ or to
-list all available commands etc)
-
-
-
self.obj - the object on which this command was defined. It is often
the same as self.caller.
-
-
-
-
A MUX command has the following possible syntax:
-
-
name[ with several words][/switch[/switch..]] arg1[,arg2,…] [[=|,] arg[,..]]
-
-
The ‘name[ with several words]’ part is already dealt with by the
-cmdhandler at this point, and stored in self.cmdname (we don’t use
-it here). The rest of the command is stored in self.args, which can
-start with the switch indicator /.
-
-
Optional variables to aid in parsing, if set:
-
self.switch_options - (tuple of valid /switches expected by this
command (without the /))
-
-
self.rhs_split - Alternate string delimiter or tuple of strings
to separate left/right hand sides. tuple form
-gives priority split to first string delimiter.
-
-
-
-
-
This parser breaks self.args into its constituents and stores them in the
-following variables:
-
-
self.switches = [list of /switches (without the /)]
-self.raw = This is the raw argument input, including switches
-self.args = This is re-defined to be everything except the switches
-self.lhs = Everything to the left of = (lhs:’left-hand side’). If
-
-
no = is found, this is identical to self.args.
-
-
-
self.rhs: Everything to the right of = (rhs:’right-hand side’).
If no ‘=’ is found, this is None.
-
-
-
self.lhslist - [self.lhs split into a list by comma]
-self.rhslist - [list of self.rhs split into a list by comma]
-self.arglist = [list of space-separated args (stripped, including ‘=’ if it exists)]
-
All args and list members are stripped of excess whitespace around the
-strings, but case is preserved.
-search_index_entry = {'aliases': '', 'category': 'general', 'key': '__send_to_channel_command', 'tags': '', 'text': '\n This is a special command that the cmdhandler calls\n when it detects that the command given matches\n an existing Channel object key (or alias).\n '}¶
-
-
-
-
diff --git a/docs/1.0-dev/api/evennia.commands.default.system.html b/docs/1.0-dev/api/evennia.commands.default.system.html
index eb7c8cfe75..46f7f00385 100644
--- a/docs/1.0-dev/api/evennia.commands.default.system.html
+++ b/docs/1.0-dev/api/evennia.commands.default.system.html
@@ -385,7 +385,7 @@ given, <nr> defaults to 10.
-search_index_entry = {'aliases': 'listobjects stats listobjs db', 'category': 'system', 'key': 'objects', 'tags': '', 'text': '\n statistics on objects in the database\n\n Usage:\n objects [<nr>]\n\n Gives statictics on objects in database as well as\n a list of <nr> latest objects in database. If not\n given, <nr> defaults to 10.\n '}¶
+search_index_entry = {'aliases': 'db stats listobjects listobjs', 'category': 'system', 'key': 'objects', 'tags': '', 'text': '\n statistics on objects in the database\n\n Usage:\n objects [<nr>]\n\n Gives statictics on objects in database as well as\n a list of <nr> latest objects in database. If not\n given, <nr> defaults to 10.\n '}¶
@@ -612,7 +612,7 @@ the released memory will instead be re-used by the program.
@@ -643,7 +643,7 @@ the released memory will instead be re-used by the program.
-search_index_entry = {'aliases': 'serverload serverprocess', 'category': 'system', 'key': 'server', 'tags': '', 'text': "\n show server load and memory statistics\n\n Usage:\n server[/mem]\n\n Switches:\n mem - return only a string of the current memory usage\n flushmem - flush the idmapper cache\n\n This command shows server load statistics and dynamic memory\n usage. It also allows to flush the cache of accessed database\n objects.\n\n Some Important statistics in the table:\n\n |wServer load|n is an average of processor usage. It's usually\n between 0 (no usage) and 1 (100% usage), but may also be\n temporarily higher if your computer has multiple CPU cores.\n\n The |wResident/Virtual memory|n displays the total memory used by\n the server process.\n\n Evennia |wcaches|n all retrieved database entities when they are\n loaded by use of the idmapper functionality. This allows Evennia\n to maintain the same instances of an entity and allowing\n non-persistent storage schemes. The total amount of cached objects\n are displayed plus a breakdown of database object types.\n\n The |wflushmem|n switch allows to flush the object cache. Please\n note that due to how Python's memory management works, releasing\n caches may not show you a lower Residual/Virtual memory footprint,\n the released memory will instead be re-used by the program.\n\n "}¶
+search_index_entry = {'aliases': 'serverprocess serverload', 'category': 'system', 'key': 'server', 'tags': '', 'text': "\n show server load and memory statistics\n\n Usage:\n server[/mem]\n\n Switches:\n mem - return only a string of the current memory usage\n flushmem - flush the idmapper cache\n\n This command shows server load statistics and dynamic memory\n usage. It also allows to flush the cache of accessed database\n objects.\n\n Some Important statistics in the table:\n\n |wServer load|n is an average of processor usage. It's usually\n between 0 (no usage) and 1 (100% usage), but may also be\n temporarily higher if your computer has multiple CPU cores.\n\n The |wResident/Virtual memory|n displays the total memory used by\n the server process.\n\n Evennia |wcaches|n all retrieved database entities when they are\n loaded by use of the idmapper functionality. This allows Evennia\n to maintain the same instances of an entity and allowing\n non-persistent storage schemes. The total amount of cached objects\n are displayed plus a breakdown of database object types.\n\n The |wflushmem|n switch allows to flush the object cache. Please\n note that due to how Python's memory management works, releasing\n caches may not show you a lower Residual/Virtual memory footprint,\n the released memory will instead be re-used by the program.\n\n "}¶
diff --git a/docs/1.0-dev/api/evennia.commands.default.tests.html b/docs/1.0-dev/api/evennia.commands.default.tests.html
index e28e62c23d..42b65f22c9 100644
--- a/docs/1.0-dev/api/evennia.commands.default.tests.html
+++ b/docs/1.0-dev/api/evennia.commands.default.tests.html
@@ -221,6 +221,11 @@ the proper order:
class evennia.commands.default.tests.TestHelp(methodName='runTest')[source]¶
(you may see this if a child command had no help text defined)
Usage:
command [args]
@@ -696,7 +879,7 @@ set in self.parse())
-search_index_entry = {'aliases': '', 'category': 'general', 'key': 'interrupt', 'tags': '', 'text': '\n Base command\n\n Usage:\n command [args]\n\n This is the base command class. Inherit from this\n to create new commands.\n\n The cmdhandler makes the following variables available to the\n command methods (so you can always assume them to be there):\n self.caller - the game object calling the command\n self.cmdstring - the command name used to trigger this command (allows\n you to know which alias was used, for example)\n cmd.args - everything supplied to the command following the cmdstring\n (this is usually what is parsed in self.parse())\n cmd.cmdset - the cmdset from which this command was matched (useful only\n seldomly, notably for help-type commands, to create dynamic\n help entries and lists)\n cmd.obj - the object on which this command is defined. If a default command,\n this is usually the same as caller.\n cmd.rawstring - the full raw string input, including any args and no parsing.\n\n The following class properties can/should be defined on your child class:\n\n key - identifier for command (e.g. "look")\n aliases - (optional) list of aliases (e.g. ["l", "loo"])\n locks - lock string (default is "cmd:all()")\n help_category - how to organize this help entry in help system\n (default is "General")\n auto_help - defaults to True. Allows for turning off auto-help generation\n arg_regex - (optional) raw string regex defining how the argument part of\n the command should look in order to match for this command\n (e.g. must it be a space between cmdname and arg?)\n auto_help_display_key - (optional) if given, this replaces the string shown\n in the auto-help listing. This is particularly useful for system-commands\n whose actual key is not really meaningful.\n\n (Note that if auto_help is on, this initial string is also used by the\n system to create the help entry for the command, so it\'s a good idea to\n format it similar to this one). This behavior can be changed by\n overriding the method \'get_help\' of a command: by default, this\n method returns cmd.__doc__ (that is, this very docstring, or\n the docstring of your command). You can, however, extend or\n replace this without disabling auto_help.\n '}¶
+search_index_entry = {'aliases': '', 'category': 'general', 'key': 'interrupt', 'tags': '', 'text': '\n ## Base command\n\n (you may see this if a child command had no help text defined)\n\n Usage:\n command [args]\n\n This is the base command class. Inherit from this\n to create new commands.\n\n The cmdhandler makes the following variables available to the\n command methods (so you can always assume them to be there):\n self.caller - the game object calling the command\n self.cmdstring - the command name used to trigger this command (allows\n you to know which alias was used, for example)\n cmd.args - everything supplied to the command following the cmdstring\n (this is usually what is parsed in self.parse())\n cmd.cmdset - the cmdset from which this command was matched (useful only\n seldomly, notably for help-type commands, to create dynamic\n help entries and lists)\n cmd.obj - the object on which this command is defined. If a default command,\n this is usually the same as caller.\n cmd.rawstring - the full raw string input, including any args and no parsing.\n\n The following class properties can/should be defined on your child class:\n\n key - identifier for command (e.g. "look")\n aliases - (optional) list of aliases (e.g. ["l", "loo"])\n locks - lock string (default is "cmd:all()")\n help_category - how to organize this help entry in help system\n (default is "General")\n auto_help - defaults to True. Allows for turning off auto-help generation\n arg_regex - (optional) raw string regex defining how the argument part of\n the command should look in order to match for this command\n (e.g. must it be a space between cmdname and arg?)\n auto_help_display_key - (optional) if given, this replaces the string shown\n in the auto-help listing. This is particularly useful for system-commands\n whose actual key is not really meaningful.\n\n (Note that if auto_help is on, this initial string is also used by the\n system to create the help entry for the command, so it\'s a good idea to\n format it similar to this one). This behavior can be changed by\n overriding the method \'get_help\' of a command: by default, this\n method returns cmd.__doc__ (that is, this very docstring, or\n the docstring of your command). You can, however, extend or\n replace this without disabling auto_help.\n '}¶
@@ -737,11 +920,6 @@ set in self.parse())
test_multimatch()[source]¶
@@ -94,7 +94,7 @@ there is no object yet before the account has logged in)
-search_index_entry = {'aliases': 'con conn co', 'category': 'general', 'key': 'connect', 'tags': '', 'text': '\n connect to the game\n\n Usage (at login screen):\n connect accountname password\n connect "account name" "pass word"\n\n Use the create command to first create an account before logging in.\n\n If you have spaces in your name, enclose it in double quotes.\n '}¶
+search_index_entry = {'aliases': 'conn co con', 'category': 'general', 'key': 'connect', 'tags': '', 'text': '\n connect to the game\n\n Usage (at login screen):\n connect accountname password\n connect "account name" "pass word"\n\n Use the create command to first create an account before logging in.\n\n If you have spaces in your name, enclose it in double quotes.\n '}¶
@@ -173,7 +173,7 @@ version is a bit more complicated.
@@ -199,7 +199,7 @@ version is a bit more complicated.
-search_index_entry = {'aliases': 'q qu', 'category': 'general', 'key': 'quit', 'tags': '', 'text': '\n quit when in unlogged-in state\n\n Usage:\n quit\n\n We maintain a different version of the quit command\n here for unconnected accounts for the sake of simplicity. The logged in\n version is a bit more complicated.\n '}¶
+search_index_entry = {'aliases': 'qu q', 'category': 'general', 'key': 'quit', 'tags': '', 'text': '\n quit when in unlogged-in state\n\n Usage:\n quit\n\n We maintain a different version of the quit command\n here for unconnected accounts for the sake of simplicity. The logged in\n version is a bit more complicated.\n '}¶
@@ -272,7 +272,7 @@ for simplicity. It shows a pane of info.
@@ -298,7 +298,7 @@ for simplicity. It shows a pane of info.
-search_index_entry = {'aliases': '? h', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n get help when in unconnected-in state\n\n Usage:\n help\n\n This is an unconnected version of the help command,\n for simplicity. It shows a pane of info.\n '}¶
+search_index_entry = {'aliases': 'h ?', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n get help when in unconnected-in state\n\n Usage:\n help\n\n This is an unconnected version of the help command,\n for simplicity. It shows a pane of info.\n '}¶
The channel handler, accessed from this module as CHANNEL_HANDLER is a
-singleton that handles the stored set of channels and how they are
-represented against the cmdhandler.
-
If there is a channel named ‘newbie’, we want to be able to just write
-
-
newbie Hello!
-
-
For this to work, ‘newbie’, the name of the channel, must be
-identified by the cmdhandler as a command name. The channelhandler
-stores all channels as custom ‘commands’ that the cmdhandler can
-import and look through.
-
> Warning - channel names take precedence over command names, so make
-sure to not pick clashing channel names.
-
Unless deleting a channel you normally don’t need to bother about the
-channelhandler at all - the create_channel method handles the update.
-
To delete a channel cleanly, delete the channel object, then call
-update() on the channelhandler. Or use Channel.objects.delete() which
-does this for you.
{lower_channelkey} <message>
-{lower_channelkey}/history [start]
-{lower_channelkey} off - mutes the channel
-{lower_channelkey} on - unmutes the channel
-
-
Switch:
-
history: View 20 previous messages, either from the end or
-search_index_entry = {'aliases': '', 'category': 'channel names', 'key': 'general', 'tags': '', 'text': '\n {channelkey} channel\n\n {channeldesc}\n\n Usage:\n {lower_channelkey} <message>\n {lower_channelkey}/history [start]\n {lower_channelkey} off - mutes the channel\n {lower_channelkey} on - unmutes the channel\n\n Switch:\n history: View 20 previous messages, either from the end or\n from <start> number of messages from the end.\n\n Example:\n {lower_channelkey} Hello World!\n {lower_channelkey}/history\n {lower_channelkey}/history 30\n\n '}¶
The ChannelHandler manages all active in-game channels and
-dynamically creates channel commands for users so that they can
-just give the channel’s key or alias to write to it. Whenever a
-new channel is created in the database, the update() method on
-this handler must be called to sync it with the database (this is
-done automatically if creating the channel with
-evennia.create_channel())
Add an individual channel to the handler. This is called
-whenever a new channel is created.
-
-
Parameters
-
channel (Channel) – The channel to add.
-
-
-
Notes
-
To remove a channel, simply delete the channel object and
-run self.update on the handler. This should usually be
-handled automatically by one of the deletion methos of
-the Channel itself.
Add an individual channel to the handler. This is called
-whenever a new channel is created.
-
-
Parameters
-
channel (Channel) – The channel to add.
-
-
-
Notes
-
To remove a channel, simply delete the channel object and
-run self.update on the handler. This should usually be
-handled automatically by one of the deletion methos of
-the Channel itself.
-
-
-
\ No newline at end of file
diff --git a/docs/1.0-dev/api/evennia.comms.comms.html b/docs/1.0-dev/api/evennia.comms.comms.html
index 49fc9112bf..5cf7812369 100644
--- a/docs/1.0-dev/api/evennia.comms.comms.html
+++ b/docs/1.0-dev/api/evennia.comms.comms.html
@@ -46,11 +46,50 @@
This is the base class for all Channel Comms. Inherit from this to
create different types of communication channels.
+
+
Class-level variables:
+
send_to_online_only (bool, default True) - if set, will only try to
+send to subscribers that are actually active. This is a useful optimization.
+
log_file (str, default “channel_{channelname}.log”). This is the
+log file to which the channel history will be saved. The {channelname} tag
+will be replaced by the key of the Channel. If an Attribute ‘log_file’
+is set, this will be used instead. If this is None and no Attribute is found,
+no history will be saved.
+
channel_prefix_string (str, default “[{channelname} ]”) - this is used
+as a simple template to get the channel prefix with .channel_prefix().
Send the given message to all accounts connected to channel. Note that
-no permission-checking is done here; it is assumed to have been
-done before calling this method. The optional keywords are not used if
-persistent is False.
-
-
Parameters
-
-
msgobj (Msg, TempMsg or str) – If a Msg/TempMsg, the remaining
-keywords will be ignored (since the Msg/TempMsg object already
-has all the data). If a string, this will either be sent as-is
-(if persistent=False) or it will be used together with header
-and senders keywords to create a Msg instance on the fly.
-
header (str, optional) – A header for building the message.
-
senders (Object, Account or list, optional) – Optional if persistent=False, used
-to build senders for the message.
-
sender_strings (list, optional) – Name strings of senders. Used for external
-connections where the sender is not an account or object.
-When this is defined, external will be assumed. The list will be
-filtered so each sender-string only occurs once.
-
keep_log (bool or None, optional) – This allows to temporarily change the logging status of
-this channel message. If None, the Channel’s keep_log Attribute will
-be used. If True or False, that logging status will be used for this
-message only (note that for unlogged channels, a True value here will
-create a new log file only for this message).
-
online (bool, optional) – online. Otherwise, messages all accounts connected. This can
-make things faster, but may not trigger listeners on accounts
-that are offline.
-
emit (bool, optional) – not to be directly associated with a name.
-
external (bool, optional) – Treat this message as being
-agnostic of its sender.
Add a personal user-alias for this channel to a given subscriber.
Parameters
-
senders (list) – Sender object names.
-
**kwargs (dict) – Arbitrary, optional arguments for users
-overriding the call (unused by default).
+
user (Object or Account) – The one to alias this channel.
+
alias (str) – The desired alias.
-
Returns
-
formatted_list (str) – The list of names formatted appropriately.
+
+
+
Note
+
This is tightly coupled to the default channel command. If you
+change that, you need to change this as well.
+
We add two nicks - one is a plain alias -> channel.key that
+users need to be able to reference this channel easily. The other
+is a templated nick to easily be able to send messages to the
+channel without needing to give the full channel command. The
+structure of this nick is given by self.channel_msg_nick_pattern
+and self.channel_msg_nick_replacement. By default it maps
+alias <msg> -> channel <channelname> = <msg>, so that you can
+for example just write pub Hello to send a message.
+
The alias created is alias $1 -> channel channel = $1, to allow
+for sending to channel using the main channel command.
user (Object or Account) – The user to remove an alias from.
+
alias (str) – The alias to remove.
+
**kwargs – Unused by default. Can be used to pass extra variables
+into a custom implementation.
+
Notes
-
This function exists separately so that external sources
-can use it to format source names in the same manner as
-normal object/account names.
+
The channel-alias actually consists of two aliases - one
+channel-based one for searching channels with the alias and one
+inputline one for doing the ‘channelalias msg’ - call.
+
This is a classmethod because it doesn’t actually operate on the
+channel instance.
+
It sits on the channel because the nick structure for this is
+pretty complex and needs to be located in a central place (rather
+on, say, the channel command).
Called before the starting of sending the message to a receiver. This
+is called before any hooks on the receiver itself. If this returns
+None/False, the sending will be aborted.
Parameters
-
msgobj (Msg or TempMsg) – The message to analyze for a pose.
-
sender_string (str) – The name of the sender/poser.
-
**kwargs (dict) – Arbitrary, optional arguments for users
-overriding the call (unused by default).
+
message (str) – The message to send.
+
**kwargs (any) – Keywords passed on from .msg. This includes
+senders.
Returns
-
string (str) –
+
str, False or None –
-
A message that combines the sender_string
component with msg in different ways depending on if a
-pose was performed or not (this must be analyzed by the
-hook).
Hook method. Used for formatting external messages. This is
-needed as a separate operation because the senders of external
-messages may not be in-game objects/accounts, and so cannot
-have things like custom user preferences.
emit (bool, optional) – A sender-agnostic message or not.
-
**kwargs (dict) – Arbitrary, optional arguments for users
-overriding the call (unused by default).
+
message (str) – The message to send.
+
senders (Object, Account or list, optional) – If not given, there is
+no way to associate one or more senders with the message (like
+a broadcast message or similar).
+
bypass_mute (bool, optional) – If set, always send, regardless of
+individual mute-state of subscriber. This can be used for
+global announcements or warnings/alerts.
+
**kwargs (any) – This will be passed on to all hooks. Use no_prefix
+to exclude the channel prefix.
-
Returns
-
transformed (str) – A formatted string.
-
+
Notes
+
The call hook calling sequence is:
+
+
msg = channel.at_pre_msg(message, **kwargs) (aborts for all if return None)
+
msg = receiver.at_pre_channel_msg(msg, channel, **kwargs) (aborts for receiver if return None)
Hook method. Runs before a message is sent to the channel and
-should return the message object, after any transformations.
-If the message is to be discarded, return a false value.
diff --git a/docs/1.0-dev/api/evennia.comms.html b/docs/1.0-dev/api/evennia.comms.html
index da95432f7d..313b4f199e 100644
--- a/docs/1.0-dev/api/evennia.comms.html
+++ b/docs/1.0-dev/api/evennia.comms.html
@@ -45,7 +45,6 @@ as code related to external communication like IRC or RSS.
diff --git a/docs/1.0-dev/api/evennia.comms.managers.html b/docs/1.0-dev/api/evennia.comms.managers.html
index 82b8ac05d0..2dc457ea7e 100644
--- a/docs/1.0-dev/api/evennia.comms.managers.html
+++ b/docs/1.0-dev/api/evennia.comms.managers.html
@@ -135,19 +135,15 @@ of which may be Channels).
diff --git a/docs/1.0-dev/api/evennia.contrib.dice.html b/docs/1.0-dev/api/evennia.contrib.dice.html
index c5f9272a8c..e0c088a700 100644
--- a/docs/1.0-dev/api/evennia.contrib.dice.html
+++ b/docs/1.0-dev/api/evennia.contrib.dice.html
@@ -149,7 +149,7 @@ everyone but the person rolling.
@@ -175,7 +175,7 @@ everyone but the person rolling.
-search_index_entry = {'aliases': 'roll @dice', 'category': 'general', 'key': 'dice', 'tags': '', 'text': "\n roll dice\n\n Usage:\n dice[/switch] <nr>d<sides> [modifier] [success condition]\n\n Switch:\n hidden - tell the room the roll is being done, but don't show the result\n secret - don't inform the room about neither roll nor result\n\n Examples:\n dice 3d6 + 4\n dice 1d100 - 2 < 50\n\n This will roll the given number of dice with given sides and modifiers.\n So e.g. 2d6 + 3 means to 'roll a 6-sided die 2 times and add the result,\n then add 3 to the total'.\n Accepted modifiers are +, -, * and /.\n A success condition is given as normal Python conditionals\n (<,>,<=,>=,==,!=). So e.g. 2d6 + 3 > 10 means that the roll will succeed\n only if the final result is above 8. If a success condition is given, the\n outcome (pass/fail) will be echoed along with how much it succeeded/failed\n with. The hidden/secret switches will hide all or parts of the roll from\n everyone but the person rolling.\n "}¶
+search_index_entry = {'aliases': '@dice roll', 'category': 'general', 'key': 'dice', 'tags': '', 'text': "\n roll dice\n\n Usage:\n dice[/switch] <nr>d<sides> [modifier] [success condition]\n\n Switch:\n hidden - tell the room the roll is being done, but don't show the result\n secret - don't inform the room about neither roll nor result\n\n Examples:\n dice 3d6 + 4\n dice 1d100 - 2 < 50\n\n This will roll the given number of dice with given sides and modifiers.\n So e.g. 2d6 + 3 means to 'roll a 6-sided die 2 times and add the result,\n then add 3 to the total'.\n Accepted modifiers are +, -, * and /.\n A success condition is given as normal Python conditionals\n (<,>,<=,>=,==,!=). So e.g. 2d6 + 3 > 10 means that the roll will succeed\n only if the final result is above 8. If a success condition is given, the\n outcome (pass/fail) will be echoed along with how much it succeeded/failed\n with. The hidden/secret switches will hide all or parts of the roll from\n everyone but the person rolling.\n "}¶
diff --git a/docs/1.0-dev/api/evennia.contrib.email_login.html b/docs/1.0-dev/api/evennia.contrib.email_login.html
index ed925528f1..e4df3dce86 100644
--- a/docs/1.0-dev/api/evennia.contrib.email_login.html
+++ b/docs/1.0-dev/api/evennia.contrib.email_login.html
@@ -74,7 +74,7 @@ the module given by settings.CONNECTION_SCREEN_MODULE.
@@ -104,7 +104,7 @@ there is no object yet before the account has logged in)
-search_index_entry = {'aliases': 'con conn co', 'category': 'general', 'key': 'connect', 'tags': '', 'text': '\n Connect to the game.\n\n Usage (at login screen):\n connect <email> <password>\n\n Use the create command to first create an account before logging in.\n '}¶
+search_index_entry = {'aliases': 'conn co con', 'category': 'general', 'key': 'connect', 'tags': '', 'text': '\n Connect to the game.\n\n Usage (at login screen):\n connect <email> <password>\n\n Use the create command to first create an account before logging in.\n '}¶
@@ -181,7 +181,7 @@ version is a bit more complicated.
@@ -207,7 +207,7 @@ version is a bit more complicated.
-search_index_entry = {'aliases': 'q qu', 'category': 'general', 'key': 'quit', 'tags': '', 'text': '\n We maintain a different version of the `quit` command\n here for unconnected accounts for the sake of simplicity. The logged in\n version is a bit more complicated.\n '}¶
+search_index_entry = {'aliases': 'qu q', 'category': 'general', 'key': 'quit', 'tags': '', 'text': '\n We maintain a different version of the `quit` command\n here for unconnected accounts for the sake of simplicity. The logged in\n version is a bit more complicated.\n '}¶
@@ -270,7 +270,7 @@ for simplicity. It shows a pane of info.
@@ -296,7 +296,7 @@ for simplicity. It shows a pane of info.
-search_index_entry = {'aliases': '? h', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n This is an unconnected version of the help command,\n for simplicity. It shows a pane of info.\n '}¶
+search_index_entry = {'aliases': 'h ?', 'category': 'general', 'key': 'help', 'tags': '', 'text': '\n This is an unconnected version of the help command,\n for simplicity. It shows a pane of info.\n '}¶
diff --git a/docs/1.0-dev/api/evennia.contrib.evscaperoom.commands.html b/docs/1.0-dev/api/evennia.contrib.evscaperoom.commands.html
index 948963a427..5218cd9358 100644
--- a/docs/1.0-dev/api/evennia.contrib.evscaperoom.commands.html
+++ b/docs/1.0-dev/api/evennia.contrib.evscaperoom.commands.html
@@ -147,7 +147,7 @@ the operation will be general or on the room.
-search_index_entry = {'aliases': 'abort chicken out quit q', 'category': 'evscaperoom', 'key': 'give up', 'tags': '', 'text': '\n Give up\n\n Usage:\n give up\n\n Abandons your attempts at escaping and of ever winning the pie-eating contest.\n\n '}¶
+search_index_entry = {'aliases': 'chicken out quit q abort', 'category': 'evscaperoom', 'key': 'give up', 'tags': '', 'text': '\n Give up\n\n Usage:\n give up\n\n Abandons your attempts at escaping and of ever winning the pie-eating contest.\n\n '}¶
-search_index_entry = {'aliases': 'e examine unfocus ex', 'category': 'evscaperoom', 'key': 'focus', 'tags': '', 'text': '\n Focus your attention on a target.\n\n Usage:\n focus <obj>\n\n Once focusing on an object, use look to get more information about how it\n looks and what actions is available.\n\n '}¶
+search_index_entry = {'aliases': 'examine unfocus e ex', 'category': 'evscaperoom', 'key': 'focus', 'tags': '', 'text': '\n Focus your attention on a target.\n\n Usage:\n focus <obj>\n\n Once focusing on an object, use look to get more information about how it\n looks and what actions is available.\n\n '}¶
diff --git a/docs/1.0-dev/api/evennia.contrib.rpsystem.html b/docs/1.0-dev/api/evennia.contrib.rpsystem.html
index e00bed964f..7bda19d85c 100644
--- a/docs/1.0-dev/api/evennia.contrib.rpsystem.html
+++ b/docs/1.0-dev/api/evennia.contrib.rpsystem.html
@@ -801,7 +801,7 @@ Using the command without arguments will list all current recogs.
@@ -828,7 +828,7 @@ Using the command without arguments will list all current recogs.
-search_index_entry = {'aliases': 'forget recognize', 'category': 'general', 'key': 'recog', 'tags': '', 'text': '\n Recognize another person in the same room.\n\n Usage:\n recog\n recog sdesc as alias\n forget alias\n\n Example:\n recog tall man as Griatch\n forget griatch\n\n This will assign a personal alias for a person, or forget said alias.\n Using the command without arguments will list all current recogs.\n\n '}¶
+search_index_entry = {'aliases': 'recognize forget', 'category': 'general', 'key': 'recog', 'tags': '', 'text': '\n Recognize another person in the same room.\n\n Usage:\n recog\n recog sdesc as alias\n forget alias\n\n Example:\n recog tall man as Griatch\n forget griatch\n\n This will assign a personal alias for a person, or forget said alias.\n Using the command without arguments will list all current recogs.\n\n '}¶
diff --git a/docs/1.0-dev/api/evennia.contrib.tutorial_examples.red_button.html b/docs/1.0-dev/api/evennia.contrib.tutorial_examples.red_button.html
index 93d924f702..7091707601 100644
--- a/docs/1.0-dev/api/evennia.contrib.tutorial_examples.red_button.html
+++ b/docs/1.0-dev/api/evennia.contrib.tutorial_examples.red_button.html
@@ -79,7 +79,7 @@ such as when closing the lid and un-blinding a character.
-search_index_entry = {'aliases': 'break lid smash lid smash', 'category': 'general', 'key': 'smash glass', 'tags': '', 'text': '\n Smash the protective glass.\n\n Usage:\n smash glass\n\n Try to smash the glass of the button.\n\n '}¶
+search_index_entry = {'aliases': 'smash smash lid break lid', 'category': 'general', 'key': 'smash glass', 'tags': '', 'text': '\n Smash the protective glass.\n\n Usage:\n smash glass\n\n Try to smash the glass of the button.\n\n '}¶
-search_index_entry = {'aliases': 'ex listen get examine l feel', 'category': 'general', 'key': 'look', 'tags': '', 'text': "\n Looking around in darkness\n\n Usage:\n look <obj>\n\n ... not that there's much to see in the dark.\n\n "}¶
+search_index_entry = {'aliases': 'l listen examine get ex feel', 'category': 'general', 'key': 'look', 'tags': '', 'text': "\n Looking around in darkness\n\n Usage:\n look <obj>\n\n ... not that there's much to see in the dark.\n\n "}¶
diff --git a/docs/1.0-dev/api/evennia.contrib.tutorial_world.objects.html b/docs/1.0-dev/api/evennia.contrib.tutorial_world.objects.html
index ce25e9df60..6d8a556990 100644
--- a/docs/1.0-dev/api/evennia.contrib.tutorial_world.objects.html
+++ b/docs/1.0-dev/api/evennia.contrib.tutorial_world.objects.html
@@ -492,7 +492,7 @@ shift green root up/down
@@ -741,7 +741,7 @@ parry - forgoes your attack but will make you harder to hit on next
-search_index_entry = {'aliases': 'kill bash thrust pierce parry slash chop hit defend fight stab', 'category': 'tutorialworld', 'key': 'attack', 'tags': '', 'text': '\n Attack the enemy. Commands:\n\n stab <enemy>\n slash <enemy>\n parry\n\n stab - (thrust) makes a lot of damage but is harder to hit with.\n slash - is easier to land, but does not make as much damage.\n parry - forgoes your attack but will make you harder to hit on next\n enemy attack.\n\n '}¶
+search_index_entry = {'aliases': 'thrust chop defend slash bash fight parry kill stab hit pierce', 'category': 'tutorialworld', 'key': 'attack', 'tags': '', 'text': '\n Attack the enemy. Commands:\n\n stab <enemy>\n slash <enemy>\n parry\n\n stab - (thrust) makes a lot of damage but is harder to hit with.\n slash - is easier to land, but does not make as much damage.\n parry - forgoes your attack but will make you harder to hit on next\n enemy attack.\n\n '}¶
diff --git a/docs/1.0-dev/api/evennia.contrib.tutorial_world.rooms.html b/docs/1.0-dev/api/evennia.contrib.tutorial_world.rooms.html
index 48a1ce50bd..61fe9fa96f 100644
--- a/docs/1.0-dev/api/evennia.contrib.tutorial_world.rooms.html
+++ b/docs/1.0-dev/api/evennia.contrib.tutorial_world.rooms.html
@@ -713,7 +713,7 @@ if they fall off the bridge.
@@ -893,7 +893,7 @@ random chance of eventually finding a light source.
-search_index_entry = {'aliases': 'feel around fiddle l search feel', 'category': 'tutorialworld', 'key': 'look', 'tags': '', 'text': '\n Look around in darkness\n\n Usage:\n look\n\n Look around in the darkness, trying\n to find something.\n '}¶
+search_index_entry = {'aliases': 'l feel around search feel fiddle', 'category': 'tutorialworld', 'key': 'look', 'tags': '', 'text': '\n Look around in darkness\n\n Usage:\n look\n\n Look around in the darkness, trying\n to find something.\n '}¶
The filehelp-system allows for defining help files outside of the game. These
+will be treated as non-command help entries and displayed in the same way as
+help entries created using the sethelp default command. After changing an
+entry on-disk you need to reload the server to have the change show in-game.
+
An filehelp file is a regular python modules with dicts representing each help
+entry. If a list HELP_ENTRY_DICTS is found in the module, this should be a list of
+dicts. Otherwise all top-level dicts in the module will be assumed to be a
+help-entry dict.
where the **category** is optional and the **text** should be formatted on the
+same form as other help entry-texts and contain **# subtopics** as normal.
+
New help-entry modules are added to the system by providing the python-path to
+the module to settings.FILE_HELP_ENTRY_MODULES. Note that if same-key entries are
+added, entries in latter modules will override that of earlier ones. Use
+**settings.DEFAULT_HELP_CATEGORY** to customize what category is used if
+not set explicitly.
+
An example of the contents of a module:
+
help_entry1={
+ "key":"The Gods",# case-insensitive, can be searched by 'gods' as well
+ "aliases":['pantheon','religion']
+ "category":"Lore",
+ "text":'''
+ The gods formed the world ...
+
+ # Subtopics
+
+ ## Pantheon
+
+ ...
+
+ ### God of love
+
+ ...
+
+ ### God of war
+
+ ...
+
+ '''
+}
+
+
+HELP_ENTRY_DICTS=[
+ help_entry1,
+ ...
+]
+
Represents a help entry read from file. This mimics the api of the
+database-bound HelpEntry so that they can be used interchangeably in the
+help command.
Lunr-powered fast index search and suggestion wrapper. See https://lunrjs.com/.
+
+
Parameters
+
+
query (str) – The query to search for.
+
candidate_entries (list) – This is the body of possible entities to search. Each
+must have a property .search_index_entry that returns a dict with all
+keys in the fields arg.
+
suggestion_maxnum (int) – How many matches to allow at most in a multi-match.
+
fields (list, optional) – A list of Lunr field mappings
+**{“field_name”: str, “boost”: int}**. See the Lunr documentation
+for more details. The field name must exist in the dicts returned
+by .search_index_entry of the candidates. If not given, a default setup
+is used, prefering keys > aliases > category > tags.
+
+
+
Returns
+
tuple –
+
+
A tuple (matches, suggestions), each a list, where the suggestion_maxnum limits
Parse a command docstring for special sub-category blocks:
+
+
Parameters
+
entry (str) – A help entry to parse
+
+
Returns
+
dict –
+
+
The dict is a mapping that splits the entry into subcategories. This
will always hold a key None for the main help entry and
+zero or more keys holding the subcategories. Each is itself
+a dict with a key None for the main text of that subcategory
+followed by any sub-sub-categories down to a max-depth of 5.
+
+
+
+
+
+
Example:
+
'''
+Main topic text
+
+# SUBTOPICS
+
+## foo
+
+A subcategory of the main entry, accessible as **help topic foo**
+(or using /, like **help topic/foo**)
+
+## bar
+
+Another subcategory, accessed as **help topic bar**
+(or **help topic/bar**)
+
+### moo
+
+A subcategory of bar, accessed as **help bar moo**
+(or **help bar/moo**)
+
+#### dum
+
+A subcategory of moo, accessed **help bar moo dum**
+(or **help bar/moo/dum**)
+
+'''
+
+
+
This will result in this returned entry structure:
+
+
+
\ No newline at end of file
diff --git a/docs/1.0-dev/api/evennia.html b/docs/1.0-dev/api/evennia.html
index a6a1583b5b..86955be701 100644
--- a/docs/1.0-dev/api/evennia.html
+++ b/docs/1.0-dev/api/evennia.html
@@ -45,7 +45,6 @@
evennia.ANSIString
evennia.AccountDB
-
evennia.CHANNEL_HANDLER
evennia.ChannelDB
evennia.CmdSet
evennia.Command
@@ -177,7 +176,6 @@ with ‘q’, remove the break line and restart server when finished.
task_id (int): the global id for this task
+deferred (deferred): a reference to this task’s deferred
+
+
Property Attributes:
paused (bool): check if the deferred instance of a task has been paused.
+called(self): A task attribute to check if the deferred instance of a task has been called.
Execute the task (call its callback).
+If calling before timedelay, cancel the deferred instance affliated to this task.
+Remove the task from the dictionary of current tasks on a successful
+callback.
+
+
Returns
+
bool or any – Set to False if the task does not exist in task
+handler. Otherwise it will be the return of the task’s callback.
Call the callback of a task.
+Leave the task unaffected otherwise.
+This does not use the task’s deferred instance.
+The only requirement is that the task exist in task handler.
+
+
Returns
+
bool or any – Set to False if the task does not exist in task
+handler. Otherwise it will be the return of the task’s callback.
A task attribute to check if the deferred instance of a task has been called.
+
This exists to mock usage of a twisted deferred object.
+It will not set to True if Task.call has been called. This only happens if
+task’s deferred instance calls the callback.
+
+
Returns
+
bool –
+
+
True if the deferred instance of this task has called the callback.
False if the deferred instnace of this task has not called the callback.
Returns the global id for this task. For use with
+evennia.scripts.taskhandler.TASK_HANDLER.
+
+
Returns
+
task_id (int) – global task id for this task.
+
+
+
+
+
+
class evennia.scripts.taskhandler.TaskHandler[source]¶
Bases: object
A light singleton wrapper allowing to access permanent tasks.
When utils.delay is called, the task handler is used to create
-the task. If utils.delay is called with persistent=True, the
-task handler stores the new task and saves.
-
It’s easier to access these tasks (should it be necessary) using
-evennia.scripts.taskhandler.TASK_HANDLER, which contains one
-instance of this class, and use its add and remove methods.
+the task.
+
Task handler will automatically remove uncalled but canceled from task
+handler. By default this will not occur until a canceled task
+has been uncalled for 60 second after the time it should have been called.
+To adjust this time use TASK_HANDLER.stale_timeout. If stale_timeout is 0
+stale tasks will not be automatically removed.
+This is not done on a timer. I is done as new tasks are added or the load method is called.
By default this will not occur until a canceled task
+has been uncalled for 60 second after the time it should have been called.
+To adjust this time use TASK_HANDLER.stale_timeout.
@@ -77,20 +333,91 @@ It populates self.tasks according to the ServerConfig.
If the persistent kwarg is truthy:
+The callback, args and values for kwarg will be serialized. Type
+and attribute errors during the serialization will be logged,
+but will not throw exceptions.
+For persistent tasks do not use memory references in the callback
+function or arguments. After a restart those memory references are no
+longer accurate.
Parameters
-
timedelay (int or float) – time in sedconds before calling the callback.
+
timedelay (int or float) – time in seconds before calling the callback.
callback (function or instance method) – the callback itself
any (any) – any additional positional arguments to send to the callback
+
*args – positional arguments to pass to callback.
+
**kwargs –
keyword arguments to pass to callback.
+- persistent (bool, optional): persist the task (stores it).
+
+
Persistent key and value is removed from kwargs it will
+not be passed to callback.
+
+
-
Keyword Arguments
-
-
persistent (bool, optional) – persist the task (store it).
-
any (any) – additional keyword arguments to send to the callback
-
+
Returns
+
TaskHandlerTask –
+
+
An object to represent a task.
Reference evennia.scripts.taskhandler.TaskHandlerTask for complete details.
Call the callback of a task.
+Leave the task unaffected otherwise.
+This does not use the task’s deferred instance.
+The only requirement is that the task exist in task handler.
+
+
Parameters
+
task_id (int) – an existing task ID.
+
+
Returns
+
bool or any – Set to False if the task does not exist in task
+handler. Otherwise it will be the return of the task’s callback.
+
-
-
Note
-
A non-persistent task doesn’t have a task_id, it is not stored
-in the TaskHandler.
Execute the task (call its callback).
+If calling before timedelay cancel the deferred instance affliated to this task.
+Remove the task from the dictionary of current tasks on a successful
+callback.
Parameters
task_id (int) – a valid task ID.
+
Returns
+
bool or any – Set to False if the task does not exist in task
+handler. Otherwise it will be the return of the task’s callback.
Create the delayed tasks for the persistent tasks.
-
-
Note
-
This method should be automatically called when Evennia starts.
-
+
Create the delayed tasks for the persistent tasks.
+This method should be automatically called when Evennia starts.
diff --git a/docs/1.0-dev/api/evennia.typeclasses.attributes.html b/docs/1.0-dev/api/evennia.typeclasses.attributes.html
index 0e3eec4f90..85c621d2e8 100644
--- a/docs/1.0-dev/api/evennia.typeclasses.attributes.html
+++ b/docs/1.0-dev/api/evennia.typeclasses.attributes.html
@@ -987,7 +987,8 @@ permission lock will be checked before returning each
looked-after Attribute.
default_access (bool, optional) – If no attrread lock is set on
object, this determines if the lock should then be passed or not.
-
return_list (bool, optional) –
+
return_list (bool, optional) – Always return a list, also if there is only
+one or zero matches found.
Returns
@@ -1187,20 +1188,31 @@ Attributes has no lock of type attrread defined on them.
Initialize the nick templates for matching and remapping a string.
Parameters
-
in_template (str) – The template to be used for nick recognition.
-
out_template (str) – The template to be used to replace the string
-matched by the in_template.
+
pattern (str) – The pattern to be used for nick recognition. This will
+be parsed for shell patterns into a regex, unless pattern_is_regex
+is True, in which case it must be an already valid regex string. In
+this case, instead of $N, numbered arguments must instead be given
+as matching groups named as argN, such as (?P<arg1>.+?).
+
replacement (str) – The template to be used to replace the string
+matched by the pattern. This can contain $N markers and is never
+parsed into a regex.
+
pattern_is_regex (bool) – If set, pattern is a full regex string
+instead of containing shell patterns.
Returns
-
regex (regex) – Regex to match against strings
-template (str): Template with markers **{arg1}, {arg2}**, etc for
-replacement using the standard .format method.
+
regex, template (str) –
+
+
Regex to match against strings and template
with markers **{arg1}, {arg2}**, etc for replacement using the standard
+.format method.
+
+
+
Raises
@@ -1209,6 +1221,12 @@ replacement using the standard .format method.
+
Examples
+
+
pattern (shell syntax): “grin $1”
+
pattern (regex): “grin (?P<arg1.+?>)”
+
replacement: “emote gives a wicked grin to $1”
+
@@ -1218,12 +1236,14 @@ replacement using the standard .format method.
Parameters
-
string (str) – The input string to processj
+
string (str) – The input string to process
template_regex (regex) – A template regex created with
initialize_nick_template.
outtemplate (str) – The template to which to map the matches
produced by the template_regex. This should have $1, $2,
-etc to match the regex.
+etc to match the template-regex. Un-found $N-markers (possible if
+the regex has optional matching groups) are replaced with empty
+strings.
@@ -1292,25 +1312,51 @@ a string.
kwargs (any, optional) – These are passed on to AttributeHandler.get.
+
Returns
+
str or tuple – The nick replacement string or nick tuple.
key (str) – A key (or template) for the nick to match for.
-
replacement (str) – The string (or template) to replace key with (the “nickname”).
+
pattern (str) – A pattern to match for. This will be parsed for
+shell patterns using the fnmatch library and can contain
+$N-markers to indicate the locations of arguments to catch. If
+pattern_is_regex=True, this must instead be a valid regular
+expression and the $N-markers must be named argN that matches
+numbered regex groups (see examples).
+
replacement (str) – The string (or template) to replace key with
+(the “nickname”). This may contain $N markers to indicate where to
+place the argument-matches
category (str, optional) – the category within which to
retrieve the nick. The “inputline” means replacing data
sent by the user.
-
kwargs (any, optional) – These are passed on to AttributeHandler.get.
+
pattern_is_regex (bool) – If True, the pattern will be parsed as a
+raw regex string. Instead of using $N markers in this string, one
+then must mark numbered arguments as a named regex-groupd named argN.
+For example, (?P<arg1>.+?) will match the behavior of using $1
+in the shell pattern.
+
**kwargs (any, optional) – These are passed on to AttributeHandler.get.
+
Notes
+
For most cases, the shell-pattern is much shorter and easier. The
+regex pattern form can be useful for more complex matchings though,
+for example in order to add optional arguments, such as with
+(?P<argN>.*?).
+
Example
+
+
pattern (default shell syntax): “gr $1 at $2”
+
pattern (with pattern_is_regex=True): r”gr (?P<arg1>.+?) at (?P<arg2>.+?)”
+
replacement: “emote With a flourish, $1 grins at $2.”
+
diff --git a/docs/1.0-dev/api/evennia.utils.create.html b/docs/1.0-dev/api/evennia.utils.create.html
index 6e66a9afa6..42808c83f9 100644
--- a/docs/1.0-dev/api/evennia.utils.create.html
+++ b/docs/1.0-dev/api/evennia.utils.create.html
@@ -158,21 +158,20 @@ in-game setting information and so on.
Create a new communication Msg. Msgs represent a unit of
database-persistent communication between entites.
Parameters
-
senderobj (Object or Account) – The entity sending the Msg.
+
senderobj (Object, Account, Script, str or list) – The entity (or
+
sending the Msg. If a str (entities)) – for an external sender type.
+
is the id-string (this) – for an external sender type.
message (str) – Text with the message. Eventual headers, titles
etc should all be included in this text string. Formatting
will be retained.
-
channels (Channel, key or list) – A channel or a list of channels to
-send to. The channels may be actual channel objects or their
-unique key strings.
-
receivers (Object, Account, str or list) – An Account/Object to send
-to, or a list of them. May be Account objects or accountnames.
+
receivers (Object, Account or list) – An Account/Object to send
+to, or a list of them.
locks (str) – Lock definition string.
tags (list) – A list of tags or tuples (tag, category).
header (str) – Mime-type or other optional information for the message
-search_index_entry = {'aliases': 'yes n y a no __nomatch_command abort', 'category': 'general', 'key': '__noinput_command', 'tags': '', 'text': '\n Handle a prompt for yes or no. Press [return] for the default choice.\n\n '}¶
+search_index_entry = {'aliases': '__nomatch_command a no n y abort yes', 'category': 'general', 'key': '__noinput_command', 'tags': '', 'text': '\n Handle a prompt for yes or no. Press [return] for the default choice.\n\n '}¶
+evennia.utils.evmenu.ask_yes_no(caller, prompt='Yes or No {options}?', yes_action='Yes', no_action='No', default=None, allow_abort=False, session=None, *args, **kwargs)[source]¶
A helper question for asking a simple yes/no question. This will cause
the system to pause and wait for input from the player.
@@ -1018,8 +1018,9 @@ If a string, this string will be echoed back to the caller.
with (caller, *args, **kwargs) when the No-choice is made.
If a string, this string will be echoed back to the caller.
default (str optional) – This is what the user will get if they just press the
-return key without giving any input. One of ‘N’, ‘Y’, ‘A’ or ‘None’
-for no default. If ‘A’ is given, allow_abort is auto-set.
+return key without giving any input. One of ‘N’, ‘Y’, ‘A’ or None
+for no default (an explicit choice must be given). If ‘A’ (abort)
+is given, allow_abort kwarg is ignored and assumed set.
allow_abort (bool, optional) – If set, the ‘A(bort)’ option is available
(a third option meaning neither yes or no but just exits the prompt).
session (Session, optional) – This allows to specify the
@@ -1027,13 +1028,12 @@ session to send the prompt to. It’s usually only needed if callercaller.ndb._yes_no_question.session.
-
*args – These are passed into the callables.
-
**kwargs –
These are passed into the callables.
-
+
*args – Additional arguments passed on into callables.
+
**kwargs – Additional keyword args passed on into callables.
Raises
-
RuntimeError, FooError – If default and allow_abort clashes.
+
RuntimeError, FooError – If default and allow_abort clashes.
Example
diff --git a/docs/1.0-dev/api/evennia.utils.evmore.html b/docs/1.0-dev/api/evennia.utils.evmore.html
index 67ea94b4eb..35621245ab 100644
--- a/docs/1.0-dev/api/evennia.utils.evmore.html
+++ b/docs/1.0-dev/api/evennia.utils.evmore.html
@@ -75,7 +75,7 @@ the caller.msg() construct every time the page is updated.
@@ -101,7 +101,7 @@ the caller.msg() construct every time the page is updated.
-search_index_entry = {'aliases': 'back end top b t n e a next abort q quit', 'category': 'general', 'key': '__noinput_command', 'tags': '', 'text': '\n Manipulate the text paging\n '}¶
+search_index_entry = {'aliases': 'next quit a q e top end back n t abort b', 'category': 'general', 'key': '__noinput_command', 'tags': '', 'text': '\n Manipulate the text paging\n '}¶
diff --git a/docs/1.0-dev/api/evennia.utils.logger.html b/docs/1.0-dev/api/evennia.utils.logger.html
index 7c2c9f0f8f..359695ce42 100644
--- a/docs/1.0-dev/api/evennia.utils.logger.html
+++ b/docs/1.0-dev/api/evennia.utils.logger.html
@@ -319,7 +319,7 @@ to preserve a continuous chat history for channel log files.
diff --git a/docs/1.0-dev/api/evennia.utils.search.html b/docs/1.0-dev/api/evennia.utils.search.html
index b40056f90f..c9d9997796 100644
--- a/docs/1.0-dev/api/evennia.utils.search.html
+++ b/docs/1.0-dev/api/evennia.utils.search.html
@@ -147,7 +147,7 @@ one of the arguments must be given to do a search.
Parameters
-
sender (Object or Account, optional) – Get messages sent by a particular account or object
+
sender (Object, Account or Script, optional) – Get messages sent by a particular sender.
receiver (Object, Account or Channel, optional) – Get messages
received by a certain account,object or channel
freetext (str) – Search for a text string in a message. NOTE:
@@ -159,7 +159,7 @@ always gives only one match.
Returns
-
messages (list or Msg) – A list of message matches or a single match if dbref was given.
+
Queryset – Message matches.
diff --git a/docs/1.0-dev/api/evennia.utils.utils.html b/docs/1.0-dev/api/evennia.utils.utils.html
index 567c6962e9..cc686528f0 100644
--- a/docs/1.0-dev/api/evennia.utils.utils.html
+++ b/docs/1.0-dev/api/evennia.utils.utils.html
@@ -160,16 +160,18 @@ suffix, the suffix will be dropped.
Safely clean all whitespace at the left of a paragraph.
Parameters
text (str) – The text to dedent.
-
baseline_index (int or None, optional) – Which row to use as a ‘base’
+
baseline_index (int, optional) – Which row to use as a ‘base’
for the indentation. Lines will be dedented to this level but
no further. If None, indent so as to completely deindent the
least indented text.
+
indent (int, optional) – If given, force all lines to this indent.
+This bypasses baseline_index.
Returns
@@ -678,7 +680,7 @@ shortcut to having to use the full backend name.
persistent (bool) – Make the delay persistent over a reboot or reload.
-
any – Any other keywords will be use as keyword arguments to callback.
+
persistent (bool, optional) – Should make the delay persistent
+over a reboot or reload. Defaults to False.
+
any (any) – Will be used as keyword arguments to callback.
Returns
-
deferred – Will fire with callback after timedelay seconds. Note that
-if timedelay() is used in the
-commandhandler callback chain, the callback chain can be
-defined directly in the command body and don’t need to be
-specified here.
+
deferred or int –
+
+
If **persistent** kwarg is False, return deferred
that will fire with callback after timedelay seconds. Note that
+if timedelay() is used in the commandhandler callback chain, the
+callback chain can be defined directly in the command body and
+don’t need to be specified here. Reference twisted.internet.defer.Deferred.
+If persistent kwarg is set, return the task’s ID as an integer. This is
+intended for use with **evennia.scripts.taskhandler.TASK_HANDLER**
+.do_task and .remove methods.
+
+
+
Notes
The task handler (evennia.scripts.taskhandler.TASK_HANDLER) will
be called for persistent or non-persistent tasks.
If persistent is set to True, the callback, its arguments
-and other keyword arguments will be saved in the database,
+and other keyword arguments will be saved (serialized) in the database,
assuming they can be. The callback will be executed even after
a server restart/reload, taking into account the specified delay
-(and server down time).
+(and server down time).
+Keep in mind that persistent tasks arguments and callback should not
+use memory references.
+If persistent is set to True the delay function will return an int
+which is the task’s id itended for use with TASK_HANDLER’s do_task
+and remove methods.
+
All task’s whose time delays have passed will be called on server startup.
@@ -890,7 +906,7 @@ parsed and imported. Returns None and logs error if import fail
already imported module object (e.g. models)
This helper function makes a ‘grid’ output, where it distributes the given
string-elements as evenly as possible to fill out the given width.
will not work well if the variation of length is very big!
@@ -1258,7 +1275,7 @@ decorations in the grid, such as horizontal bars.
Returns
-
gridstr – The grid as a list of ready-formatted rows. We return it
+
list – The grid as a list of ready-formatted rows. We return it
like this to make it easier to insert decorations between rows, such
as horizontal bars.